Куда писать в localStorage в приложении Redux?

167

Я хочу сохранить некоторые части моего дерева состояний в localStorage. Какое подходящее место для этого? Редуктор или действие?

Марко де Йонг
источник

Ответы:

224

Редуктор никогда не подходит для этого, потому что редукторы должны быть чистыми и не иметь побочных эффектов.

Я бы порекомендовал просто сделать это в подписчике:

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

Перед созданием магазина прочитайте эти сохраненные части:

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

Если вы используете, combineReducers()вы заметите, что редукторы, которые не получили состояние, будут «загружаться» как обычно, используя значение stateаргумента по умолчанию . Это может быть очень удобно.

Желательно, чтобы вы отменили подписку, чтобы не писать слишком быстро в localStorage, иначе у вас будут проблемы с производительностью.

Наконец, вы можете создать промежуточное программное обеспечение, которое инкапсулирует это как альтернативу, но я бы начал с подписчика, потому что это более простое решение и хорошо выполняет свою работу.

Дан Абрамов
источник
1
Что делать, если я хотел бы подписаться только на часть государства? это возможно? Я продолжил что-то немного другое: 1. сохранить сохраненное состояние за пределами редукса. 2. загрузить постоянное состояние внутри реактора-конструктора (или с помощью componentWillMount) с редукционным действием. 2 вполне нормально вместо прямой загрузки сохраненных данных в хранилище? (который я пытаюсь сохранить отдельно для ССР). Кстати, спасибо за Redux! Это потрясающе, мне это нравится, после того, как я потерял свой большой проектный код, теперь все вернулось к простому и предсказуемому :)
Марк Урецкий,
в обратном вызове store.subscribe у вас есть полный доступ к текущим данным магазина, поэтому вы можете сохранить любую интересующую вас часть
Бо Чен
35
Дан Абрамов также снял на него целое видео: egghead.io/lessons/…
NateW
2
Существуют ли законные проблемы с производительностью при сериализации состояния при каждом обновлении магазина? Может ли браузер сериализоваться в отдельном потоке?
Стивен Пол
7
Это выглядит потенциально расточительно. ОП сказал, что только часть государства должна сохраняться. Допустим, у вас есть магазин с 100 различными ключами. Вы хотите сохранить только 3 из них, которые редко меняются. Теперь вы анализируете и обновляете локальное хранилище при каждом небольшом изменении любого из ваших 100 ключей, при этом ни один из трех ключей, которые вы хотите сохранить, никогда не менялся. Решение от @Gardezi, представленное ниже, является лучшим подходом, так как вы можете добавить прослушиватели событий в ваше промежуточное ПО, чтобы вы могли обновлять только тогда, когда вам действительно нужно, например: codepen.io/retrcult/pen/qNRzKN
Анна Т
153

Чтобы заполнить пробелы в ответе Дана Абрамова, вы можете использовать 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... */
)
Эндрю Самуэльсен
источник
6
Просто и эффективно, без лишних зависимостей.
AxeEffect
1
создание промежуточного программного обеспечения может выглядеть немного лучше, но я согласен, что это эффективно и достаточно для большинства случаев
Бо Чен
Есть ли хороший способ сделать это при использовании combReducers, и вы хотите сохранить только один магазин?
brendangibson
1
Если в localStorage ничего нет, должен persistedStateвозвращаться initialStateвместо пустого объекта? В противном случае я думаю, что createStoreбудет инициализироваться с этим пустым объектом.
Алекс
1
@ Алекс Вы правы, если ваш initialState пуст.
Ссылка 14
47

Одним словом: промежуточное ПО.

Проверьте избыточно-постоянный . Или написать свой.

[ОБНОВЛЕНИЕ 18 декабря 2016 года] Отредактировано, чтобы удалить упоминание о двух похожих проектах, которые сейчас неактивны или устарели.

Дэвид Л. Уолш
источник
12

Если у кого-то есть проблемы с вышеуказанными решениями, вы можете написать свое собственное. Позвольте мне показать вам, что я сделал. Игнорировать saga middlewareвещи, просто сосредоточиться на двух вещах localStorageMiddlewareи reHydrateStoreметоде. localStorageMiddlewareтянуть все redux stateи помещает его в local storageи rehydrateStoreтянут все 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;
Гардези
источник
2
Привет, не приведет ли это к большому количеству записей, localStorageдаже если ничего в магазине не изменилось? Как вы компенсируете ненужные записи
user566245
Ну, это работает, в любом случае это хорошая идея иметь? Вопрос: Что произойдет в случае, если у нас будет больше данных, несколько переходов с большими данными?
Кунвар Сингх
3

Я не могу ответить @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
    )
);

разница в том, что мы просто сохраняем некоторые действия, вы можете использовать функцию debounce для сохранения только последнего взаимодействия вашего состояния

Дуглас Каина
источник
1

Я немного опоздал, но я реализовал постоянное состояние в соответствии с примерами, приведенными здесь. Если вы хотите обновлять состояние только каждые 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 секунд, независимо от того, как часто запускается обновление.

movcmpret
источник
1
Вы можете обернуть (state) => { updateLocalStorage(store.getState()) }в lodash.throttleтак: store.subscribe(throttle(() => {(state) => { updateLocalStorage(store.getState())} }и удалить время проверки логики внутри.
GeorgeMA
1

Опираясь на отличные предложения и выдержки из коротких кодов, приведенные в других ответах (и средней статье Джема Кричии ), вот полное решение!

Нам нужен файл, содержащий 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;

Хранилище импортируется в index.js, поэтому его можно передать провайдеру, который упаковывает App.js :

// 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"]
}

CanProgram
источник