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

时间:2015-07-11 00:38:14

标签: javascript reactjs

我有一个维持内部状态的简单组件<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>
    )
  }
});

这显然不起作用,每次切换都会创建一个新的<StatefulView>

这是JSFiddle

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

10 个答案:

答案 0 :(得分:33)

我也在考虑这个问题。

我已将我的尝试放在这个回购中:react-keep-state

对我来说,最合适的解决方案如下:

import React, { Component, PropTypes } from 'react';

// Set initial state
let state = { counter: 5 };

class Counter extends Component {

  constructor(props) {
    super(props);

    // Retrieve the last state
    this.state = state;

    this.onClick = this.onClick.bind(this);
  }

  componentWillUnmount() {
    // Remember state for the next mount
    state = this.state;
  }

  onClick(e) {
    e.preventDefault();
    this.setState(prev => ({ counter: prev.counter + 1 }));
  }

  render() {
    return (
      <div>
        <span>{ this.state.counter }</span>
        <button onClick={this.onClick}>Increase</button>
      </div>
    );
  }
}

export default Counter;

注意:如果您的应用中多次存在此组件,则此方法无效。

请参阅回购以了解有关此问题的一些讨论(以及使用更高阶组件的另一种解决方案)。

答案 1 :(得分:17)

行。因此,在与一群人交谈之后,事实证明无法保存组件的实例。因此,我们必须将其保存在其他地方。

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

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

答案 2 :(得分:8)

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

React Web

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()是一项昂贵的操作,因此您不应该在按键等操作。如果您有疑虑,请调查去抖动。

答案 3 :(得分:2)

我不是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看到它。

答案 4 :(得分:2)

我迟到了,但如果你正在使用Redux。你可以通过redux-persist几乎开箱即可获得这种行为。只需将其autoRehydrate添加到商店,它就会收听REHYDRATE个动作,这些动作将自动恢复组件的先前状态(来自网络存储)。

答案 5 :(得分:2)

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

  • 在某些商店(Redux)或Context中保存状态可能是最好的 解决方案。
  • 仅在一次只有一个组件实例的情况下,才可以在全局变量中存储。 (每个实例如何知道它们的存储状态?)
  • 保存localStorage的注意事项与全局变量相同,并且由于我们不是在讨论通过浏览器刷新来恢复状态,因此似乎并没有增加任何好处。

答案 6 :(得分:1)

在渲染之间缓存状态的一种简单方法是利用以下事实:导出的模块在您正在处理的文件上形成闭包。

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

var cachedState

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

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

  return (...)
}

答案 7 :(得分:0)

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

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

我会修改你的小提琴功能并用它来更新我的答案。

答案 8 :(得分:0)

我为此专门制作了一个简单的 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)

就是这样。轻轻松松。

答案 9 :(得分:0)

我看到了这篇文章,正在寻找一种方法来随着时间的推移构建组件状态,即后端的每个额外页面。

我使用了持久化和 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 数据复制到组件的本地状态中,只是某种摘要,例如长度。