¿Dónde escribir a localStorage en una aplicación Redux?

166

Quiero persistir algunas partes de mi árbol de estado en localStorage. ¿Cuál es el lugar apropiado para hacerlo? ¿Reductor o acción?

Marco de Jongh
fuente

Respuestas:

223

Reductor nunca es un lugar apropiado para hacer esto porque los reductores deben ser puros y no tener efectos secundarios.

Recomendaría solo hacerlo en un suscriptor:

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

Antes de crear la tienda, lea esas partes persistentes:

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

Si usa combineReducers(), notará que los reductores que no han recibido el estado se "arrancarán" normalmente usando su valor de stateargumento predeterminado . Esto puede ser bastante útil.

Es recomendable que renuncies a tu suscriptor para que no escribas en localStorage demasiado rápido, o tendrás problemas de rendimiento.

Finalmente, puede crear un middleware que encapsule eso como una alternativa, pero comenzaría con un suscriptor porque es una solución más simple y funciona bien.

Dan Abramov
fuente
1
¿Qué pasa si me gustaría suscribirme solo a una parte del estado? ¿es eso posible? Seguí adelante con algo un poco diferente: 1. guardar el estado persistente fuera de redux. 2. cargar el estado persistente dentro del constructor reaccionar (o con componentWillMount) con acción redux. ¿2 está totalmente bien en lugar de cargar los datos persistentes en la tienda directamente? (que estoy tratando de mantener por separado para SSR). Por cierto, gracias por Redux! Es increíble, me encanta, después de perderme con mi gran código de proyecto ahora todo vuelve a ser simple y predecible :)
Mark Uretsky
dentro de la devolución de llamada de store.subscribe, tiene acceso completo a los datos de la tienda actual, por lo que puede conservar cualquier parte que le interese
Bo Chen,
35
Dan Abramov también ha hecho un video completo sobre él: egghead.io/lessons/…
NateW
2
¿Existen preocupaciones legítimas de rendimiento con la serialización del estado en cada actualización de la tienda? ¿El navegador se serializa en un hilo separado tal vez?
Stephen Paul
77
Esto parece potencialmente derrochador. OP dijo que solo una parte del estado necesita persistir. Digamos que tienes una tienda con 100 llaves diferentes. Solo desea persistir 3 de ellos que se cambian con poca frecuencia. Ahora está analizando y actualizando la tienda local en cada pequeño cambio a cualquiera de sus 100 claves, mientras que ninguna de las 3 claves que le interesa persistir ha sido cambiada. La solución de @Gardezi a continuación es un mejor enfoque, ya que puede agregar oyentes de eventos a su middleware para que solo actualice cuando realmente necesite, por ejemplo: codepen.io/retrcult/pen/qNRzKN
Anna T
153

Para completar los espacios en blanco de la respuesta de Dan Abramov, podría usar store.subscribe()así:

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

Antes de crear la tienda, verifique localStoragey analice cualquier JSON bajo su clave de esta manera:

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

Luego pasa esta persistedStateconstante a su createStoremétodo de esta manera:

const store = createStore(
  reducer, 
  persistedState,
  /* any middleware... */
)
Andrew Samuelsen
fuente
66
Simple y efectivo, sin dependencias adicionales.
AxeEffect
1
crear un middleware puede verse un poco mejor, pero estoy de acuerdo en que es efectivo y suficiente para la mayoría de los casos
Bo Chen
¿Hay una buena manera de hacer esto al usar combineReducers, y solo desea conservar una tienda?
brendangibson
1
Si no hay nada en localStorage, ¿debería persistedStateregresar en initialStatelugar de un objeto vacío? De lo contrario, creo que createStorese inicializará con ese objeto vacío.
Alex
1
@ Alex Tiene razón, a menos que su estado inicial esté vacío.
Link14
47

En una palabra: middleware.

Echa un vistazo a redux-persist . O escribe el tuyo.

[ACTUALIZACIÓN 18 de diciembre de 2016] Editado para eliminar la mención de dos proyectos similares ahora inactivos o en desuso.

David L. Walsh
fuente
12

Si alguien tiene algún problema con las soluciones anteriores, puede escribir el suyo. Déjame mostrarte lo que hice. Ignorar las saga middlewarecosas solo se centran en dos cosas localStorageMiddlewarey reHydrateStoremétodos. el localStorageMiddlewareextrae todo redux statey lo coloca local storagey rehydrateStoreextrae todo el applicationStatealmacenamiento local si está presente y lo coloca enredux 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
fuente
2
Hola, ¿esto no daría lugar a muchas escrituras localStorageincluso cuando nada en la tienda ha cambiado? Cómo compensa las escrituras innecesarias
user566245
Bueno, funciona, de todos modos es una buena idea tener? Pregunta: ¿Qué pasó en caso de que tengamos más datos como reductores múltiples con datos grandes?
Kunvar Singh
3

No puedo responder a @Gardezi, pero una opción basada en su código podría ser:

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 diferencia es que solo estamos guardando algunas acciones, podría utilizar una función antirrebote para guardar solo la última interacción de su estado

Douglas Caina
fuente
1

Llegué un poco tarde, pero implementé un estado persistente de acuerdo con los ejemplos aquí expuestos. Si desea actualizar el estado solo cada X segundos, este enfoque puede ayudarlo a:

  1. Definir una función de contenedor

    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. Llame a una función de contenedor en su suscriptor

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

En este ejemplo, el estado se actualiza como máximo cada 5 segundos, independientemente de la frecuencia con que se active una actualización.

movcmpret
fuente
1
Usted puede envolver (state) => { updateLocalStorage(store.getState()) }en lodash.throttleeste aspecto: store.subscribe(throttle(() => {(state) => { updateLocalStorage(store.getState())} }y quitar tiempo comprobando el interior de la lógica.
GeorgeMA
1

Sobre la base de las excelentes sugerencias y extractos de código breve proporcionados en otras respuestas (y el artículo de Jam Creencia's Medium ), ¡aquí hay una solución completa!

Necesitamos un archivo que contenga 2 funciones que guarden / carguen el estado a / desde el almacenamiento 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;
  }
};

Esas funciones son importadas por store.js donde configuramos nuestra tienda:

NOTA: Deberá agregar una dependencia: 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;

La tienda se importa a index.js para que se pueda pasar al proveedor que envuelve 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')
)

Tenga en cuenta que las importaciones absolutas requieren este cambio en YourProjectFolder / jsconfig.json : esto le indica dónde buscar archivos si no puede encontrarlos al principio. De lo contrario, verá quejas sobre el intento de importar algo desde fuera de src .

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

CanProgram
fuente