【问题标题】:How to keep React component state between mount/unmount?如何在挂载/卸载之间保持 React 组件状态?
【发布时间】:2015-07-11 00:38:14
【问题描述】:

我有一个简单的组件<StatefulView> 维护内部状态。我有另一个组件<App> 可以切换是否呈现<StatefulView>

但是,我想在安装/卸载之间保持<StatefulView> 的内部状态。

我想我可以在<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>
    )
  }
});

这显然不起作用,每次切换都会创建一个新的&lt;StatefulView&gt;

这是JSFiddle

有没有办法在卸载后挂在同一个组件上以便重新安装它?

【问题讨论】:

  • 您可以使用 Flux 将状态存储在 Store 中
  • 是的,我试图没有保存/加载方法。此外,这对我来说可能会变得棘手。在 UINavigationController 的上下文中考虑这一点——我可能有多个完全相同的组件,但具有不同的状态,在导航堆栈的不同级别......

标签: javascript reactjs


【解决方案1】:

由于您无法在组件卸载时将状态保留在组件本身中,因此您必须决定应该将其保存在何处。

这些是您的选择:

  1. React state in parent:如果父组件仍然挂载,则它可能应该是状态的所有者,或者可以为下面的不受控制的组件提供初始状态。您可以在组件卸载之前将值传回。使用 React 上下文,您可以将状态提升到应用程序的最顶部(参见例如 unstated)。
  2. React 之外:例如use-local-storage-state。请注意,您可能需要在测试之间手动重置状态。其他选项包括 URL 中的查询参数、MobX 或 Redux 等状态管理库等。

你正在寻找一种简单的解决方案,将数据保存在 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];
}

用法:

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

【讨论】:

  • 只是对其他人的警告,这不适用于在多个地方使用的测试或组件。
【解决方案2】:

好的。所以在和一堆人交谈之后,事实证明,没有办法保存一个组件的实例。因此,我们必须将其保存在其他地方。

1) 保存状态最明显的地方是在父组件内。

这对我来说不是一个选项,因为我正在尝试从类似 UINavigationController 的对象中推送和弹出视图。

2) 您可以将状态保存在其他地方,例如 Flux 存储或某个全局对象中。

这对我来说也不是最佳选择,因为跟踪哪些数据属于哪个导航控制器中的哪个视图等将是一场噩梦。

3) 传递一个可变对象来保存和恢复状态。

这是我在 React 的 Github 存储库的各种问题单中评论时发现的一个建议。这似乎是我要走的路,因为我可以创建一个可变对象并将其作为道具传递,然后使用相同的可变道具重新渲染相同的对象。

我实际上已经对其进行了一些修改以使其更通用,并且我使用的是函数而不是可变对象。我认为这更理智——不可变数据总是比我更可取。这是我正在做的事情:

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

现在在getInitialState 我正在为组件创建一个新实例:

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

然后在StatefulView我只需要在componentWillMountcomponentWillUnmount中保存和恢复即可。

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

就是这样。它对我来说真的很好用。现在我可以将组件视为实例:)

【讨论】:

  • 这看起来完全像我需要的东西。你能详细说明一下吗?举一个在现实生活中存储组件状态并恢复它的例子?
  • 这是一个很好的修复,谢谢
【解决方案3】:

对于那些刚刚在 2019 年或以后阅读本文的人,其他答案中已经给出了很多细节,但我想在这里强调几点:

  • 在某些商店 (Redux) 或上下文中保存状态可能是最好的 解决方案。
  • 仅当您的组件一次只有一个实例时,存储在全局变量中才有效。 (每个实例如何知道自己的存储状态是什么?)
  • 保存在 localStorage 中的注意事项与全局变量相同,而且由于我们不是在谈论在浏览器刷新时恢复状态,因此它似乎没有任何好处。

【讨论】:

  • 我有同样的情况,我没有去坚持库只是因为我想要一个即使用户不同也应该发生的通用模式。只需使用 redux
  • 所有这些都是等价的并且共享相同的问题。在 Redux 中存储状态仍然不能解决实例的问题
【解决方案4】:

嗯,如果是 React Native,你可以使用 localStorageAsyncStorage

反应网络

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

然后在当天晚些时候或 2 秒后:

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

反应原生

import { AsyncStorage } from 'react-native'

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

然后在当天晚些时候或 2 秒后:

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

您也可以使用Redux 并在渲染时将数据传递给子组件。研究 serializing 状态和 Redux 的第二个参数 createStore 函数可能会对您有所帮助,该函数用于重新水合初始状态。

请注意,JSON.stringify() 是一项昂贵的操作,因此您不应该在按键等上执行此操作。如果您有顾虑,请研究去抖动。

