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



我有一个维护内部状态的简单组件<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。

是否有一种方法挂在相同的组件后,它被卸载,所以它可以重新安装?

因为你不能在组件卸载时将状态保存在组件本身,所以你必须决定它应该保存在哪里。

这些是你的选择:

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

如果你正在寻找一个简单的解决方案,在React之外持久化数据,这个钩子可能会派上用场:

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']);

OK。因此,在与一群人交谈之后,发现没有办法保存组件的实例。因此,我们必须把它保存在其他地方。

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

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

2)你可以在其他地方保存状态,比如Flux store,或者在某个全局对象中。

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

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

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

我实际上已经修改了一点,使其更一般化,我使用函数而不是可变对象。我认为这更合理——不可变数据总是更适合我。下面是我正在做的:

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

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

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

  • 保存状态在一些商店(Redux)或上下文可能是最好的解决方案。
  • 只有当你的组件一次只有一个实例时,存储在全局变量中才会起作用。(每个实例如何知道哪个存储状态是他们的?)
  • 在localStorage中保存与全局变量有相同的警告,因为我们不是在谈论在浏览器刷新时恢复状态,它似乎没有增加任何好处。

如果是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()是一个昂贵的操作,所以您不应该在按键等情况下执行此操作。

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

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

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

我迟到了,但是如果你使用Redux。使用redux-persist,您将获得几乎是开箱即用的行为。只需将其autoRehydrate添加到存储中,然后它将侦听REHYDRATE动作,这将自动恢复组件的先前状态(从web存储)。

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

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

就是这样。容易peasy。

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

(当你这样做的时候,你应该调用App内部的一个toggle函数,你想要改变数据的功能与数据一起生活)。

我会修改你的小提琴功能和更新我的答案。

我偶然发现了这篇文章,寻找一种方法来构建组件状态随着时间的推移,即从后端添加每个额外的页面。

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

这对于线程打开器的场景是不够的,但是对于其他遇到这个线程的人来说可能是不够的:根据您需要的持久性和您想要存储的数据有多复杂,一个查询参数也可能足够了。

例如,如果你只是想在窗口大小调整操作之间保留一个bool值hideItems,如果你以my.example.com/myPage?hideItems=true的方式附加它,它将保留。你必须评估你的页面/组件内部渲染参数,例如,NextJS,它将是

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

在我的例子中,我选择要支付的项目并将其保存到redux存储中的状态,当我的组件被卸载时,我想将状态设置为新列表。但是我有一个特殊的情况,我支付成功后,重定向到另一个页面,我不想保留我的旧数据,我使用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>

最新更新