facebook/react

can React support feature like keep-alive in Vue?

zengjialuo opened this issue · 20 comments

i found this issue: #4770, and @sophiebits said that React never reuses an instance after it's been unmounted.

does it means that React will never support feature like keep-alive in Vue? or there is other way to maintain component's state?

Can you describe what this feature is for someone who doesn’t know Vue, and how you imagine it working in React?

As Vue's API said:

When wrapped around a dynamic component, <keep-alive> caches the inactive component instances without destroying them. Similar to <transition>, <keep-alive> is an abstract component: it doesn’t render a DOM element itself, and doesn’t show up in the component parent chain.

When a component is toggled inside <keep-alive>, its activated and deactivated lifecycle hooks will be invoked accordingly.

Primarily used with preserve component state or avoid re-rendering.

basic usage:

<keep-alive>
  <!-- page-component matched by the route will render here -->
  <router-view></router-view>
</keep-alive>

so, with <keep-alive>, when user switch between pages, these pages will not be refreshed and their status will be preserved.

It will give users a great experience in some scenarios like news webapp.

+1

keep the component in cache when route to next component,
instead of recreating a new one, reuse the cached component, avoid refetching the data via network to fulfill the component.

act some like a stack.

In React this is usually solved in one of the two ways:

  • Keep data cached separately from the component. For example, you can lift state up to an ancestor that doesn't get mounted, or put it in a sideways cache like Redux. We're also working on a first-class API support for this.

  • Don't unmount the views you want to “keep alive”, just hide them with style={{display: 'none'}}.

  • The first way: this way can make things done, but, sometimes it's too heavy. E.g., we use sate to record whether a component is exposed or not. It's suitable to use state, and seems be strange to lift these state up or store them with Redux.
  • The second way: the problem is that just hide the element is not enough, some event listeners should be removed, usually implemented in componentWillUnmount

@gaearon Could you please explain some details about the first-class API you mentioned?

The second way: the problem is that just hide the element is not enough, some event listeners should be removed, usually implemented in componentWillUnmount

you can also intercept the change (prop/state change) that leads to visible -> display: none and do your event listeners clean up easily, and when going from display: none to visible attach the listeners again. you are effectively making the component inactive and maintaining component state by doing display: none, any other use case for keeping an instance alive?

@bjrmatos , your suggestion is practical. There are ways to work around. But i think they are not so perfect or clean. since when you write a component, you should concern both the React life-cycle and something like page life-cycle. Doesn't it?

your suggestion is practical. There are ways to work around. But i think they are not so perfect or clean

my suggestion is not a work around, it is the react way of doing things.. reacting to state/props changes and do something is the whole react model. i would say it is clean because it is the way how things should be done with react.

you should concern both the React life-cycle and something like page life-cycle. Doesn't it?

to me what you are describing is just a react component life-cycle concern, a component is a component no matter if it is a whole page or not and you need to use the available lifecycle hooks to do your thing, you can even create a PageContainer component that has the visible -> display: none, display:none -> visible logic and never bother with this in your app again.

don't know about the Vue approach but a keep alive feature in react sounds dirty, because there is the traditional way of doing the same with just reacting to changes in normal react lifecycles (componetWillReceiveProps)

Could you please explain some details about the first-class API you mentioned?

We’ll post it soon at https://github.com/reactjs/rfcs. Stay tuned for PRs there.

@bjrmatos the keypoint is how the PageComponent inform its descendant component to remove event listeners except for all component has a visible prop?

Hello @gaearon, I'm a big fan of React butI had to work with Vue at work. I'm working with Vue for months now and I still prefere React. But today I found about keep-alive and for the first time I found something better than React. Just because React does not have this feature. Anyway, I think it's something really increreble and maybe something that React could do too (I don't know).

Here the doc to help you to know more about it: https://br.vuejs.org/v2/guide/components-dynamic-async.html#keep-alive-with-Dynamic-Components

Restrictions like this sound quite artificial:

Note, <keep-alive> is designed for the case where it has one direct child component that is being toggled. It does not work if you have v-for inside it. When there are multiple conditional children, as above, <keep-alive> requires that only one child is rendered at a time.

We try to avoid introducing APIs in React that work in a very limited subset of cases but then can’t work in others. This might seem “nice” at first but then you need to change the code a little bit, and hit a wall because you have to change the whole approach. Instead, we prefer a more limited set of APIs that work the same way in all circumstances. Then you can learn them once and apply the same techniques everywhere without having to tweak the code whenever you run into a limitation in some convenience shortcut.

