Où écrire sur localStorage dans une application Redux?

167

Je souhaite conserver certaines parties de mon arborescence d'état dans le localStorage. Quel est le lieu approprié pour le faire? Réducteur ou action?

Marco de Jongh
la source

Réponses:

224

Le réducteur n'est jamais un endroit approprié pour le faire, car les réducteurs doivent être purs et ne pas avoir d'effets secondaires.

Je recommanderais simplement de le faire dans un abonné:

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

Avant de créer le magasin, lisez ces parties persistantes:

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

Si vous utilisez, combineReducers()vous remarquerez que les réducteurs qui n'ont pas reçu l'état «démarreront» normalement en utilisant leur valeur d' stateargument par défaut . Cela peut être très pratique.

Il est conseillé de supprimer votre abonné afin de ne pas écrire trop rapidement dans localStorage, sinon vous aurez des problèmes de performances.

Enfin, vous pouvez créer un middleware qui encapsule cela comme une alternative, mais je commencerais par un abonné car c'est une solution plus simple et fait bien le travail.

Dan Abramov
la source
1
que faire si je souhaite m'abonner à une partie seulement de l'état? est-ce possible? Je suis allé de l'avant avec quelque chose d'un peu différent: 1. enregistrer l'état persistant en dehors de redux. 2. chargez l'état persistant dans le constructeur react (ou avec componentWillMount) avec l'action redux. Est-ce que 2 est tout à fait correct au lieu de charger directement les données persistantes sur le magasin? (que j'essaye de garder séparément pour SSR). Au fait, merci pour Redux! C'est génial, j'adore ça, après m'être perdu avec mon gros code de projet maintenant tout est revenu à la simplicité et à la prévisibilité :)
Mark Uretsky
dans le rappel de store.subscribe, vous avez un accès complet aux données du magasin actuel, vous pouvez donc conserver toute partie qui vous intéresse
Bo Chen
35
Dan Abramov a également fait une vidéo entière dessus: egghead.io/lessons
...
2
Existe-t-il des problèmes de performances légitimes liés à la sérialisation de l'état à chaque mise à jour du magasin? Le navigateur sérialise-t-il peut-être sur un thread séparé?
Stephen Paul
7
Cela semble potentiellement inutile. OP a dit que seule une partie de l'État avait besoin de persister. Disons que vous avez un magasin avec 100 clés différentes. Vous ne voulez conserver que 3 d'entre eux qui sont rarement modifiés. Maintenant, vous analysez et mettez à jour le magasin local à chaque petit changement de l'une de vos 100 clés, alors qu'aucune des 3 clés que vous souhaitez conserver n'a jamais été modifiée. La solution de @Gardezi ci-dessous est une meilleure approche car vous pouvez ajouter des écouteurs d'événements dans votre middleware afin de ne mettre à jour que lorsque vous en avez réellement besoin, par exemple: codepen.io/retrcult/pen/qNRzKN
Anna T
153

Pour remplir les blancs de la réponse de Dan Abramov, vous pouvez utiliser store.subscribe()comme ceci:

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

Avant de créer le magasin, vérifiez localStorageet analysez tout JSON sous votre clé comme ceci:

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

Vous passez ensuite cette persistedStateconstante à votre createStoreméthode comme ceci:

const store = createStore(
  reducer, 
  persistedState,
  /* any middleware... */
)
Andrew Samuelsen
la source
6
Simple et efficace, sans dépendances supplémentaires.
AxeEffect
1
créer un middleware peut sembler un peu mieux, mais je suis d'accord que c'est efficace et suffisant dans la plupart des cas
Bo Chen
Existe-t-il un bon moyen de le faire lorsque vous utilisez combineReducers et que vous ne souhaitez conserver qu'un seul magasin?
brendangibson le
1
Si rien n'est dans localStorage, doit-il persistedStateretourner initialStateau lieu d'un objet vide? Sinon, je pense que createStoreva initialiser avec cet objet vide.
Alex
1
@Alex Vous avez raison, sauf si votre état initial est vide.
Link14
47

En un mot: middleware.

Découvrez redux-persist . Ou écrivez le vôtre.

[UPDATE 18 Dec 2016] Modifié pour supprimer la mention de deux projets similaires désormais inactifs ou obsolètes.

David L. Walsh
la source
12

Si quelqu'un a un problème avec les solutions ci-dessus, vous pouvez écrire le vôtre. Laissez-moi vous montrer ce que j'ai fait. Ignorez les saga middlewarechoses, concentrez-vous simplement sur deux choses localStorageMiddlewareet la reHydrateStoreméthode. le localStorageMiddlewaretirer tout le redux stateet le met dedans local storageet rehydrateStoretirez tout le applicationStatedans le stockage local si présent et le met dedansredux 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
la source
2
Salut, cela n'entraînerait-il pas beaucoup d'écritures localStoragemême si rien dans le magasin n'a changé? Comment compenser les écritures inutiles
user566245
Eh bien, ça marche, de toute façon c'est cette bonne idée d'avoir? Question: Que se passe-t-il si nous avons plus de données comme plusieurs réducteurs avec des données volumineuses?
Kunvar Singh
3

Je ne peux pas répondre à @Gardezi mais une option basée sur son code pourrait être:

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 différence est que nous ne sauvegardons que quelques actions, vous pouvez éventuellement utiliser une fonction anti-rebond pour ne sauvegarder que la dernière interaction de votre état

Douglas Caina
la source
1

Je suis un peu en retard mais j'ai implémenté un état persistant selon les exemples cités ici. Si vous souhaitez mettre à jour l'état uniquement toutes les X secondes, cette approche peut vous aider:

  1. Définir une fonction 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. Appeler une fonction wrapper dans votre abonné

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

Dans cet exemple, l'état est mis à jour au maximum toutes les 5 secondes, quelle que soit la fréquence à laquelle une mise à jour est déclenchée.

movcmpret
la source
1
Vous pouvez envelopper (state) => { updateLocalStorage(store.getState()) }dans lodash.throttlecomme ceci: store.subscribe(throttle(() => {(state) => { updateLocalStorage(store.getState())} }et de supprimer la logique de vérification de temps à l' intérieur.
GeorgeMA
1

S'appuyant sur les excellentes suggestions et extraits de code courts fournis dans d'autres réponses (et l'article Medium de Jam Creencia ), voici une solution complète!

Nous avons besoin d'un fichier contenant 2 fonctions qui sauvegardent / chargent l'état vers / depuis le stockage local:

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

Ces fonctions sont importées par store.js où nous configurons notre boutique:

REMARQUE: vous devrez ajouter une dépendance: 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;

Le magasin est importé dans index.js afin qu'il puisse être transmis au fournisseur qui encapsule 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')
)

Notez que les importations absolues nécessitent cette modification de YourProjectFolder / jsconfig.json - cela lui indique où chercher les fichiers s'il ne peut pas les trouver au début. Sinon, vous verrez des plaintes concernant la tentative d'importer quelque chose de l'extérieur de src .

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

Programme Can
la source