简体   繁体   English

如何在挂载/卸载之间保持 React 组件 state?

[英]How to keep React component state between mount/unmount?

I have a simple component <StatefulView> that maintains an internal state. I have another component <App> that toggles whether or not <StatefulView> is rendered.我有一个简单的组件<StatefulView>维护一个内部 state。我有另一个组件<App>切换是否呈现<StatefulView>

However, I want to keep <StatefulView> 's internal state between mounting/unmouting.但是,我想在安装/卸载之间保留<StatefulView>的内部 state 。

I figured I could instantiate the component in <App> and then control whether its rendered/mounted.我想我可以在<App>中实例化该组件,然后控制它是否呈现/安装。

var StatefulView = React.createClass({
  getInitialState: function() {
    return {
      count: 0
    }
  },
  inc: function() {
    this.setState({count: this.state.count+1})
  },
  render: function() {
    return (
        <div>
          <button onClick={this.inc}>inc</button>
          <div>count:{this.state.count}</div>
        </div>
    )
  }
});

var App = React.createClass({
  getInitialState: function() {
    return {
      show: true,
      component: <StatefulView/>
    }
  },
  toggle: function() {
    this.setState({show: !this.state.show})
  },
  render: function() {
    var content = this.state.show ? this.state.component : false
    return (
      <div>
        <button onClick={this.toggle}>toggle</button>
        {content}
      </div>
    )
  }
});

This apparently doesnt work and a new <StatefulView> is created on each toggle.这显然不起作用,并且在每次切换时都会创建一个新的<StatefulView>

Here's a JSFiddle .这是一个JSFiddle

Is there a way to hang on to the same component after it is unmounted so it can be re-mounted?有没有办法在卸载后挂在同一个组件上以便重新安装?

Since you can't keep the state in the component itself when it unmounts, you have to decide where else it should be saved.由于您无法在卸载时将状态保留在组件本身中,因此您必须决定将其保存在何处。

These are your options:这些是您的选择:

  1. React state in parent : If a parent component remains mounted, maybe it should be the owner of the state or could provide an initial state to an uncontrolled component below. React state in parent :如果父组件保持挂载,它可能应该是状态的所有者,或者可以为下面的不受控制的组件提供初始状态。 You can pass the value back up before the component unmounts.您可以在卸载组件之前将值传递回来。 With React context you can hoist the state to the very top of your app (see eg unstated ).使用 React 上下文,您可以将状态提升到应用程序的最顶部(参见例如unstated )。
  2. Outside of React : Eg use-local-storage-state .在 React 之外:例如use-local-storage-state Note that you might need to manually reset the state inbetween tests.请注意,您可能需要在测试之间手动重置状态。 Other options are query params in the URL, state management libraries like MobX or Redux, etc.其他选项是 URL 中的查询参数、状态管理库(如 MobX 或 Redux 等)。

I've you're looking for an easy solution where the data is persisted outside of React, this Hook might come in handy:我一直在寻找一个简单的解决方案,其中数据在 React 之外持久化,这个 Hook 可能会派上用场:

const memoryState = {};

function useMemoryState(key, initialState) {
  const [state, setState] = useState(() => {
    const hasMemoryValue = Object.prototype.hasOwnProperty.call(memoryState, key);
    if (hasMemoryValue) {
      return memoryState[key]
    } else {
      return typeof initialState === 'function' ? initialState() : initialState;
    }
  });

  function onChange(nextState) {
    memoryState[key] = nextState;
    setState(nextState);
  }

  return [state, onChange];
}

Usage:用法:

const [todos, setTodos] = useMemoryState('todos', ['Buy milk']);

OK.好的。 So after talking with a bunch of people, it turns out that there is no way to save an instance of a component.所以和一堆人聊了聊,发现一个组件的实例是没有办法保存的。 Thus, we HAVE to save it elsewhere.因此,我们必须将其保存在其他地方。

1) The most obvious place to save the state is within the parent component. 1) 保存状态最明显的地方是在父组件内。

This isn't an option for me because I'm trying to push and pop views from a UINavigationController-like object.这对我来说不是一个选择,因为我试图从类似 UINavigationController 的对象中推送和弹出视图。

2) You can save the state elsewhere, like a Flux store, or in some global object. 2)您可以将状态保存在其他地方,例如 Flux 商店或某个全局对象。

This also isn't the best option for me because it would be a nightmare keeping track of which data belongs to which view in which Navigation controller, etc.这对我来说也不是最好的选择,因为在哪个导航控制器中跟踪哪些数据属于哪个视图,这将是一场噩梦。