The approaches I mentioned above (either lifting state up to a parent component or hiding/showing with style={{display: visible ? 'block' : 'none'}}) don’t suffer from these limitations and work regardless of how many children you have. The lifecycles are also handled in a predictable way: either you get a full unmount and then re-mount of children (with state preserved in the parent), or the children stay mounted all the way through (and thus don’t need special lifecycle calls). This is consistent with how React works in general and doesn’t require special behavior.

so, with <keep-alive>, when user switch between pages, these pages will not be refreshed and their status will be preserved.

This sounds like a recipe for memory leaks to me. If you keep all the components instantiated forever, your app will keep eating memory as you navigate between the pages. Just caching data alone would be okay (and that’s exactly what React lets you do), but keeping component instances and state sounds like it would create problems as the app becomes more complex (but at that point it’s too late to fix because the app heavily relies on this pattern). I don’t think we’d want to introduce APIs that encourage irresponsible memory usage into React.

That said we definitely want to make the caching use case easier (as a few people rightly noted, brining in something like Redux just to cache network responses is overkill). We’re working on something for this — stay tuned for announcements.

Thanks everyone for your links and the discussion! Hope my answer wasn’t too frustrating. I think the “React way” might feel annoying if you’re used to having a helper like <keep-alive> but I also think that having used React a bit more you’ll appreciate the explicitness and control that React gives you in this case.

Fair enough.Thank you for your explanation @gaearon. I understand... everything it's trade offs after all.

good

@zengjialuo @gaearon @bjrmatos @ninahaack
I wrote a component using the React.createPortal API (react-keep-alive), it implements similar functionality to <keep-alive> in Vue.

Now, I have some doubts about the lifecycle of react-keep-alive. In the first version, I added two lifecycles componentDidActivate and componentWillUnactivate, which of course was affected by Vue. In the current version I deleted these two lifecycles and replaced the new lifecycle of the old version with componentDidMount and componentWillUnmount.

This can be confusing for users, such as [Lifecycle and events] (https://codesandbox.io/s/q1xprn1qq) and [Control cache] (https://codesandbox.io/s/llp50vxnq7), via `bindLifecycle The life cycle of the component after the high-order function package will be:

Mounting: componentWillMount -> componentDidMount
Unmounting: componentWillUnmount

Activating: componentWillUpdate -> componentDidMount(This is where the user is confused. I am tampering with the life cycle when I activate it.)
Unactivating: componentWillUnmount

I used the life cycle of the old version of React, and it would be confusing if the user used it like this; but the new life cycle may not be a problem, I am not sure.

i hope react can support keep-alive

I have my implementation react-activation and here is the Online Demo

Because React will unload components that are in the intrinsic component hierarchy, we can extract the components in <KeepAlive>, that is, its children prop, and render them into a component that will not be unloaded, drag the rendered content back to the <KeepAlive> using the DOM operation

The principle is easy to say but it's not a good idea...The implementation destroys the original rendering level and brings some bad effects.

Simplest Implementation as follows

import React, { Component, createContext } from 'react'

const { Provider, Consumer } = createContext()
const withScope = WrappedCompoennt => props => (
  <Consumer>{keep => <WrappedCompoennt {...props} keep={keep} />}</Consumer>
)

export class AliveScope extends Component {
  nodes = {}
  state = {}

  keep = (id, children) =>
    new Promise(resolve =>
      this.setState(
        {
          [id]: { id, children }
        },
        () => resolve(this.nodes[id])
      )
    )

  render() {
    return (
      <Provider value={this.keep}>
        {this.props.children}
        {Object.values(this.state).map(({ id, children }) => (
          <div
            key={id}
            ref={node => {
              this.nodes[id] = node
            }}
          >
            {children} {/* render them into a component that will not be unloaded */}
          </div>
        ))}
      </Provider>
    )
  }
}

@withScope
class KeepAlive extends Component {
  constructor(props) {
    super(props)
    this.init(props)
  }

  init = async ({ id, children, keep }) => {
    const realContent = await keep(id, children) // extract the children in <KeepAlive>
    this.placeholder.appendChild(realContent) // drag the rendered content back using the DOM operation
  }

  render() {
    return (
      <div
        ref={node => {
          this.placeholder = node
        }}
      />
    )
  }
}

export default KeepAlive

I love Vue than React, Vue is so great!!

@bjrmatos的关键是PageComponent如何通知其后代组件除去所有组件都有可见道具的事件侦听器?

Sometimes it's not a good thing that brother's behavior Vue encapsulates perfectly. The best way to build it is to fit your own framework, isn't it?