Gdzie pisać do localStorage w aplikacji Redux?

166

Chcę zachować niektóre części mojego drzewa stanu w magazynie localStorage. Jakie jest odpowiednie miejsce, aby to zrobić? Reduktor czy akcja?

Marco de Jongh
źródło

Odpowiedzi:

223

Reduktor nigdy nie jest odpowiednim miejscem do tego, ponieważ reduktory powinny być czyste i nie mieć skutków ubocznych.

Poleciłbym po prostu zrobić to w subskrybencie:

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

Przed utworzeniem sklepu przeczytaj te utrwalone części:

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

Jeśli użyjesz combineReducers(), zauważysz, że reduktory, które nie otrzymały stanu, „uruchomią się” normalnie, używając domyślnej statewartości argumentu. To może być bardzo przydatne.

Wskazane jest, aby usunąć subskrybenta, aby nie pisać zbyt szybko do localStorage, w przeciwnym razie będziesz mieć problemy z wydajnością.

Na koniec możesz stworzyć oprogramowanie pośredniczące, które hermetyzuje to jako alternatywę, ale zacznę od subskrybenta, ponieważ jest to prostsze rozwiązanie i dobrze spełnia swoje zadanie.

Dan Abramov
źródło
1
co jeśli chciałbym subskrybować tylko część stanu? czy to jest możliwe? Poszedłem do czegoś nieco innego: 1. zapisać stan utrwalony poza reduxem. 2. Załaduj trwały stan wewnątrz konstruktora reagującego (lub z komponentemWillMount) z akcją redux. Czy 2 jest całkowicie w porządku zamiast bezpośredniego ładowania utrwalonych danych do magazynu? (które staram się zachować osobno dla SSR). Przy okazji dzięki za Redux! To niesamowite, uwielbiam to, po tym, jak zagubiłem się w moim dużym kodzie projektu, teraz wszystko wróciło do prostego i przewidywalnego :)
Mark Uretsky
w ramach oddzwonienia store.subscribe masz pełny dostęp do aktualnych danych sklepu, dzięki czemu możesz utrwalić dowolną część, która Cię interesuje
Bo Chen
35
Dan Abramov również nakręcił na ten temat cały film: egghead.io/lessons/ ...
NateW,
2
Czy istnieją uzasadnione problemy z wydajnością związane z serializacją stanu przy każdej aktualizacji sklepu? Czy przeglądarka może serializować w osobnym wątku?
Stephen Paul,
7
Wygląda to potencjalnie na marnotrawstwo. OP powiedział, że tylko część stanu musi zostać utrzymana. Załóżmy, że masz sklep ze 100 różnymi kluczami. Chcesz tylko utrzymać 3 z nich, które są rzadko zmieniane. Teraz analizujesz i aktualizujesz lokalny sklep przy każdej drobnej zmianie dowolnego ze 100 kluczy, podczas gdy żaden z 3 kluczy, które chcesz zachować, nigdy nie został nawet zmieniony. Rozwiązanie autorstwa @Gardezi poniżej jest lepszym podejściem, ponieważ możesz dodać detektory zdarzeń do swojego oprogramowania pośredniego, dzięki czemu aktualizujesz tylko wtedy, gdy naprawdę potrzebujesz, np .: codepen.io/retrcult/pen/qNRzKN
Anna T
153

Aby wypełnić puste miejsca w odpowiedzi Dana Abramowa, możesz użyć store.subscribe()tego:

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

Przed utworzeniem sklepu sprawdź localStoragei przeanalizuj dowolny kod JSON pod kluczem w następujący sposób:

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

Następnie przekazujesz tę persistedStatestałą do swojej createStoremetody w następujący sposób:

