【问题标题】:Where to write to localStorage in a Redux app?在 Redux 应用程序中写入 localStorage 的位置?
【发布时间】:2016-05-20 05:47:59
【问题描述】:

我想将状态树的某些部分保存到 localStorage。什么是合适的地方这样做?减速器还是动作?

【问题讨论】:

    标签: javascript redux local-storage state


    【解决方案1】:

    我一直在寻找关于如何使用 redux-toolkit-persist 将状态持久化到本地存储中的完整示例,但没有成功,直到我遇到上面的 @canProm 响应来解决我的问题。 这对我有用

    //package.json
     "reduxjs-toolkit-persist": "^7.0.1",
    "lodash": "^4.17.21"
    
     //localstorage.ts
     import localStorage from 'reduxjs-toolkit-persist/es/storage';
      export const saveState = (state: any) => {
      try {
         console.log(state);
         const serializableState = JSON.stringify(state);
         localStorage.setItem('globalState', serializableState);
       } catch (err) {
        console.log('Redux was not able to persist the state into the localstorage');
     }
    };
    
    
    export const loadState = () => {
      try {
        const serializableState: string | any = 
     localStorage.getItem('globalState');
        return serializableState !== null || serializableState === undefined ? JSON.parse(serializableState) : undefined;
       } catch (error) {
          return undefined;
      }
    };
    
    
    
    //slices - actions
    //reduxjs-toolkit-slices.ts
      import { combineReducers, createSlice, PayloadAction } from '@reduxjs/toolkit';
      import { UserModel } from '../model/usermodel';
      import { GlobalState } from './type';
    
      const deaultState: GlobalState = {
           counter: 0,
           isLoggedIn: false
      };
    
     const stateSlice = createSlice({
        name: "state",
       initialState: deaultState,
    reducers: {
        isLoggedIn: (state, action: PayloadAction<boolean>) => {
            console.log('isLogged');
            console.log(state.isLoggedIn);
            console.log(action);
            state.isLoggedIn = action.payload;
            console.log(state.isLoggedIn);
        },
        setUserDetails: (state, action: PayloadAction<UserModel>) => {
            console.log('setUserDetails');
            console.log(state);
            console.log(action);
            //state.userContext.user = action.payload;
        }
      }
    });
    
    //export actions under slices
    export const {
      isLoggedIn: isUserLoggedAction,
      setUserDetails: setUserDetailActions
    } = stateSlice.actions;
    
    //TODO: use the optimal way for combining reducer using const
    //combine reducer from all slice
    export const combineReducer = combineReducers({
       stateReducer: stateSlice.reducer
    });
    
     //storeConfig
      //reduxjs-toolkit-store.ts
     import { configureStore } from '@reduxjs/toolkit';
     import { throttle } from 'lodash';
     import { persistReducer } from 'reduxjs-toolkit-persist';
     import autoMergeLevel2 from 'reduxjs-toolkit-persist/lib/stateReconciler/autoMergeLevel2';
     import storage from 'reduxjs-toolkit-persist/lib/storage';
      import { loadState, saveState } from './localStorage';
     import { combineReducer } from './reduxjs-toolkit-slices';
    
      // persist config
      const persistConfig = {
          key: 'root',
          storage: storage,
          stateReconciler: autoMergeLevel2,
      };
      const persistedReducer = persistReducer(persistConfig, combineReducer);
    
      // export reducers under slices
      const store = configureStore({
          reducer: persistedReducer,
         devTools: process.env.NODE_ENV !== 'production',
         preloadedState: loadState(), //call loadstate method to initiate store from localstorage
         middleware: (getDefaultMiddleware) =>
            getDefaultMiddleware({
            thunk: true,
            serializableCheck: false,
        }),
     });
     // handle state update event. Whenever the state will change, this subscriber will call the saveState methode to update and persist the state into the store
    store.subscribe(throttle(() => {
        saveState(store.getState());
     }, 1000));
    
    
    export default store;
    
     //App.ts
     import { persistStore } from 'reduxjs-toolkit-persist';
     import { PersistGate } from 'reduxjs-toolkit-persist/integration/react';
     import './i18n';
    
     let persistor = persistStore(store);
    
      ReactDOM.render(
         <Provider store={store}>
           <PersistGate loading={<div>Loading .....</div>} persistor={persistor}>
             <HalocarburesRouter />
           </PersistGate>
         </Provider>,
       document.getElementById('ingenierieMdn'));
    

    【讨论】:

      【解决方案2】:

      如果您不需要将所有 redux 存储复制到 localStorage,您可以使用特定的存储参数:

      store.subscribe(()=>{
        window.localStorage.setItem('currency', store.getState().lang)
      })
      

      并设置初始状态参数值,如:

      const initialState = {
        currency: window.localStorage.getItem('lang') ?? 'en',
      }
      

      在这种情况下,您不需要将const persistedState 传递给const store = createStore()

      【讨论】:

        【解决方案3】:

        基于其他答案(和Jam Creencia's Medium article)中提供的优秀建议和简短代码摘录,这是一个完整的解决方案!

        我们需要一个包含 2 个函数的文件,用于在本地存储中保存/加载状态:

        // FILE: src/common/localStorage/localStorage.js
        
        // Pass in Redux store's state to save it to the user's browser local storage
        export const saveState = (state) =>
        {
          try
          {
            const serializedState = JSON.stringify(state);
            localStorage.setItem('state', serializedState);
          }
          catch
          {
            // We'll just ignore write errors
          }
        };
        
        
        
        // Loads the state and returns an object that can be provided as the
        // preloadedState parameter of store.js's call to configureStore
        export const loadState = () =>
        {
          try
          {
            const serializedState = localStorage.getItem('state');
            if (serializedState === null)
            {
              return undefined;
            }
            return JSON.parse(serializedState);
          }
          catch (error)
          {
            return undefined;
          }
        };

        这些函数由 store.js 导入,我们在其中配置我们的商店:

        注意:您需要添加一个依赖项:npm install lodash.throttle

        // FILE: src/app/redux/store.js
        
        import { configureStore, applyMiddleware } from '@reduxjs/toolkit'
        
        import throttle from 'lodash.throttle';
        
        import rootReducer from "./rootReducer";
        import middleware from './middleware';
        
        import { saveState, loadState } from 'common/localStorage/localStorage';
        
        
        // By providing a preloaded state (loaded from local storage), we can persist
        // the state across the user's visits to the web app.
        //
        // READ: https://redux.js.org/recipes/configuring-your-store
        const store = configureStore({
        	reducer: rootReducer,
        	middleware: middleware,
        	enhancer: applyMiddleware(...middleware),
        	preloadedState: loadState()
        })
        
        
        // We'll subscribe to state changes, saving the store's state to the browser's
        // local storage. We'll throttle this to prevent excessive work.
        store.subscribe(
        	throttle( () => saveState(store.getState()), 1000)
        );
        
        
        export default store;

        store 被导入到 index.js 中,因此它可以被传递到包装 App.js 的 Provider 中:

        // FILE: src/index.js
        
        import React from 'react'
        import { render } from 'react-dom'
        import { Provider } from 'react-redux'
        
        import App from './app/core/App'
        
        import store from './app/redux/store';
        
        
        // Provider makes the Redux store available to any nested components
        render(
        	<Provider store={store}>
        		<App />
        	</Provider>,
        	document.getElementById('root')
        )

        请注意,绝对导入需要对 YourProjectFolder/jsconfig.json 进行此更改 - 如果一开始找不到文件,这会告诉它在哪里查找文件。否则,您会看到有关尝试从 src 之外导入内容的投诉。

        {
          "compilerOptions": {
            "baseUrl": "src"
          },
          "include": ["src"]
        }

        【讨论】:

          【解决方案4】:

          要填写 Dan Abramov 答案的空白,您可以像这样使用store.subscribe()

          store.subscribe(()=>{
            localStorage.setItem('reduxState', JSON.stringify(store.getState()))
          })
          

          在创建商店之前,检查localStorage 并解析您的密钥下的任何 JSON,如下所示:

          const persistedState = localStorage.getItem('reduxState') 
                                 ? JSON.parse(localStorage.getItem('reduxState'))
                                 : {}
          

          然后您将这个 persistedState 常量传递给您的 createStore 方法,如下所示:

          const store = createStore(
            reducer, 
            persistedState,
            /* any middleware... */
          )
          

          【讨论】:

          • 简单有效,没有额外的依赖。
          • 创建一个中间件可能看起来更好一些,但我同意它是有效的,并且对于大多数情况来说已经足够了
          • 在使用 combineReducers 时是否有一种很好的方法可以做到这一点,并且您只想保留一个商店?
          • 如果localStorage中什么都没有,persistedState应该返回initialState而不是一个空对象吗?否则我认为createStore 将使用该空对象进行初始化。
          • @Alex 你是对的,除非你的 initialState 是空的。
          【解决方案5】:

          我有点晚了,但我根据此处所述的示例实现了持久状态。如果您只想每 X 秒更新一次状态,这种方法可能会对您有所帮助:

          1. 定义一个包装函数

            let oldTimeStamp = (Date.now()).valueOf()
            const millisecondsBetween = 5000 // Each X milliseconds
            function updateLocalStorage(newState)
            {
                if(((Date.now()).valueOf() - oldTimeStamp) > millisecondsBetween)
                {
                    saveStateToLocalStorage(newState)
                    oldTimeStamp = (Date.now()).valueOf()
                    console.log("Updated!")
                }
            }
            
          2. 在订阅者中调用包装函数

                store.subscribe((state) =>
                {
                updateLocalStorage(store.getState())
                 });
            

          在此示例中,状态最多每 5 秒更新一次,无论触发更新的频率如何。

          【讨论】:

          • 您可以像这样将(state) =&gt; { updateLocalStorage(store.getState()) } 包裹在lodash.throttle 中:store.subscribe(throttle(() =&gt; {(state) =&gt; { updateLocalStorage(store.getState())} } 并删除其中的时间检查逻辑。
          【解决方案6】:

          我无法回答@Gardezi,但基于他的代码的选项可能是:

          const rootReducer = combineReducers({
              users: authReducer,
          });
          
          const localStorageMiddleware = ({ getState }) => {
              return next => action => {
                  const result = next(action);
                  if ([ ACTIONS.LOGIN ].includes(result.type)) {
                      localStorage.setItem(appConstants.APP_STATE, JSON.stringify(getState()))
                  }
                  return result;
              };
          };
          
          const reHydrateStore = () => {
              const data = localStorage.getItem(appConstants.APP_STATE);
              if (data) {
                  return JSON.parse(data);
              }
              return undefined;
          };
          
          return createStore(
              rootReducer,
              reHydrateStore(),
              applyMiddleware(
                  thunk,
                  localStorageMiddleware
              )
          );
          

          不同的是我们只是保存了一些动作,你可以使用去抖动函数来只保存你的状态的最后一次交互

          【讨论】:

            【解决方案7】:

            如果有人对上述解决方案有任何疑问,您可以自己写信给。让我告诉你我做了什么。忽略saga middleware 的事情只关注localStorageMiddlewarereHydrateStore 方法两件事。 localStorageMiddleware 拉出所有redux state 并将其放入local storagerehydrateStore 拉出所有applicationState 在本地存储中(如果存在)并将其放入redux store

            import {createStore, applyMiddleware} from 'redux'
            import createSagaMiddleware from 'redux-saga';
            import decoristReducers from '../reducers/decorist_reducer'
            
            import sagas from '../sagas/sagas';
            
            const sagaMiddleware = createSagaMiddleware();
            
            /**
             * Add all the state in local storage
             * @param getState
             * @returns {function(*): function(*=)}
             */
            const localStorageMiddleware = ({getState}) => { // <--- FOCUS HERE
                return (next) => (action) => {
                    const result = next(action);
                    localStorage.setItem('applicationState', JSON.stringify(
                        getState()
                    ));
                    return result;
                };
            };
            
            
            const reHydrateStore = () => { // <-- FOCUS HERE
            
                if (localStorage.getItem('applicationState') !== null) {
                    return JSON.parse(localStorage.getItem('applicationState')) // re-hydrate the store
            
                }
            }
            
            
            const store = createStore(
                decoristReducers,
                reHydrateStore(),// <-- FOCUS HERE
                applyMiddleware(
                    sagaMiddleware,
                    localStorageMiddleware,// <-- FOCUS HERE 
                )
            )
            
            sagaMiddleware.run(sagas);
            
            export default store;
            

            【讨论】:

            • 嗨,即使商店中没有任何变化,这不会导致对localStorage 的大量写入吗?如何补偿不必要的写入
            • 嗯,它有效,无论如何这是个好主意吗?问题:如果我们有更多的数据,并且有多个具有大数据的 reducer,会发生什么?
            【解决方案8】:

            一句话:中间件。

            查看redux-persist。或者自己写。

            [2016 年 12 月 18 日更新] 编辑删除了两个类似项目现在不活动或已弃用的提及。

            【讨论】:

              【解决方案9】:

              Reducer 永远不是一个合适的地方,因为 reducer 应该是纯粹的并且没有副作用。

              我建议只在订阅者中进行:

              store.subscribe(() => {
                // persist your state
              })
              

              在创建商店之前,请阅读那些持久化的部分:

              const persistedState = // ...
              const store = createStore(reducer, persistedState)
              

              如果您使用combineReducers(),您会注意到尚未收到状态的reducer 将使用其默认的state 参数值正常“启动”。这可能非常方便。

              建议您对订阅者进行去抖动处理,以免写入 localStorage 太快,否则会出现性能问题。

              最后,您可以创建一个中间件来封装它作为替代方案,但我会从订阅者开始,因为它是一个更简单的解决方案并且可以很好地完成工作。

              【讨论】:

              • 如果我只想订阅部分状态怎么办?那可能吗?我继续做一些不同的事情: 1. 在 redux 之外保存持久状态。 2.使用redux动作在react构造函数(或componentWillMount)中加载持久状态。 2 是否完全可以,而不是直接在存储中加载持久数据? (我试图单独为 SSR 保留)。顺便感谢 Redux!太棒了,我喜欢它,在迷失了我的大项目代码之后,现在一切都回到了简单和可预测的状态:)
              • Dan Abramov 还制作了完整的视频:egghead.io/lessons/…
              • 在每次商店更新时序列化状态是否存在合理的性能问题?浏览器是否可能在单独的线程上序列化?
              • 这看起来可能很浪费。 OP确实说过只需要保留一部分状态。假设您有一家商店,里面有 100 个不同的钥匙。您只想保留其中不经常更改的 3 个。现在,您正在解析和更新本地存储,对 100 个键中的任何一个进行微小的更改,而您感兴趣的 3 个键中的任何一个都没有被更改过。下面@Gardezi 的解决方案是一种更好的方法,因为您可以将事件侦听器添加到中间件中,以便您在实际需要时更新,例如:codepen.io/retrcult/pen/qNRzKN
              • 我认为所有这些答案都有点误导,谁说他们想在页面加载时执行 localStorage,如果我们想连同一个动作或效果一起做呢?订阅有什么作用?每次都打电话?
              猜你喜欢
              • 1970-01-01
              • 2016-07-26
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2017-06-26
              • 1970-01-01
              • 2020-11-17
              • 2017-03-14
              相关资源
              最近更新 更多