如何在刷新时保留 redux 状态树?

IT技术 reactjs redux flux
2021-04-13 00:53:24

Redux 文档的第一原则是:

整个应用程序的状态存储在单个存储中的对象树中。

我实际上认为我很好地理解了所有的原则。但我现在很困惑,申请是什么意思。

如果应用程序只是网站中的一个小复杂部分并且只在一页中工作,我理解。但是如果应用程序意味着整个网站呢?我应该使用 LocalStorage 还是 cookie 或其他东西来保存状态树?但是如果浏览器不支持 LocalStorage 呢?

我想知道开发人员如何保持他们的状态树!:)

3个回答

如果你想在浏览器刷新时保持你的 redux 状态,最好使用 redux 中间件来做到这一点。查看redux-persistredux-storage中间件。它们都试图完成存储您的 redux 状态的相同任务,以便可以随意保存和加载它。

——

编辑

自从我重新审视这个问题已经有一段时间了,但是看到另一个(尽管更多的赞成的答案)鼓励推出您自己的解决方案,我想我会再次回答这个问题。

截至本次编辑时,这两个库均已在过去六个月内更新。我的团队已经在生产中使用 redux-persist 几年了,并且没有出现任何问题。

虽然这似乎是一个简单的问题,但您很快就会发现,推出自己的解决方案不仅会造成维护负担,还会导致错误和性能问题。想到的第一个例子是:

  1. JSON.stringify并且JSON.parse不仅会在不需要时损害性能,而且会抛出错误,如果在诸如 redux store 之类的关键代码中未处理,可能会使您的应用程序崩溃。
  2. (在下面的答案中部分提到):弄清楚何时以及如何保存和恢复您的应用程序状态不是一个简单的问题。这样做太频繁,你会损害性能。还不够,或者如果状态的错误部分被持久化,你可能会发现自己有更多的错误。上面提到的库在他们的方法中经过了实战测试,并提供了一些非常简单的自定义行为的方法。
  3. redux 的部分优点(尤其是在 React 生态系统中)是它能够放置在多种环境中的能力。截至本次编辑,redux-persist 有15 种不同的存储实现,包括用于 Web 的出色localForage 库,以及对 React Native、Electron 和Node.js 的支持。

总而言之,对于 3kB 缩小 + gzipped(在此编辑时),这不是问题,我会要求我的团队自行解决。

我可以推荐 redux-persist(还没有尝试过 redux-storage)但它对我来说非常好,只需很少的配置和设置。
2021-05-25 00:53:24
看起来 redux-persist 又回来了,在我写作的时候 22 天前有一个新的发布
2021-05-26 00:53:24
请注意这个答案:现实情况是,软件和库通常采用基于社区(支持)的方法,即使是编程语言的一些非常重要的module也得到第三方/库的支持。通常,开发人员必须密切关注其堆栈中使用的每个工具,以了解它是否已被弃用/更新。两种选择;1.实施您自己的并永远保持发展以确保性能和跨平台标准。2.使用经过实战考验的解决方案,只检查更新/建议,如@MiFreidgeimSO-stopbeingevil 所说
2021-06-10 00:53:24
redux-storage 的新位置是github.com/react-stack/redux-storage
2021-06-19 00:53:24
截至目前,这两个库都已经死了,并且早在 2 年前就没有最后一次提交进行维护。
2021-06-19 00:53:24

2019 年 8 月 25 日编辑

如其中一条评论所述。原始的redux-storage包已移至react-stack这种方法仍然侧重于实现您自己的状态管理解决方案。


原答案

虽然提供的答案在某些时候是有效的,但重要的是要注意原始redux-storage包已被弃用并且不再维护......

包 redux-storage 的原作者决定弃用该项目,不再维护。

现在,如果您不想依赖其他软件包以避免将来出现此类问题,那么推出您自己的解决方案非常容易。

您需要做的就是:

1- 创建一个函数,该函数返回状态localStorage,然后将状态传递给createStore第二个参数中的 redux 函数,以便对 store 进行水化

 const store = createStore(appReducers, state);

2- 监听状态变化,每次状态变化时,将状态保存到 localStorage

store.subscribe(() => {
    //this is just a function that saves state to localStorage
    saveState(store.getState());
}); 

就是这样......我实际上在生产中使用了类似的东西,但是我没有使用函数,而是编写了一个非常简单的类,如下所示......

class StateLoader {