const store = createStore(
  reducer, 
  persistedState,
  /* any middleware... */
)
Andrew Samuelsen
źródło
6
Prosty i skuteczny, bez dodatkowych zależności.
AxeEffect
1
tworzenie oprogramowania pośredniego może wyglądać trochę lepiej, ale zgadzam się, że jest skuteczne i wystarczające w większości przypadków
Bo Chen
Czy istnieje dobry sposób na zrobienie tego przy użyciu CombinedReducers i chcesz utrzymać tylko jeden sklep?
brendangibson
1
Jeśli nic nie znajduje się w localStorage, czy powinien persistedStatezwrócić initialStatezamiast pustego obiektu? W przeciwnym razie myślę, że createStorezainicjuje się z tym pustym obiektem.
Alex
1
@Alex Masz rację, chyba że twój stan początkowy jest pusty.
Link14
47

Jednym słowem: oprogramowanie pośredniczące.

Sprawdź redux-persist . Lub napisz własne.

[AKTUALIZACJA 18 grudnia 2016 r.] Zmieniono, aby usunąć wzmiankę o dwóch podobnych projektach, które są obecnie nieaktywne lub wycofane.

David L. Walsh
źródło
12

Jeśli ktoś ma jakiś problem z powyższymi rozwiązaniami, możesz napisać własne na adres. Pokażę ci, co zrobiłem. Ignoruj saga middlewarerzeczy, skup się tylko na dwóch rzeczach localStorageMiddlewarei reHydrateStoremetodzie. localStorageMiddlewarepociągają za wszystkie redux statei umieszcza go w local storagei rehydrateStorewyciągnąć wszystko applicationStatew pamięci lokalnej, jeśli obecne i umieszcza go wredux 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;
Gardezi
źródło
2
Cześć, czy nie spowodowałoby to wielu zapisów, localStoragenawet jeśli nic w sklepie się nie zmieniło? Jak rekompensujesz niepotrzebne zapisy
user566245
Cóż, to działa, w każdym razie to dobry pomysł? Pytanie: Co się stanie w przypadku, gdy mamy więcej danych w wielu reduktorach z dużymi danymi?
Kunvar Singh
3

Nie mogę odpowiedzieć na @Gardezi, ale opcją opartą na jego kodzie może być:

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

różnica polega na tym, że po prostu zapisujemy niektóre akcje, możesz użyć funkcji odbicia, aby zapisać tylko ostatnią interakcję twojego stanu

Douglas Caina
źródło
1

Trochę się spóźniłem, ale zaimplementowałem stan trwały zgodnie z podanymi tutaj przykładami. Jeśli chcesz aktualizować stan tylko co X sekund, to podejście może Ci pomóc:

  1. Zdefiniuj funkcję opakowania

    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. Wywołaj funkcję opakowania w swoim subskrybencie

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

W tym przykładzie stan jest aktualizowany co najwyżej co 5 sekund, niezależnie od częstotliwości wyzwalania aktualizacji.

movcmpret
źródło
1
Możesz zawinąć (state) => { updateLocalStorage(store.getState()) }w lodash.throttleten sposób: store.subscribe(throttle(() => {(state) => { updateLocalStorage(store.getState())} }i usunąć z niego logikę sprawdzania czasu.
GeorgeMA
1

Opierając się na doskonałych sugestiach i krótkich fragmentach kodu zawartych w innych odpowiedziach (i artykule Jam Creencia w Medium ), oto kompletne rozwiązanie!

Potrzebujemy pliku zawierającego 2 funkcje, które zapisują / ładują stan do / z lokalnej pamięci:

// 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;
  }
};

Te funkcje są importowane przez store.js, gdzie konfigurujemy nasz sklep:

UWAGA: Musisz dodać jedną zależność: 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;

Sklep jest importowany do pliku index.js, dzięki czemu można go przekazać do dostawcy opakowującego plik 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')
)

Zwróć uwagę, że import bezwzględny wymaga tej zmiany w YourProjectFolder / jsconfig.json - to mówi mu, gdzie szukać plików, jeśli nie może ich na początku znaleźć. W przeciwnym razie zobaczysz skargi dotyczące próby zaimportowania czegoś spoza src .

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

CanProgram
źródło