Dove scrivere su localStorage in un'app Redux?


Risposte:


224

Il riduttore non è mai il luogo adatto per farlo perché i riduttori dovrebbero essere puri e senza effetti collaterali.

Consiglierei di farlo in un abbonato:

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

Prima di creare il negozio, leggi quelle parti persistenti:

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

Se si utilizza, combineReducers()si noterà che i riduttori che non hanno ricevuto lo stato "si avvieranno" normalmente utilizzando il valore statedell'argomento predefinito . Questo può essere molto utile.

Si consiglia di rimbalzare l'abbonato in modo da non scrivere troppo velocemente su localStorage, altrimenti si avranno problemi di prestazioni.

Infine, puoi creare un middleware che lo incapsula come alternativa, ma inizierei con un abbonato perché è una soluzione più semplice e fa bene il lavoro.


1
cosa succede se mi piacerebbe abbonarmi a una parte dello stato? è possibile? Sono andato avanti con qualcosa di leggermente diverso: 1. salvare lo stato persistente al di fuori di redux. 2. caricare lo stato persistente all'interno del costruttore di reazioni (o con componentWillMount) con azione redux. 2 va benissimo invece di caricare direttamente i dati persistenti sullo store? (che sto cercando di conservare separatamente per SSR). A proposito, grazie per Redux! È fantastico, lo adoro, dopo essermi perso con il mio grande codice di progetto ora è tutto tornato a semplice e prevedibile :)
Mark Uretsky,

all'interno di call.subscribe callback, hai pieno accesso ai dati del negozio corrente, in modo da poter persistere su qualsiasi parte che ti interessa
Bo Chen

35
Dan Abramov ha anche fatto un intero video su di esso: egghead.io/lessons/…
NateW

2
Esistono legittimi problemi di prestazioni con la serializzazione dello stato su ogni aggiornamento del negozio? Il browser può serializzare su un thread separato forse?
Stephen Paul,

7
Questo sembra potenzialmente dispendioso. OP ha affermato che solo una parte dello stato deve essere mantenuta. Supponiamo che tu abbia un negozio con 100 chiavi diverse. Si desidera solo persistere 3 di essi che vengono modificati di rado. Ora stai analizzando e aggiornando l'archivio locale su ogni piccola modifica di una qualsiasi delle tue 100 chiavi, nel frattempo nessuna delle 3 chiavi che ti interessa perseverare non è mai stata cambiata. La soluzione di @Gardezi di seguito è un approccio migliore in quanto è possibile aggiungere listener di eventi nel middleware in modo da aggiornare solo quando è effettivamente necessario, ad esempio: codepen.io/retrcult/pen/qNRzKN
Anna T

153

Per riempire gli spazi vuoti della risposta di Dan Abramov è possibile utilizzare in store.subscribe()questo modo:

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

Prima di creare il negozio, controlla localStoragee analizza qualsiasi JSON sotto la tua chiave in questo modo:

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

Quindi passi questa persistedStatecostante al tuo createStoremetodo in questo modo:

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

6
Semplice ed efficace, senza dipendenze extra.
AxeEffect,

1
la creazione di un middleware può sembrare un po 'migliore, ma sono d'accordo che sia efficace ed è sufficiente per la maggior parte dei casi
Bo Chen,

Esiste un modo carino per farlo quando si utilizza CombinaReduttori e vuoi mantenere un solo negozio?
Brendangibson,

1
Se non è presente nulla in localStorage, dovrebbe essere persistedStaterestituito initialStateanziché un oggetto vuoto? Altrimenti penso che createStoreverrà inizializzato con quell'oggetto vuoto.
Alex,

1
@Alex Hai ragione, a meno che il tuo stato iniziale non sia vuoto.
Link14

47

In una parola: middleware.

Dai un'occhiata a redux-persist . O scrivi il tuo.

[AGGIORNAMENTO 18 dic 2016] Modificato per rimuovere la menzione di due progetti simili ora inattivi o obsoleti.


12

Se qualcuno ha qualche problema con le soluzioni di cui sopra, puoi scriverne uno tuo. Lascia che ti mostri cosa ho fatto. Ignora le saga middlewarecose, concentrati solo su due cose localStorageMiddlewaree reHydrateStoremetodi. il localStorageMiddlewaretirare tutte le redux statee lo mette in local storagee rehydrateStoretirare tutti i applicationStatenella memoria locale se presente e lo mette inredux 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
Salve, questo non comporterebbe molte scritture localStorageanche se nulla nel negozio è cambiato? Come compensare le scritture non necessarie
user566245

Bene, funziona, comunque è una buona idea avere? Domanda: Cosa succede in caso di più dati per più riduttori con dati di grandi dimensioni?
Kunvar Singh

3

Non posso rispondere a @Gardezi ma un'opzione basata sul suo codice potrebbe essere:

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

la differenza è che stiamo solo salvando alcune azioni, potresti usare una funzione di debounce per salvare solo l'ultima interazione del tuo stato


1

Sono un po 'in ritardo ma ho implementato uno stato persistente secondo gli esempi qui riportati. Se vuoi aggiornare lo stato solo ogni X secondi, questo approccio può aiutarti:

  1. Definire una funzione wrapper

    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. Chiama una funzione wrapper nel tuo abbonato

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

In questo esempio, lo stato viene aggiornato al massimo ogni 5 secondi, indipendentemente dalla frequenza con cui viene attivato un aggiornamento.


1
Si potrebbe avvolgere (state) => { updateLocalStorage(store.getState()) }in lodash.throttlequesto modo: store.subscribe(throttle(() => {(state) => { updateLocalStorage(store.getState())} }e togliere tempo a controllare la logica interna.
GeorgeMA,

1

Sulla base degli eccellenti suggerimenti e degli estratti di codici brevi forniti in altre risposte (e nell'articolo Medium di Jam Creencia ), ecco una soluzione completa!

Abbiamo bisogno di un file contenente 2 funzioni che salvano / caricano lo stato nella / dalla memoria locale:

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

Queste funzioni sono importate da store.js dove configuriamo il nostro negozio:

NOTA: è necessario aggiungere una dipendenza: 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;

Il negozio viene importato in index.js in modo che possa essere passato nel provider che avvolge 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')
)

Si noti che le importazioni assolute richiedono questa modifica in YourProjectFolder / jsconfig.json - questo indica dove cercare i file se non li trova all'inizio. Altrimenti, vedrai lamentele riguardo al tentativo di importare qualcosa dall'esterno di src .

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

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.