3) Pass a mutable object to save and restore the state from. 3)传递一个可变对象来保存和恢复状态。

This was a suggestion I found while commenting in various issue tickets on React's Github repo.这是我在 React 的 Github 存储库的各种问题单中发表评论时发现的一个建议。 This seems to be the way to go for me because I can create a mutable object and pass it as props, then re-render the same object with the same mutable prop.这似乎是我要走的路,因为我可以创建一个可变对象并将其作为道具传递,然后使用相同的可变道具重新渲染相同的对象。

I've actually modified it a little to be more generalized and I'm using functions instead of mutable objects.我实际上对其进行了一些修改以使其更加通用,并且我使用的是函数而不是可变对象。 I think this is more sane -- immutable data is always preferable to me.我认为这更明智——不可变数据总是对我更可取。 Here's what I'm doing:这是我在做什么:

function createInstance() {
  var func = null;
  return {
    save: function(f) {
      func = f;
    },
    restore: function(context) {
      func && func(context);
    }
  }
}

Now in getInitialState I'm creating a new instance for the component:现在在getInitialState我正在为组件创建一个新实例:

component: <StatefulView instance={createInstance()}/>

Then in the StatefulView I just need to save and restore in componentWillMount and componentWillUnmount .然后在StatefulView我只需要在componentWillMountcomponentWillUnmount保存和恢复。

componentWillMount: function() {
  this.props.instance.restore(this)
},
componentWillUnmount: function() {
  var state = this.state
  this.props.instance.save(function(ctx){
    ctx.setState(state)
  })
}

And that's it.就是这样。 It works really well for me.它对我来说非常有效。 Now I can treat the components as if they're instances :)现在我可以将组件视为实例:)

For those who are just reading this in 2019 or beyond, a lot of details have already been given in the other answers, but there are a couple of things that I want to emphasize here:对于那些刚刚在 2019 年或以后阅读本文的人,其他答案中已经给出了很多细节,但我想在这里强调几点:

  • Saving state in some store (Redux) or Context is probably the best solution.在某个商店 (Redux) 或 Context 中保存状态可能是最好的解决方案。
  • Storing in global variable will only work if there is ever only ONE instance of your component at a time.仅当您的组件一次只有一个实例时,存储在全局变量中才有效。 (How will each instance know which stored state is theirs?) (每个实例如何知道哪个存储状态是他们的?)
  • Saving in localStorage has just the same caveats as global variable, and since we're not talking about restoring state on browser refresh, it doesn't seem to add any benefit.保存在 localStorage 中与全局变量有相同的警告,而且由于我们不是在谈论在浏览器刷新时恢复状态,它似乎没有增加任何好处。

Hmm, you could use localStorage , or AsyncStorage if React Native.嗯,如果 React Native,你可以使用localStorageAsyncStorage

React Web反应网

componentWillUnmount() {
  localStorage.setItem('someSavedState', JSON.stringify(this.state))
}

Then later that day or 2 seconds later:然后当天晚些时候或 2 秒后:

componentWillMount() {
  const rehydrate = JSON.parse(localStorage.getItem('someSavedState'))
  this.setState(rehydrate)
}

React Native反应本机

import { AsyncStorage } from 'react-native'

async componentWillMount() {
  try {
    const result = await AsyncStorage.setItem('someSavedState', JSON.stringify(this.state))
    return result
  } catch (e) {
    return null
  }
}

Then later that day or 2 seconds later:然后当天晚些时候或 2 秒后:

async componentWillMount() {
  try {
    const data = await AsyncStorage.getItem('someSavedState')
    const rehydrate = JSON.parse(data)
    return this.setState(rehydrate)
  } catch (e) {
    return null
  }
}

You could also use Redux and pass the data into the child component when it renders.您还可以使用Redux并在呈现时将数据传递给子组件。 You might benefit from researching serializing state and the second parameter of the Redux createStore function which is for rehydrating an initial state.您可能会从研究serializing状态和 Redux createStore函数的第二个参数中受益,该参数用于恢复初始状态。

Just note that JSON.stringify() is an expensive operation, so you should not do it on keypress, etc. If you have concern, investigate debouncing.请注意JSON.stringify()是一项昂贵的操作,因此您不应该在按键等上执行此操作。如果您有顾虑,请调查去抖动。

I'm late to the party but if you're using Redux.我迟到了,但如果你使用的是 Redux。 You'll get that behaviour almost out of the box with redux-persist.使用 redux-persist 几乎可以立即获得这种行为。 Just add its autoRehydrate to the store then it will be listening to REHYDRATE actions that will automatically restore previous state of the component (from web storage).只需将它的autoRehydrate添加到存储中,然后它就会监听REHYDRATE操作, REHYDRATE操作将自动恢复组件的先前状态(从 Web 存储)。