    loadState() {
        try {
            let serializedState = localStorage.getItem("http://contoso.com:state");

            if (serializedState === null) {
                return this.initializeState();
            }

            return JSON.parse(serializedState);
        }
        catch (err) {
            return this.initializeState();
        }
    }

    saveState(state) {
        try {
            let serializedState = JSON.stringify(state);
            localStorage.setItem("http://contoso.com:state", serializedState);

        }
        catch (err) {
        }
    }

    initializeState() {
        return {
              //state object
            }
        };
    }
}

然后在引导您的应用程序时...

import StateLoader from "./state.loader"

const stateLoader = new StateLoader();

let store = createStore(appReducers, stateLoader.loadState());

store.subscribe(() => {
    stateLoader.saveState(store.getState());
});

希望它可以帮助某人

性能说明

如果应用程序中的状态更改非常频繁,那么过于频繁地保存到本地存储可能会损害应用程序的性能,尤其是当要序列化/反序列化的状态对象图很大时。对于这些情况,您可能希望去抖动或油门,使用保存状态的localStorage功能RxJslodash或类似的东西。

我更喜欢这种方法,而不是使用中间件。感谢您提供有关性能问题的提示。
2021-05-22 00:53:24
@Zief 很难说。消息“似乎”很清楚,reducers 期待未指定的属性。这可能与为序列化状态提供默认值有关?
2021-05-28 00:53:24
非常直接的解决方案。谢谢你。
2021-06-09 00:53:24
@Joezhou 很想知道您为什么喜欢这种方法。就个人而言,这似乎正是中间件的目的。
2021-06-09 00:53:24
绝对是首选答案。但是,当我刷新页面并在创建商店时从 localstorage 加载状态时,我收到几个警告,其中包括文本“在减速器收到的先前状态中找到的意外属性 [容器名称]。预计会找到其中一个已知的 reducer 属性名称改为:“global”、“language”。意外的属性将被忽略。它仍然有效,并且基本上是在抱怨在创建存储时它不知道所有其他容器。是否有绕过这个警告的方法?
2021-06-14 00:53:24

这是基于 Leo 的答案(这应该是公认的答案,因为它在不使用任何 3rd 方库的情况下达到了问题的目的)。

我创建了一个 Singleton 类,它创建了一个 Redux Store,使用本地存储将它持久化,并允许通过 getter 简单访问其存储

要使用它,只需在主类周围放置以下 Redux-Provider 元素:

// ... Your other imports
import PersistedStore from "./PersistedStore";

ReactDOM.render(
  <Provider store={PersistedStore.getDefaultStore().store}>
    <MainClass />
  </Provider>,
  document.getElementById('root')
);

并将以下类添加到您的项目中:

import {
  createStore
} from "redux";

import rootReducer from './RootReducer'

const LOCAL_STORAGE_NAME = "localData";

class PersistedStore {

  // Singleton property
  static DefaultStore = null;

  // Accessor to the default instance of this class
  static getDefaultStore() {
    if (PersistedStore.DefaultStore === null) {
      PersistedStore.DefaultStore = new PersistedStore();
    }

    return PersistedStore.DefaultStore;
  }

  // Redux store
  _store = null;

  // When class instance is used, initialize the store
  constructor() {
    this.initStore()
  }

  // Initialization of Redux Store
  initStore() {
    this._store = createStore(rootReducer, PersistedStore.loadState());
    this._store.subscribe(() => {
      PersistedStore.saveState(this._store.getState());
    });
  }

  // Getter to access the Redux store
  get store() {
    return this._store;
  }

  // Loading persisted state from localStorage, no need to access
  // this method from the outside
  static loadState() {
    try {
      let serializedState = localStorage.getItem(LOCAL_STORAGE_NAME);

      if (serializedState === null) {
        return PersistedStore.initialState();
      }

      return JSON.parse(serializedState);
    } catch (err) {
      return PersistedStore.initialState();
    }
  }

  // Saving persisted state to localStorage every time something
  // changes in the Redux Store (This happens because of the subscribe() 
  // in the initStore-method). No need to access this method from the outside
  static saveState(state) {
    try {
      let serializedState = JSON.stringify(state);
      localStorage.setItem(LOCAL_STORAGE_NAME, serializedState);
    } catch (err) {}
  }

  // Return whatever you want your initial state to be
  static initialState() {
    return {};
  }
}

export default PersistedStore;