Come resettare lo stato di un negozio Redux?

Sto usando Redux per la gestione dello stato.
Come faccio a ripristinare il negozio al suo stato iniziale?

Ad esempio, supponiamo di avere due account utente ( u1 e u2 ).
Immagina la seguente sequenza di eventi:

  1. L’utente u1 accede all’app e fa qualcosa, quindi memorizziamo alcuni dati nell’archivio.

  2. L’utente u1 disconnette.

  3. L’utente u2 accede all’app senza aggiornare il browser.

A questo punto, i dati nella cache saranno associati a u1 , e vorrei pulirlo.

Come posso ripristinare il negozio Redux al suo stato iniziale quando il primo utente si disconnette?

    Un modo per farlo sarebbe scrivere un riduttore di radice nella tua applicazione.

    Il riduttore di radice delegherebbe normalmente la gestione dell’azione al riduttore generato da combineReducers() . Tuttavia, ogni volta che riceve un’azione USER_LOGOUT , restituisce lo stato iniziale da capo.

    Ad esempio, se il tuo riduttore di radici è simile a questo:

     const rootReducer = combineReducers({ /* your app's top-level reducers */ }) 

    Puoi rinominarlo in appReducer e scrivere un nuovo rootReducer deleghi:

     const appReducer = combineReducers({ /* your app's top-level reducers */ }) const rootReducer = (state, action) => { return appReducer(state, action) } 

    Ora dobbiamo solo insegnare al nuovo rootReducer per restituire lo stato iniziale dopo USER_LOGOUT azione USER_LOGOUT . Come sappiamo, i riduttori dovrebbero restituire lo stato iniziale quando vengono chiamati con un undefined come primo argomento, indipendentemente dall’azione. Usiamo questo fatto per appReducer condizionatamente lo state accumulato mentre lo passiamo a appReducer :

      const rootReducer = (state, action) => { if (action.type === 'USER_LOGOUT') { state = undefined } return appReducer(state, action) } 

    Ora, ogni volta che USER_LOGOUT viene USER_LOGOUT , tutti i riduttori verranno inizializzati di nuovo. Possono anche restituire qualcosa di diverso rispetto a prima, se lo desiderano, in quanto possono controllare anche action.type .

    Per reiterare, il nuovo codice completo ha il seguente aspetto:

     const appReducer = combineReducers({ /* your app's top-level reducers */ }) const rootReducer = (state, action) => { if (action.type === 'USER_LOGOUT') { state = undefined } return appReducer(state, action) } 

    Nota che non sto mutando lo stato qui, sto semplicemente riassegnando il riferimento di una variabile locale chiamata state prima di passarla ad un’altra funzione. La mutazione di un object di stato sarebbe una violazione dei principi di Redux.

    In caso di utilizzo di redux-persist , potrebbe essere necessario pulire lo spazio di archiviazione. Redux-perist conserva una copia del tuo stato in un motore di archiviazione, che verrà caricato da lì all’aggiornamento.

    Innanzitutto, è necessario importare il motore di archiviazione appropriato e quindi, analizzare lo stato prima di impostarlo su undefined e pulire ogni chiave dello stato di archiviazione.

     const rootReducer = (state, action) => { if (action.type === SIGNOUT_REQUEST) { Object.keys(state).forEach(key => { storage.removeItem(`persist:${key}`); }); state = undefined; } return AppReducers(state, action); }; 

    Vorrei sottolineare che il commento accettato da Dan Abramov è corretto, tranne che abbiamo riscontrato uno strano problema quando usavamo il pacchetto react-router-redux insieme a questo approccio. La nostra soluzione era quella di non impostare lo stato in modo undefined ma piuttosto utilizzare il riduttore di instradamento corrente. Quindi suggerirei di implementare la soluzione qui sotto se si utilizza questo pacchetto

     const rootReducer = (state, action) => { if (action.type === 'USER_LOGOUT') { const { routing } = state state = { routing } } return appReducer(state, action) } 

    Definisci un’azione:

     const RESET_ACTION = { type: "RESET" } 

    Quindi, in ciascuno dei riduttori, supponendo che si stia utilizzando switch o if-else per gestire più azioni attraverso ciascun riduttore. Ho intenzione di prendere il caso per un switch .

     const INITIAL_STATE = { loggedIn: true } const randomReducer = (state=INITIAL_STATE, action) { switch(action.type) { case 'SOME_ACTION_TYPE': //do something with it case "RESET": return INITIAL_STATE; //Always return the initial state default: return state; } } 

    In questo modo, ogni volta che si chiama RESET , il riduttore aggiornerà il negozio con lo stato predefinito.

    Ora, per il logout puoi gestire i seguenti:

     const logoutHandler = () => { store.dispatch(RESET_ACTION) // Also the custom logic like for the rest of the logout handler } 

    Ogni volta che un utente accede, senza un aggiornamento del browser. Il negozio sarà sempre al valore predefinito.

    store.dispatch(RESET_ACTION) elabora appena l’idea. Molto probabilmente avrai un creatore di azioni per lo scopo. Un modo molto migliore sarà avere LOGOUT_ACTION .

    Una volta inviato questo LOGOUT_ACTION . Un middleware personalizzato può quindi intercettare questa azione, con Redux-Saga o Redux-Thunk. In entrambi i modi, tuttavia, puoi inviare un’altra azione ‘RESET’. In questo modo, il logout del negozio e il reset avverranno in modo sincrono e il tuo negozio sarà pronto per un altro accesso utente.

      const reducer = (state = initialState, { type, payload }) => { switch (type) { case RESET_STORE: { state = initialState } break } return state } 

    È inoltre ansible eseguire un’azione gestita da tutti o alcuni riduttori, che si desidera ripristinare al negozio iniziale. Un’azione può innescare un ripristino dell’intero stato, o solo un pezzo di esso che ti sembra adatto. Credo che questo sia il modo più semplice e più controllabile per farlo.

    Con Redux si è applicata la seguente soluzione, che presuppone di aver impostato uno stato iniziale in tutti i miei riduttori (ad esempio {utente: {nome, email}}). In molti componenti controllo su queste proprietà annidate, quindi con questa correzione prevengo che i miei metodi di rendering vengano interrotti su condizioni di proprietà accoppiate (ad es. Se state.user.email, che genererà un errore, l’utente non è definito se le soluzioni sopra menzionate).

     const appReducer = combineReducers({ tabs, user }) const initialState = appReducer({}, {}) const rootReducer = (state, action) => { if (action.type === 'LOG_OUT') { state = initialState } return appReducer(state, action) } 

    Combinando gli approcci di Dan, Ryan e Rob, per tenere conto del mantenimento dello stato del router e inizializzare tutto il resto nell’albero dello stato, ho finito con questo:

     const rootReducer = (state, action) => appReducer(action.type === LOGOUT ? { ...appReducer({}, {}), router: state && state.router || {} } : state, action); 

    AGGIORNAMENTO NGRX4

    Se si sta eseguendo la migrazione a NGRX 4, si è notato dalla guida alla migrazione che il metodo rootreducer per la combinazione dei riduttori è stato sostituito con il metodo ActionReducerMap. All’inizio, questo nuovo modo di fare le cose potrebbe rendere la reimpostazione una sfida. In realtà è semplice, ma il modo di farlo è cambiato.

    Questa soluzione è ispirata alla sezione API dei meta-riduttori dei documenti Github NGRX4.

    Innanzitutto, diciamo che stai combinando i tuoi riduttori in questo modo usando la nuova opzione ActionReducerMap di NGRX:

     //index.reducer.ts export const reducers: ActionReducerMap = { auth: fromAuth.reducer, layout: fromLayout.reducer, users: fromUsers.reducer, networks: fromNetworks.reducer, routingDisplay: fromRoutingDisplay.reducer, routing: fromRouting.reducer, routes: fromRoutes.reducer, routesFilter: fromRoutesFilter.reducer, params: fromParams.reducer } 

    Ora, diciamo che vuoi reimpostare lo stato da app.module `

     //app.module.ts import { IndexReducer } from './index.reducer'; import { StoreModule, ActionReducer, MetaReducer } from '@ngrx/store'; ... export function debug(reducer: ActionReducer): ActionReducer { return function(state, action) { switch (action.type) { case fromAuth.LOGOUT: console.log("logout action"); state = undefined; } return reducer(state, action); } } export const metaReducers: MetaReducer[] = [debug]; @NgModule({ imports: [ ... StoreModule.forRoot(reducers, { metaReducers}), ... ] }) export class AppModule { } 

    `

    E questo è fondamentalmente un modo per ottenere lo stesso effetto con NGRX 4.

    Ho creato un componente per consentire a Redux di reimpostare lo stato, è sufficiente utilizzare questo componente per migliorare il tuo negozio e inviare uno specifico action.type per triggersre il ripristino. Il pensiero dell’attuazione è lo stesso di quello che @Dan Abramov ha detto.

    Github: https://github.com/wwayne/redux-reset

    Solo una risposta semplificata alla migliore risposta:

     const rootReducer = combineReducers({ auth: authReducer, ...formReducers, routing }); export default (state, action) => ( action.type === 'USER_LOGOUT' ? rootReducer(undefined, action) : rootReducer(state, action) ) 

    Ho creato azioni per chiarire lo stato. Così, quando invio un creatore di azioni di logout, invio anche le azioni per cancellare lo stato.

    Azione record utente

     export const clearUserRecord = () => ({ type: CLEAR_USER_RECORD }); 

    Logout action creatore

     export const logoutUser = () => { return dispatch => { dispatch(requestLogout()) dispatch(receiveLogout()) localStorage.removeItem('auth_token') dispatch({ type: 'CLEAR_USER_RECORD' }) } }; 

    Reducer

     const userRecords = (state = {isFetching: false, userRecord: [], message: ''}, action) => { switch (action.type) { case REQUEST_USER_RECORD: return { ...state, isFetching: true} case RECEIVE_USER_RECORD: return { ...state, isFetching: false, userRecord: action.user_record} case USER_RECORD_ERROR: return { ...state, isFetching: false, message: action.message} case CLEAR_USER_RECORD: return {...state, isFetching: false, message: '', userRecord: []} default: return state } }; 

    Non sono sicuro che sia ottimale?

    Solo un’estensione della risposta di @ dan-abramov , a volte potremmo aver bisogno di mantenere certe chiavi dall’essere resettate.

     const retainKeys = ['appConfig']; const rootReducer = (state, action) => { if (action.type === 'LOGOUT_USER_SUCCESS' && state) { state = !isEmpty(retainKeys) ? pick(state, retainKeys) : undefined; } return appReducer(state, action); }; 

    Questo approccio è molto giusto: distruggi qualsiasi stato specifico “NAME” per ignorare e mantenere gli altri.

     const rootReducer = (state, action) => { if (action.type === 'USER_LOGOUT') { state.NAME = undefined } return appReducer(state, action) } 

    perché non usi semplicemente return module.exports.default() 😉

     export default (state = {pending: false, error: null}, action = {}) => { switch (action.type) { case "RESET_POST": return module.exports.default(); case "SEND_POST_PENDING": return {...state, pending: true, error: null}; // .... } return state; } 

    Nota: assicurati di impostare il valore predefinito dell’azione su {} e stai bene perché non vuoi riscontrare errori quando controlli action.type all’interno action.type switch.

    Se stai usando azioni di redux , ecco una soluzione rapida usando un HOF ( funzione di ordine superiore ) per handleActions .

     import { handleActions } from 'redux-actions'; export function handleActionsEx(reducer, initialState) { const enhancedReducer = { ...reducer, RESET: () => initialState }; return handleActions(enhancedReducer, initialState); } 

    Quindi utilizzare handleActionsEx anziché handleActions originali per gestire i riduttori.

    La risposta di Dan dà una grande idea di questo problema, ma non ha funzionato bene per me, perché sto usando redux-persist .
    Se usato con redux-persist , il semplice passaggio dello stato non undefined non ha triggersto il comportamento persistente, quindi sapevo che dovevo rimuovere manualmente l’elemento dallo spazio di archiviazione (React Native nel mio caso, quindi AsyncStorage ).

     await AsyncStorage.removeItem('persist:root'); 

    o

     await persistor.flush(); // or await persistor.purge(); 

    non ha funzionato neanche per me – mi hanno urlato contro. (es. lamentarsi come “Chiave inaspettata _persist …” )

    Poi all’improvviso ho riflettuto su tutto ciò che desidero: basta fare in modo che ogni singolo riduttore restituisca il proprio stato iniziale quando viene rilevato il tipo di azione RESET . In questo modo, la persistenza viene gestita in modo naturale. Ovviamente senza la funzione di utilità di cui sopra ( handleActionsEx ), il mio codice non sembrerà DRY (anche se è solo un liner, cioè RESET: () => initialState ), ma non potrei sopportarlo RESET: () => initialState ‘amo metaprogramming.

    La seguente soluzione ha funzionato per me.

    Ho aggiunto la funzione di stato di ripristino ai meta-riduttori. La chiave era da usare

     return reducer(undefined, action); 

    per impostare tutti i riduttori allo stato iniziale. Il ritorno undefined invece causava errori dovuti al fatto che la struttura del negozio è stata distrutta.

    /reducers/index.ts

     export function resetState(reducer: ActionReducer): ActionReducer { return function (state: State, action: Action): State { switch (action.type) { case AuthActionTypes.Logout: { return reducer(undefined, action); } default: { return reducer(state, action); } } }; } export const metaReducers: MetaReducer[] = [ resetState ]; 

    app.module.ts

     import { StoreModule } from '@ngrx/store'; import { metaReducers, reducers } from './reducers'; @NgModule({ imports: [ StoreModule.forRoot(reducers, { metaReducers }) ] }) export class AppModule {} 

    Ho trovato che la risposta accettata ha funzionato bene per me, ma ha innescato l’errore ESLint no-param-reassignhttps://eslint.org/docs/rules/no-param-reassign

    Ecco come l’ho gestito, assicurandomi di creare una copia dello stato (che è, a mio modo di vedere, la cosa da fare di Reduxy …):

     import { combineReducers } from "redux" import { routerReducer } from "react-router-redux" import ws from "reducers/ws" import session from "reducers/session" import app from "reducers/app" const appReducer = combineReducers({ "routing": routerReducer, ws, session, app }) export default (state, action) => { const stateCopy = action.type === "LOGOUT" ? undefined : { ...state } return appReducer(stateCopy, action) } 

    Ma forse creare una copia dello stato per passarlo semplicemente in un’altra funzione di riduttore che crea una copia di quello che è un po ‘complicato? Questo non è molto piacevole, ma è più immediato:

     export default (state, action) => { return appReducer(action.type === "LOGOUT" ? undefined : state, action) } 

    La mia soluzione alternativa quando si lavora con typescript, costruita sopra la risposta di Dan (le digitazioni redux rendono imansible passare undefined al riduttore come primo argomento, quindi memorizzo lo stato root iniziale della cache in una costante):

     // store export const store: Store = createStore( rootReducer, storeEnhacer, ) export const initialRootState = { ...store.getState(), } // root reducer const appReducer = combineReducers(reducers) export const rootReducer = (state: IStoreState, action: IAction) => { if (action.type === "USER_LOGOUT") { return appReducer(initialRootState, action) } return appReducer(state, action) } // auth service class Auth { ... logout() { store.dispatch({type: "USER_LOGOUT"}) } } 

    Oltre alla risposta di Dan Abramov, non dovremmo impostare esplicitamente un’azione come action = {type: ‘@@ INIT’} accanto a state = undefined. Con il tipo di azione sopra, ogni riduttore restituisce lo stato iniziale.

    nel server, ho una variabile è: global.isSrr = true e in ogni riduttore, ho un const è: initialState Per ripristinare i dati nello Store, faccio quanto segue con ciascun Reducer: esempio con appReducer.js :

      const initialState = { auth: {}, theme: {}, sidebar: {}, lsFanpage: {}, lsChatApp: {}, appSelected: {}, }; export default function (state = initialState, action) { if (typeof isSsr!=="undefined" && isSsr) { //< == using global.isSsr = true state = {...initialState};//<= important "will reset the data every time there is a request from the client to the server" } switch (action.type) { //...other code case here default: { return state; } } } 

    finalmente sul router del server:

     router.get('*', (req, res) => { store.dispatch({type:'reset-all-blabla'});//< = unlike any action.type // i use Math.random() // code ....render ssr here }); 

    La seguente soluzione funziona per me.

    Prima di iniziare la nostra applicazione lo stato del riduttore è fresco e nuovo con InitialState predefinito.

    Dobbiamo aggiungere un’azione che richiama il caricamento iniziale di APP per mantenere lo stato predefinito .

    Durante la disconnessione dall’applicazione, possiamo semplicemente reimpostare lo stato di default e il riduttore funzionerà come nuovo .

    Contenitore principale APP

      componentDidMount() { this.props.persistReducerState(); } 

    Main APP Reducer

     const appReducer = combineReducers({ user: userStatusReducer, analysis: analysisReducer, incentives: incentivesReducer }); let defaultState = null; export default (state, action) => { switch (action.type) { case appActions.ON_APP_LOAD: defaultState = defaultState || state; break; case userLoginActions.USER_LOGOUT: state = defaultState; return state; default: break; } return appReducer(state, action); }; 

    On Logout chiama l’azione per il ripristino dello stato

     function* logoutUser(action) { try { const response = yield call(UserLoginService.logout); yield put(LoginActions.logoutSuccess()); } catch (error) { toast.error(error.message, { position: toast.POSITION.TOP_RIGHT }); } } 

    Spero che questo risolva il tuo problema!

    Un’altra opzione è:

     store.dispatch({type: '@@redux/INIT'}) 

    '@@redux/INIT' è il tipo di azione che redux invia automaticamente quando createStore , quindi supponendo che i vostri riduttori abbiano già un valore predefinito, questo verrebbe catturato da quelli e farà ripartire il vostro stato. Potrebbe essere considerato un dettaglio di implementazione privato di redux, però, quindi l’acquirente deve stare attento …

    Dal punto di vista della sicurezza, la cosa più sicura da fare quando si registra un utente è ripristinare tutti gli stati persistenti (ex cookie, localStorage , localStorage , Web SQL , ecc.) E fare un aggiornamento della pagina utilizzando window.location.reload() . È ansible che uno sviluppatore sciatto memorizzi accidentalmente o intenzionalmente alcuni dati sensibili su una window , nel DOM, ecc. Eliminare tutti gli stati persistenti e aggiornare il browser è l’unico modo per garantire che nessuna informazione proveniente dall’utente precedente sia trapelata all’utente successivo.

    (Ovviamente, come utente su un computer condiviso dovresti usare la modalità “navigazione privata”, chiudi la finestra del browser da solo, usa la funzione “cancella dati di navigazione”, ecc., Ma come sviluppatore non possiamo aspettarci che tutti siano sempre quello diligente)

    Basta che il tuo logout link cancelli la sessione e aggiorni la pagina. Nessun codice aggiuntivo necessario per il tuo negozio. Ogni volta che si desidera ripristinare completamente lo stato, l’aggiornamento di una pagina è un modo semplice e ripetibile per gestirlo.