Wo kann man in einer Redux-App in localStorage schreiben?

165

Ich möchte einige Teile meines Statusbaums im localStorage beibehalten. Was ist der geeignete Ort dafür? Reduzierer oder Aktion?

Marco de Jongh
quelle

Antworten:

222

Reduktionsmittel sind niemals ein geeigneter Ort, um dies zu tun, da Reduzierstücke rein sein sollten und keine Nebenwirkungen haben sollten.

Ich würde empfehlen, es nur in einem Abonnenten zu tun:

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

Lesen Sie vor dem Erstellen des Geschäfts die folgenden Teile:

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

Wenn Sie verwenden, werden combineReducers()Sie feststellen, dass Reduzierungen, die den Status nicht erhalten haben, wie gewohnt mit ihrem Standardargumentwert "hochfahren" state. Das kann sehr praktisch sein.

Es ist ratsam, dass Sie Ihren Abonnenten entprellen, damit Sie nicht zu schnell in localStorage schreiben, da sonst Leistungsprobleme auftreten.

Schließlich können Sie eine Middleware erstellen, die dies als Alternative kapselt. Ich würde jedoch mit einem Abonnenten beginnen, da dies eine einfachere Lösung ist und die Aufgabe gut erledigt.

Dan Abramov
quelle
1
Was ist, wenn ich nur einen Teil des Staates abonnieren möchte? ist das möglich? Ich habe etwas anderes gemacht: 1. Speichern Sie den dauerhaften Zustand außerhalb von Redux. 2. Laden Sie den dauerhaften Zustand im Reaktionskonstruktor (oder mit componentWillMount) mit Redux-Aktion. Ist 2 völlig in Ordnung, anstatt die persistierten Daten direkt in den Speicher zu laden? (was ich versuche, separat für SSR zu behalten). Übrigens danke für Redux! Es ist großartig, ich liebe es, nachdem ich mich mit meinem großen Projektcode verlaufen habe, ist alles wieder einfach und vorhersehbar :)
Mark Uretsky
Innerhalb des Rückrufs von store.subscribe haben Sie vollen Zugriff auf die aktuellen Geschäftsdaten, sodass Sie jeden Teil, an dem Sie interessiert sind, beibehalten können
Bo Chen
35
Dan Abramov hat auch ein ganzes Video darauf gemacht: egghead.io/lessons/…
NateW
2
Gibt es berechtigte Leistungsprobleme bei der Serialisierung des Status bei jedem Store-Update? Serialisiert der Browser möglicherweise in einem separaten Thread?
Stephen Paul
7
Dies sieht möglicherweise verschwenderisch aus. OP hat gesagt, dass nur ein Teil des Staates bestehen bleiben muss. Angenommen, Sie haben ein Geschäft mit 100 verschiedenen Schlüsseln. Sie möchten nur 3 davon beibehalten, die nur selten geändert werden. Jetzt analysieren und aktualisieren Sie den lokalen Speicher bei jeder kleinen Änderung an einem Ihrer 100 Schlüssel, während keiner der 3 Schlüssel, die Sie beibehalten möchten, jemals geändert wurde. Die unten stehende Lösung von @Gardezi ist ein besserer Ansatz, da Sie Ereignis-Listener zu Ihrer Middleware hinzufügen können, sodass Sie nur dann aktualisieren, wenn Sie tatsächlich Folgendes
Anna T
153

Um die Lücken in Dan Abramovs Antwort zu füllen, könnten Sie Folgendes verwenden store.subscribe():

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

Überprüfen localStorageund analysieren Sie vor dem Erstellen des Speichers JSON unter Ihrem Schlüssel wie folgt:

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

Sie übergeben diese persistedStateKonstante dann createStorewie folgt an Ihre Methode:

