Де написати в localStorage у додатку Redux?


166

Я хочу зберегти деякі частини свого державного дерева до місцевого сховища. Яке відповідне місце для цього? Редуктор або дія?

Відповіді:


223

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

Я б рекомендував просто зробити це підпискою:

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

Перш ніж створити магазин, прочитайте ці збережені частини:

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

Якщо ви використовуєте, combineReducers()ви помітите, що редуктори, які не отримали стан, "завантажуватимуться" як звичайне, використовуючи stateзначення аргументу за замовчуванням . Це може бути дуже зручно.

Бажано, щоб ви дебютували передплатником, щоб ви не писали в localStorage занадто швидко, інакше у вас виникнуть проблеми з роботою.

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


1
що якщо я хотів би підписатись лише на частину штату? це можливо? Я продовжив щось трохи інше: 1. зберегти збережений стан поза редукцією. 2. стан, що зберігається, всередині конструктора реагування (або з компонентWillMount) із зворотною дією. 2 повністю, замість завантаження збережених даних у магазині безпосередньо? (яку я намагаюся зберегти окремо для SSR). До речі дякую за Redux! Це приголомшливо, я люблю це, після того, як загубився зі свого великого коду проекту, тепер все повернеться до простого і передбачуваного :)
Марк Урецький

під час зворотного дзвінка store.subscribe, ви маєте повний доступ до поточних даних магазину, тому ви можете зберегти будь-яку цікавить вас частину
Bo Chen

35
Дан Абрамов також зняв на ньому ціле відео: egghead.io/lessons/…
NateW

2
Чи є законні проблеми щодо ефективності роботи серіалізації за кожним оновленням магазину? Можливо, браузер серіалізується на окремий потік?
Стівен Пол

7
Це виглядає потенційно марно. ОП заявила, що потрібно зберігати лише частину держави. Скажімо, у вас є магазин зі 100 різними ключами. Ви хочете зберегти лише 3 з них, які нечасто змінюються. Тепер ви розбираєте та оновлюєте місцевий магазин при кожній невеликій зміні будь-якої зі ваших 100 клавіш, і жоден з трьох клавіш, які ви хочете наполегливо, ніколи не змінювались. Рішення від @Gardezi нижче - це кращий підхід, оскільки ви можете додавати слухачів подій до свого проміжного програмного забезпечення, щоб ви оновлювались лише тоді, коли вам потрібно, наприклад: codepen.io/retrcult/pen/qNRzKN
Anna T,

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
створення проміжного програмного забезпечення може виглядати трохи краще, але я погоджуюся, що це ефективно та достатньо для більшості випадків
Бо Чен

Чи є приємний спосіб зробити це при використанні комбінованих рудників, і ви хочете зберегти лише один магазин?
brendangibson

1
Якщо нічого немає в localStorage, слід persistedStateповернутись initialStateзамість порожнього об’єкта? Інакше я думаю createStore, що ініціалізуватимуться з цим порожнім об’єктом.
Олексій

1
@Alex Ви маєте рацію, якщо тільки ваша початкова держава порожня.
Посилання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
    )
);

Різниця полягає в тому, що ми просто економимо деякі дії, ви можете використати функцію дебютації, щоб зберегти лише останню взаємодію вашої держави


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 секунд, незалежно від того, як часто відбувається оновлення.


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

1

Спираючись на чудові пропозиції та уривки з короткими кодами, надані в інших відповідях (та середній статті Jam Creencia ), ось повне рішення!

Нам потрібен файл, що містить 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"]
}

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.