Onde gravar no localStorage em um aplicativo Redux?

166

Quero persistir algumas partes da minha árvore de estado no localStorage. Qual é o local apropriado para fazer isso? Redutor ou ação?

Marco de Jongh
fonte

Respostas:

223

O redutor nunca é um local apropriado para fazer isso, porque os redutores devem ser puros e sem efeitos colaterais.

Eu recomendaria fazer isso em um assinante:

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

Antes de criar a loja, leia as partes persistentes:

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

Se você usar, combineReducers()notará que os redutores que não receberam o estado serão "inicializados" normalmente usando o statevalor padrão do argumento. Isso pode ser bastante útil.

É recomendável que você renuncie ao seu assinante para não gravar muito rapidamente no localStorage, ou você terá problemas de desempenho.

Finalmente, você pode criar um middleware que o encapsule como alternativa, mas eu começaria com um assinante porque é uma solução mais simples e faz o trabalho bem.

Dan Abramov
fonte
1
what Se eu gostaria de assinar apenas parte do estado? isso é possível? Fui adiante com algo um pouco diferente: 1. salve o estado persistente fora do redux. 2. carregar o estado persistido dentro do construtor react (ou com componentWillMount) com ação redux. 2 é totalmente bom em vez de carregar os dados persistentes na loja diretamente? (que estou tentando manter separadamente para SSR). A propósito, obrigado pelo Redux! É incrível, eu adoro isso, depois de se perder com o meu código grande projeto agora está tudo de volta para simples e :) previsível
Mark Uretsky
dentro callback store.subscribe, você tem acesso total aos dados de loja atual, de modo que você pode persistir qualquer parte que você está interessado em
Bo Chen
35
Dan Abramov também fez um vídeo inteiro: egghead.io/lessons/…
NateW 4/17
2
Existem preocupações legítimas de desempenho com a serialização do estado em todas as atualizações da loja? O navegador serializa em um thread separado, talvez?
Stephen Paul
7
Isso parece potencialmente inútil. O OP disse que apenas uma parte do estado precisa ser persistida. Digamos que você tenha uma loja com 100 chaves diferentes. Você deseja persistir apenas três deles que são alterados com pouca frequência. Agora você está analisando e atualizando o armazenamento local a cada pequena alteração em qualquer uma das suas 100 chaves, enquanto nenhuma das três chaves que você deseja persistir foi alterada. Solução por @Gardezi abaixo é uma abordagem melhor, como você pode adicionar ouvintes de eventos em seu middleware para que você única atualizar quando você realmente precisa, por exemplo: codepen.io/retrcult/pen/qNRzKN
Anna T
153

Para preencher os espaços em branco da resposta de Dan Abramov, você pode usar o store.subscribe()seguinte:

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

Antes de criar a loja, verifique localStoragee analise qualquer JSON sob sua chave da seguinte maneira:

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

Você passa essa persistedStateconstante para o seu createStoremétodo assim:

const store = createStore(
  reducer, 
  persistedState,
  /* any middleware... */
)
Andrew Samuelsen
fonte
6
Simples e eficaz, sem dependências extras.
AxeEffect
1
a criação de um middleware pode parecer um pouco melhor, mas eu concordo que é eficaz e é suficiente para a maioria dos casos
Bo Chen
Existe uma boa maneira de fazer isso ao usar o combineReducers e você deseja apenas manter um armazenamento?
Brendangibson
1
Se nada estiver em localStorage, deve persistedStateretornar em initialStatevez de um objeto vazio? Caso contrário, acho que createStoreserá inicializado com esse objeto vazio.
21417 Alex
1
@ Alex Você está certo, a menos que seu estado inicial esteja vazio.
precisa saber é
47

Em uma palavra: middleware.

Confira redux-persist . Ou escreva o seu.

[ATUALIZAÇÃO 18 de dezembro de 2016] Editado para remover a menção de dois projetos semelhantes agora inativos ou descontinuados.

David L. Walsh
fonte
12

Se alguém tiver algum problema com as soluções acima, você pode escrever para si mesmo. Deixe-me mostrar o que eu fiz. Ignore as saga middlewarecoisas, apenas se concentre em duas coisas localStorageMiddlewaree reHydrateStoremétodos. a localStorageMiddlewarepuxar todos os redux statee coloca-o local storagee rehydrateStorepuxar todos os applicationStateno armazenamento local se presente e coloca-lo emredux 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
fonte
2
Olá, isso não resultaria em muitas gravações, localStoragemesmo quando nada na loja mudou? Como compensar as gravações desnecessárias
user566245
Bem, funciona, enfim, é uma boa ideia ter isso? Pergunta: O que aconteceu no caso de termos mais menus de dados como múltiplos redutores com dados grandes?
Kunvar Singh
3

Não consigo responder @Gardezi, mas uma opção baseada em seu código poderia 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
    )
);

a diferença é que estamos apenas salvando algumas ações. Você pode usar uma função de debounce para salvar apenas a última interação do seu estado

Douglas Caina
fonte
1

Estou um pouco atrasado, mas implementei um estado persistente de acordo com os exemplos declarados aqui. Se você deseja atualizar o estado apenas a cada X segundos, essa abordagem pode ajudá-lo:

  1. Definir uma função de 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. Chame uma função de invólucro no seu assinante

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

Neste exemplo, o estado é atualizado no máximo a cada 5 segundos, independentemente da frequência com que uma atualização é acionada.

movcmpret
fonte
1
Você poderia envolver (state) => { updateLocalStorage(store.getState()) }em lodash.throttlecomo este: store.subscribe(throttle(() => {(state) => { updateLocalStorage(store.getState())} }e remover tempo verificando dentro da lógica.
GeorgeMA 20/02
1

Com base nas excelentes sugestões e trechos de código curto fornecidos em outras respostas (e no artigo Médio de Jam Creencia ), aqui está uma solução completa!

Precisamos de um arquivo contendo 2 funções que salvem / carreguem o estado para / do armazenamento 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;
  }
};

Essas funções são importadas pelo store.js, onde configuramos nossa loja:

NOTA: Você precisará adicionar uma dependência: 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;

A loja é importada para o index.js, para que possa ser transmitida ao provedor que envolve o 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')
)

Observe que as importações absolutas exigem essa alteração no YourProjectFolder / jsconfig.json - indica onde procurar arquivos, se não os encontrar primeiro. Caso contrário, você verá reclamações sobre a tentativa de importar algo de fora do src .

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

CanProgram
fonte