I'm not an expert in React but particularly your case could be solved very cleanly without any mutable objects.我不是 React 专家,但特别是您的情况可以在没有任何可变对象的情况下非常干净地解决。

var StatefulView = React.createClass({
  getInitialState: function() {
    return {
      count: 0
    }
  },
  inc: function() {
    this.setState({count: this.state.count+1})
  },
  render: function() {
      return !this.props.show ? null : (
        <div>
          <button onClick={this.inc}>inc</button>
          <div>count:{this.state.count}</div>
        </div>
    )

  }
});

var App = React.createClass({
  getInitialState: function() {
    return {
      show: true,
      component: StatefulView
    }
  },
  toggle: function() {
    this.setState({show: !this.state.show})
  },
  render: function() {
    return (
      <div>
        <button onClick={this.toggle}>toggle</button>
        <this.state.component show={this.state.show}/>
      </div>
    )
  }
});

ReactDOM.render(
  <App/>,
  document.getElementById('container')
);

You can see it at jsfiddle .你可以在jsfiddle看到它。

An easy way of caching state between renders would be to use the fact that modules exported form closure over the file that you are working in.在渲染之间缓存状态的一种简单方法是使用模块在您正在处理的文件上导出表单闭包的事实。

Using a useEffect hook you can specify logic that happens on component dismount (ie update a variable that is closed over at the module level).使用useEffect钩子可以指定在组件卸载时发生的逻辑(即更新在模块级别关闭的变量)。 This works because imported modules are cached, meaning that the closure created on import never disappears.这是有效的,因为导入的模块被缓存,这意味着在导入时创建的闭包永远不会消失。 I'm not sure if this is a GOOD approach or not, but works in cases where a file is only imported once (otherwise the cachedState would be shared across all instances of the default rendered component)我不确定这是否是一种好的方法,但在文件只导入一次的情况下有效(否则cachedState将在默认呈现组件的所有实例之间共享)

var cachedState

export default () => {
  const [state, setState] = useState(cachedState || {})

  useEffect(() => {
    return () => cachedState = state
  })

  return (...)
}

If you want to be able to unmount and mount while maintaining state, you will need to store the count in App and pass down the count via props.如果您希望能够在保持状态的同时卸载和挂载,则需要将计数存储在App并通过 props 传递计数。

(When doing this, you should be calling a toggle function inside of App , you want the functionality which changes data to live with the data). (这样做时,您应该在App内部调用切换函数,您希望更改数据的功能与数据一起存在)。

I will modify your fiddle to be functional and update my answer with it.我将修改您的小提琴以使其正常工作并用它更新我的答案。

I've made a simple NPM package exactly for this purpose.为此,我制作了一个简单的 NPM 包。 You can find it here:你可以在这里找到它:

https://www.npmjs.com/package/react-component-state-cache https://www.npmjs.com/package/react-component-state-cache

Usage is simple.用法很简单。 First, include the component somewhere high up in your component tree, like this首先,将组件包含在组件树中的某个位置,就像这样

import React from 'react'
import {ComponentStateCache} from 'react-component-state-cache'
import {Provider} from 'react-redux' // for example
import MyApp from './MyApp.js'

class AppWrapper extends React.Component {
    render() {
        return (
            <Provider store={this.store}>
                <ComponentStateCache>
                    <MyApp />
                </ComponentStateCache>
            </Provider>
        )
    }
}

then, you can use it in any of your components as follows:然后,您可以在任何组件中使用它,如下所示:

import React from 'react'
import { withComponentStateCache } from 'react-component-state-cache'

class X extends React.Component {

    constructor(props) {
        super(props)

        this.state = {
            // anything, really.
        }
    }

    componentDidMount() {
        // Restore the component state as it was stored for key '35' in section 'lesson'
        //
        // You can choose 'section' and 'key' freely, of course.
        this.setState(this.props.componentstate.get('lesson', 35))
    }

    componentDidUnmount() {
         // store this state's component in section 'lesson' with key '35'
        this.props.componentstate.set('lesson', 35, this.state)
    }

}

export default withComponentStateCache(X)

That's it.就是这样。 Easy peasy.十分简单。

I came across this post looking for a way to build the component state over time, ie, with each additional page from the backend.我遇到了这篇文章,正在寻找一种随着时间的推移构建组件状态的方法,即后端的每个附加页面。