【讨论】:

  • localStorage 是一个有趣的 web 概念,将试一试。谢谢
  • localStorage 是安全的。您最多可以在其中存储大约 15 MB 的数据,具体取决于浏览器等。它非常适合短期持久数据。
  • 性能如何?与改变反应应用程序的全局状态相比,读/写本地存储是否需要时间?静音状态可能会重新渲染大量组件,这就是为什么我也在寻找一种解决方案来在卸载/安装之间保存单个组件状态。但只能以高效的方式。
  • 我会说性能是次要的主要问题,只是让数据通过卸载保持不变。在上游组件中存储数据基本上是 React 中的“提升状态”。这比将数据保存到磁盘性能更高,因为您正在更改对内存中数据的引用并且基本上避免了垃圾收集,因此读/写速度将是 turbo;但是,将数据存储在 localStorage 中允许数据在根组件本身被卸载和重新安装时保持不变。
【解决方案5】:

我迟到了,但如果你使用的是 Redux。使用 redux-persist,您几乎可以立即获得这种行为。只需将其autoRehydrate 添加到存储中,它就会监听REHYDRATE 操作,这些操作将自动恢复组件的先前状态(从网络存储)。

【讨论】:

    【解决方案6】:

    我不是 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')
    );
    

    你可以在jsfiddle看到它。

    【讨论】:

      【解决方案7】:

      我已经为此目的制作了一个简单的 NPM 包。你可以在这里找到它:

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

      用法很简单。首先,将组件包含在组件树的某个较高位置,如下所示

      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>
              )
          }
      }
      

      然后,您可以在任何组件中使用它,如下所示:

      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)
      

      就是这样。轻松愉快。

      【讨论】:

      • 这不是开源的吗?
      • 我的意思是,我自己做的,并作为开源发布,所以它是!
      【解决方案8】:

      在渲染之间缓存状态的一种简单方法是使用模块在您正在处理的文件上导出表单闭包这一事实。

      使用useEffect 挂钩,您可以指定组件卸载时发生的逻辑(即更新在模块级别关闭的变量)。这是因为导入的模块被缓存了,这意味着导入时创建的闭包永远不会消失。我不确定这是否是一种好的方法,但适用于文件只导入一次的情况(否则cachedState 将在默认渲染组件的所有实例之间共享)

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

      【讨论】:

        【解决方案9】:

        如果您希望能够在保持状态的同时卸载和挂载,则需要将计数存储在 App 并通过 props 传递计数。

        (执行此操作时,您应该在App 内调用切换函数,您希望更改数据的功能与数据一起使用)。

        我将修改您的小提琴以使其正常工作并用它更新我的答案。

        【讨论】:

        • 是的,这不是我想要的。这是一个人为的示例,因此您提到的内容适用于 this 示例,但不适用于我要解决的更复杂的问题...
        • 您的问题是关于专门维护组件。据我所知,您能做的最好的事情就是维护组件的状态并重新实例化组件。
        • 我明白了。据我所知也是这样。不那么做作的版本是这样的:我正在尝试为网络构建 UITabBarController 和 UINavigationController 等效项。特别是使用 UINavigationController,我们可以将任意视图推送到堆栈并弹出它们。跟踪所有这些任意状态实际上并不可行,所以如果我可以在安装之间将状态保持在组件本身内,那就太好了......
        【解决方案10】:

        我发现这篇文章是为了寻找一种随着时间推移构建组件状态的方法,即,随着来自后端的每个附加页面。

        我使用持久化器和 redux。但是,对于这种状态,我想将其保留在组件的本地。什么最终起作用:useRef。我有点惊讶没有人提到它,所以我可能会错过问题的重要部分。尽管如此,ref 在 React 的虚拟 DOM 的渲染之间仍然存在。

        有了这些,我可以随着时间的推移构建我的缓存,观察组件更新(也就是重新渲染),而不必担心“丢弃”与组件相关的以前的 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]);
        

        通过跟踪随着缓存大小的增加而变化的本地状态,组件被欺骗重新渲染。我不会将 DOM 上的 ref 数据复制到组件的本地状态中,只是某种摘要,例如长度。

        【讨论】:

        • 我尝试了您的 ref 解决方案,但它不起作用,一旦我退出组件并再次返回组件,它就会丢失 ref 值!
        • 使用 ref 将数据保存在 DOM 中的一个节点上。您是否可能正在重新加载 DOM?
        猜你喜欢
        • 2021-10-08
        • 2018-04-11
        • 2018-12-04
        • 2020-11-02
        • 2021-03-25
        • 1970-01-01
        • 2020-07-08
        • 2020-06-12
        • 1970-01-01
        相关资源
        最近更新 更多