const store = createStore(
  reducer, 
  persistedState,
  /* any middleware... */
)
Andrew Samuelsen
quelle
6
Einfach und effektiv, ohne zusätzliche Abhängigkeiten.
AxeEffect
1
Das Erstellen einer Middleware sieht vielleicht etwas besser aus, aber ich stimme zu, dass sie effektiv ist und in den meisten Fällen ausreicht
Bo Chen
Gibt es eine gute Möglichkeit, dies zu tun, wenn Sie combinReducers verwenden, und Sie möchten nur ein Geschäft beibehalten?
brendangibson
1
Wenn nichts in localstorage ist, sollte persistedStatezurückgeben initialStatestatt einem leeren Objekt? Ansonsten denke ich, createStorewird mit diesem leeren Objekt initialisiert.
Alex
1
@Alex Sie haben Recht, es sei denn, Ihr initialState ist leer.
Link14
47

Mit einem Wort: Middleware.

Schauen Sie sich Redux-Persist an . Oder schreibe deine eigenen.

[UPDATE 18 Dec 2016] Bearbeitet, um die Erwähnung von zwei ähnlichen Projekten zu entfernen, die jetzt inaktiv oder veraltet sind.

David L. Walsh
quelle
12

Wenn jemand Probleme mit den oben genannten Lösungen hat, können Sie Ihre eigenen schreiben. Lassen Sie mich Ihnen zeigen, was ich getan habe. Ignorieren Sie saga middlewareDinge, konzentrieren Sie sich nur auf zwei Dinge localStorageMiddlewareund reHydrateStoreMethoden. Das localStorageMiddlewarezieht alles redux stateund legt es ein local storageund rehydrateStorezieht alles applicationStatein den lokalen Speicher, falls vorhanden und legt es einredux 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
quelle
2
Hallo, würde dies nicht zu vielen Schreibvorgängen führen, localStorageselbst wenn sich nichts im Laden geändert hat? Wie Sie unnötige Schreibvorgänge kompensieren
user566245
Nun, es funktioniert, trotzdem ist es diese gute Idee zu haben? Frage: Was ist passiert, wenn wir mehr Datenmenüs mit mehreren Reduzierungen und großen Datenmengen haben?
Kunvar Singh
3

Ich kann @Gardezi nicht beantworten, aber eine Option, die auf seinem Code basiert, könnte sein:

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

Der Unterschied besteht darin, dass wir nur einige Aktionen speichern. Sie können eine Entprellungsfunktion verwenden, um nur die letzte Interaktion Ihres Status zu speichern

Douglas Caina
quelle
1

Ich bin etwas spät dran, habe aber einen dauerhaften Zustand gemäß den hier angegebenen Beispielen implementiert. Wenn Sie den Status nur alle X Sekunden aktualisieren möchten, kann dieser Ansatz Ihnen helfen:

  1. Definieren Sie eine Wrapper-Funktion

    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. Rufen Sie eine Wrapper-Funktion in Ihrem Abonnenten auf

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

In diesem Beispiel wird der Status höchstens alle 5 Sekunden aktualisiert , unabhängig davon, wie oft eine Aktualisierung ausgelöst wird.

movcmpret
quelle
1
Sie könnten wickeln (state) => { updateLocalStorage(store.getState()) }in lodash.throttleetwa so aus : store.subscribe(throttle(() => {(state) => { updateLocalStorage(store.getState())} }und entfernen Zeitlogik innerhalb überprüfen.
GeorgeMA
1

Aufbauend auf den hervorragenden Vorschlägen und Kurzcode- Auszügen, die in anderen Antworten (und in Jam Creencias Medium-Artikel ) enthalten sind, finden Sie hier eine vollständige Lösung!

Wir benötigen eine Datei mit 2 Funktionen, die den Status im / vom lokalen Speicher speichern / laden:

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

Diese Funktionen werden von store.js importiert, in dem wir unseren Store konfigurieren:

HINWEIS: Sie müssen eine Abhängigkeit hinzufügen: 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;

Der Store wird in index.js importiert, damit er an den Provider übergeben werden kann, der App.js umschließt :

// 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')
)

Beachten Sie, dass für absolute Importe diese Änderung in YourProjectFolder / jsconfig.json erforderlich ist. Hier erfahren Sie, wo nach Dateien gesucht werden muss, wenn sie zuerst nicht gefunden werden können. Andernfalls werden Beschwerden über den Versuch angezeigt, etwas von außerhalb von src zu importieren .

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

CanProgram
quelle