I use a persistor and redux.我使用持久器和 redux。 However, for this state I wanted to keep it local to the component.但是,对于这种状态,我希望将其保留在组件的本地。 What ended up working: useRef .结果是什么: useRef I'm a bit surprised no one mentioned it so I could be missing an important part of the question.我有点惊讶没有人提到它,所以我可能会错过问题的一个重要部分。 Notwithstanding, the ref persists between renders of React's virtual DOM.尽管如此, ref在 React 的虚拟 DOM 的渲染之间仍然存在。

With that in place, I can build my cache over-time, watch the component update (aka re-render), but not worry about "throwing-out" previous api data that remains relevant for the component.有了这个,我可以随着时间的推移构建我的缓存,观察组件更新(又名重新渲染),但不用担心“抛出”与组件相关的先前 api 数据。

  const cacheRef = useRef([]);
  const [page, setPage] = useState(() => 0);

  // this could be part of the `useEffect` hook instead as I have here
  const getMoreData = useCallback(
    async (pageProp, limit = 15) => {

      const newData = await getData({
        sources,
        page: pageProp,
        limit,
      });

      cacheRef.current = [...(cacheRef.current || []), ...newData];
      
    },
    [page, sources],
  );

  useEffect(() => {
    const atCacheCapacity = cacheRef.current.length >= MAX;
    if (!atCacheCapacity) {
      getMoreData(page + 1);
      setPage(page + 1);
    }
  }, [MAX, getMoreData, page]);

The component is tricked into re-rendering by tracking a local state that changes with the increasing size of the cache.通过跟踪随着缓存大小的增加而变化的本地状态,该组件被诱骗进行重新渲染。 I don't copy the ref data on the DOM into the component's local state, just some sort of summary eg, length.我不会将 DOM 上的 ref 数据复制到组件的本地状态,只是某种摘要,例如长度。

This won't suffice for the scenario of the thread opener, but maybe for other people stumbling on this thread: Depending on how much persistence you need and how complex the data is you want to store, a query parameter might also suffice.这对于线程开启者的场景来说是不够的,但对于其他在这个线程上绊倒的人来说可能是不够的:根据您需要多少持久性以及您想要存储的数据有多复杂,查询参数可能也足够了。

For example, if you just want to keep a bool hideItems between window resizing operations, it will retain if you append it in the manner of my.example.com/myPage?hideItems=true .例如,如果您只想在 window 调整大小操作之间保留一个 bool hideItems ,如果您 append 它将以my.example.com/myPage?hideItems=true的方式保留。 You've got to evaluate the parameter on rendering inside you page/component, eg with NextJS it would be您必须评估页面/组件内部呈现的参数,例如,使用NextJS它将是

const router = useRouter()
const hide = router.query.hideItems

In my case, I select items to payment and save it to state in redux store, when my component is unmout, I want to set state to new list.在我的例子中,我将 select 项目付款并保存到 redux 商店中的 state,当我的组件被卸载时,我想将 state 设置为新列表。 But I have a special case after I payment success and redirect to another page, I don't want to keep my old data, I use useRef like this但是我有一个特殊情况,在我支付成功并重定向到另一个页面后,我不想保留我的旧数据,我这样使用useRef

 const isPayment = useRef(false) useEffect(() => { return () => { if(.isPayment,current){ //if my handlePayment is called, // I won't run my handle when component is unmout Your action when component is unmout } } }.[]) const handlePayment = () => { isPayment.current = true }
 <script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

相关问题 React 组件应该多久挂载和卸载一次? - How often should a React component mount and unmount? 如何通过按键在 React 中挂载/卸载组件? - How do I mount/unmount a component in React with a keystroke? React:可变高度的组件的安装/卸载动画 - React: mount/unmount animation of component with variable height 调用父函数设置父状态后,React Child组件将挂载然后卸载 - React Child component will mount then unmount after calling parent function to set parent state 使用 React Hooks 卸载组件时如何访问状态? - How to access state when component unmount with React Hooks? 如何在使用 react 卸载组件时将 state 切换为 false? - How to toggle the state to false on component unmount using react? 如何在React Naitve中卸载组件 - How to unmount the component in React Naitve 在反应中使用发布订阅使组件卸载和挂载 - using publish subscribe in react makes the component to unmount and mount ContentTools-如何在安装/卸载期间保留数据属性 - ContentTools - How to keep data- attributes during mount/unmount React.js-如何在子组件中实现一个功能,以从同一个父组件中卸载另一个子组件,并在其位置装载另一个组件? - React.js - How to implement a function in a child component to unmount another child from the same parent, and mount another component on it's place?
 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM