web-dev-qa-db-fra.com

Comment réinitialiser l'état d'un magasin Redux?

J'utilise Redux pour la gestion des états.
Comment réinitialiser le magasin à son état initial?

Par exemple, disons que j’ai deux comptes d’utilisateur (u1 et u2).
Imaginez la séquence d'événements suivante:

  1. L'utilisateur u1 se connecte à l'application et fait quelque chose. Nous mettons donc certaines données en cache dans le magasin.

  2. L'utilisateur u1 se déconnecte.

  3. L'utilisateur u2 se connecte à l'application sans actualiser le navigateur.

À ce stade, les données en cache seront associées à u1 et je voudrais les nettoyer.

Comment puis-je réinitialiser le magasin Redux à son état initial lorsque le premier utilisateur se déconnecte?

298
xyz

Une façon de le faire serait d'écrire un réducteur de racine dans votre application.

Le réducteur racine devrait normalement déléguer le traitement de l'action au réducteur généré par combineReducers(). Cependant, chaque fois qu'il reçoit l'action USER_LOGOUT, il renvoie à nouveau l'état initial.

Par exemple, si votre réducteur de racine ressemblait à ceci:

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

Vous pouvez le renommer en appReducer et écrire un nouveau rootReducer lui déléguant:

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

const rootReducer = (state, action) => {
  return appReducer(state, action)
}

Nous devons maintenant apprendre à la nouvelle rootReducer à renvoyer l'état initial après l'action USER_LOGOUT. Comme nous le savons, les réducteurs sont supposés renvoyer l’état initial lorsqu’ils sont appelés avec undefined comme premier argument, quelle que soit l’action. Utilisons ce fait pour éliminer de manière conditionnelle la state accumulée au fur et à mesure que nous la transmettons à appReducer:

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

  return appReducer(state, action)
}

Maintenant, chaque fois que USER_LOGOUT est déclenché, tous les réducteurs seront initialisés à nouveau. Ils peuvent également renvoyer quelque chose de différent de ce qu'ils ont fait initialement s'ils le souhaitent, car ils peuvent également vérifier action.type.

Pour répéter, le nouveau code complet ressemble à ceci:

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

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

  return appReducer(state, action)
}

Notez que je ne mute pas l’état ici, je ne fais que réaffecter la référence d’une variable locale appelée state avant de la transmettre à une autre fonction. La mutation d'un objet d'état constituerait une violation des principes Redux.

Si vous utilisez redux-persist , vous devrez peut-être également nettoyer votre stockage. Redux-persist conserve une copie de votre état dans un moteur de stockage, qui sera chargé à partir de ce dernier lors de l'actualisation.

Tout d'abord, vous devez importer le moteur de stockage approprié , puis, analyser l'état avant de le définir sur undefined et nettoyer chaque clé d'état de stockage.

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

Je voudrais souligner que le commentaire accepté par Dan Abramov est correct, sauf que nous avons rencontré un problème étrange lorsque nous utilisons le paquet react-router-redux avec cette approche. Notre solution était de ne pas définir l'état sur undefined, mais de continuer à utiliser le réducteur de routage actuel. Je suggère donc de mettre en œuvre la solution ci-dessous si vous utilisez ce package.

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

Définir une action:

const RESET_ACTION = {
  type: "RESET"
}

Puis, dans chacun de vos réducteurs, en supposant que vous utilisez switch ou if-else pour gérer plusieurs actions dans chaque réducteur. Je vais prendre l'affaire pour une 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; 
  }
}

De cette manière, chaque fois que vous appelez l'action RESET, votre réducteur mettra à jour le magasin avec l'état par défaut.

Maintenant, pour vous déconnecter, vous pouvez gérer ce qui suit:

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

Chaque fois qu'un utilisateur se connecte, sans actualisation du navigateur. Store sera toujours au défaut.

store.dispatch(RESET_ACTION) ne fait qu'élaborer l'idée. Vous aurez probablement un créateur d’action à cette fin. Un meilleur moyen sera d'avoir un LOGOUT_ACTION

Une fois que vous expédiez ce LOGOUT_ACTION. Un middleware personnalisé peut alors intercepter cette action, avec Redux-Saga ou Redux-Thunk. Cependant, dans les deux cas, vous pouvez envoyer une autre action 'RESET'. De cette façon, la déconnexion et la réinitialisation du magasin se produiront de manière synchrone et votre magasin sera prêt pour un autre utilisateur.

15
nirbhaygp
 const reducer = (state = initialState, { type, payload }) => {

   switch (type) {
      case RESET_STORE: {
        state = initialState
      }
        break
   }

   return state
 }

Vous pouvez également déclencher une action gérée par tous ou par certains réducteurs que vous souhaitez rétablir dans le magasin initial. Une action peut déclencher une réinitialisation de votre état entier ou simplement une partie de celle-ci qui vous semble appropriée. Je crois que c’est le moyen le plus simple et le plus contrôlable de procéder.

9
Daniel petrov

Avec Redux, si vous avez appliqué la solution suivante, qui suppose que vous avez défini un état initial dans tous mes réducteurs (par exemple, {utilisateur: {nom, email}}). Dans de nombreux composants, je vérifie ces propriétés imbriquées. Ainsi, avec ce correctif, j'empêche que mes méthodes de rendu soient interrompues dans des conditions de propriété couplées (par exemple, si state.user.email, ce qui jettera une erreur utilisateur n'est pas défini si les solutions mentionnées plus haut).

const appReducer = combineReducers({
  tabs,
  user
})

const initialState = appReducer({}, {})

const rootReducer = (state, action) => {
  if (action.type === 'LOG_OUT') {
    state = initialState
  }

  return appReducer(state, action)
}
8
Rob Moorman

MISE À JOUR NGRX4

Si vous migrez vers NGRX 4, vous avez peut-être remarqué, à partir du guide de migration que la méthode rootreducer permettant de combiner vos réducteurs a été remplacée par la méthode ActionReducerMap. Au début, cette nouvelle façon de faire pourrait faire de la réinitialisation un défi. C'est en fait simple, mais la façon de procéder a changé. 

Cette solution est inspirée de la section API méta-réducteurs de la documentation NGRX4 Github.

Tout d’abord, disons que vous combinez vos réducteurs de cette manière à l’aide de la nouvelle option ActionReducerMap de NGRX:

//index.reducer.ts
export const reducers: ActionReducerMap<State> = {
    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
}

Maintenant, disons que vous voulez réinitialiser l’état à partir de app.module `

//app.module.ts
import { IndexReducer } from './index.reducer';
import { StoreModule, ActionReducer, MetaReducer } from '@ngrx/store';
...
export function debug(reducer: ActionReducer<any>): ActionReducer<any> {
    return function(state, action) {

      switch (action.type) {
          case fromAuth.LOGOUT:
            console.log("logout action");
            state = undefined;
      }

      return reducer(state, action);
    }
  }

  export const metaReducers: MetaReducer<any>[] = [debug];

  @NgModule({
    imports: [
        ...
        StoreModule.forRoot(reducers, { metaReducers}),
        ...
    ]
})

export class AppModule { }

`

Et c’est essentiellement une façon de parvenir au même résultat avec NGRX 4.

4
Tyler Brown

Juste une réponse simplifiée à la meilleure réponse:

const rootReducer = combineReducers({
    auth: authReducer,
    ...formReducers,
    routing
});


export default (state, action) => (
    action.type === 'USER_LOGOUT'
        ? rootReducer(undefined, action)
        : rootReducer(state, action)
)
4
Matt Carlotta

En combinant les approches de Dan, Ryan et Rob pour expliquer le maintien de l'état router et l'initialisation de tout le reste dans l'arborescence d'état, j'ai obtenu ceci:

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

J'ai créé un composant pour donner à Redux la possibilité de réinitialiser l'état. Il vous suffit d'utiliser ce composant pour améliorer votre magasin et envoyer un action.type spécifique pour déclencher la réinitialisation. La pensée de la mise en œuvre est la même que celle de @Dan Abramov.

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

3
wwayne

La réponse acceptée m'a aidé à résoudre mon cas. Cependant, j’ai rencontré des cas où l’ensemble de l’État n’avait pas dû être effacé. Donc - je l'ai fait de cette façon:

const combinedReducer = combineReducers({
    // my reducers 
});

const rootReducer = (state, action) => {
    if (action.type === RESET_REDUX_STATE) {
        // clear everything but keep the stuff we want to be preserved ..
        delete state.something;
        delete state.anotherThing;
    }
    return combinedReducer(state, action);
}

export default rootReducer;

J'espère que ceci aide quelqu'un d'autre :) 

2
pesho hristov

J'ai créé des actions pour effacer l'état. Ainsi, lorsque je dépêche un créateur d’action de déconnexion, j’envoie également des actions pour effacer l’état.

Action d'enregistrement de l'utilisateur

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

Créateur d'action de déconnexion

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

Réducteur

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

Je ne suis pas sûr si c'est optimal?

2
Navinesh Chand

Si vous utilisez redux-actions , voici une solution rapide à l’aide d’un HOF (fonction d’ordre supérieur) pour handleActions.

import { handleActions } from 'redux-actions';

export function handleActionsEx(reducer, initialState) {
  const enhancedReducer = {
    ...reducer,
    RESET: () => initialState
  };
  return handleActions(enhancedReducer, initialState);
}

Et ensuite, utilisez handleActionsEx au lieu de l'original handleActions pour gérer les réducteurs.

La réponse de Dan donne une excellente idée de ce problème, mais cela n'a pas bien fonctionné pour moi, car j'utilise redux-persist.
Utilisé avec redux-persist, le simple fait de passer de l'état undefined ne provoquait pas de comportement persistant, donc je savais que je devais supprimer manuellement l'élément de la mémoire (React Native dans mon cas, donc AsyncStorage).

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

ou 

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

ça n'a pas marché pour moi non plus - ils m'ont juste crié dessus. (par exemple, se plaindre comme "clé inattendue _persiste ...")

Puis, j'ai tout à coup réfléchi. Tout ce que je veux, c’est que chaque réducteur retourne son propre état initial lorsque le type d’action RESET est rencontré. De cette façon, la persistance est gérée naturellement. Évidemment, sans la fonction d’utilitaire ci-dessus (handleActionsEx), mon code ne ressemblera pas à DRY (même s’il s’agit simplement d’une ligne, c’est-à-dire RESET: () => initialState), mais je ne pouvais pas le supporter car j’aime les métaprogrammes.

1
elquimista

Juste une extension de @ dan-abramov answer, nous avons parfois besoin de garder certaines clés non réinitialisées.

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);
};
1
gsaandy

Une option rapide et facile qui a fonctionné pour moi était d’utiliser redux-reset . Ce qui était simple et a également quelques options avancées, pour les applications plus grandes.

Installation dans le magasin de création

import reduxReset from 'redux-reset'
...
const enHanceCreateStore = compose(
applyMiddleware(...),
reduxReset()  // Will use 'RESET' as default action.type to trigger reset
)(createStore)
const store = enHanceCreateStore(reducers)

Envoyez votre 'reset' dans votre fonction de déconnexion

store.dispatch({
type: 'RESET'
})

J'espère que cela t'aides

1
Munei Nengwenani

pourquoi n'utilisez-vous pas simplement 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;
}

Remarque: assurez-vous que la valeur par défaut de l'action est définie sur {} et que tout va bien car vous ne souhaitez pas rencontrer d'erreur en cochant action.type dans l'instruction switch.

0
Fareed Alnamrouti

La solution suivante a fonctionné pour moi. 

J'ai ajouté la fonction de réinitialisation d'état aux méta-réducteurs. La clé était d'utiliser

return reducer(undefined, action);

mettre tous les réducteurs à l'état initial. Retourner undefined à la place était la cause d'erreurs car la structure du magasin avait été détruite.

/reducers/index.ts

export function resetState(reducer: ActionReducer<State>): ActionReducer<State> {
  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<State>[] = [ resetState ];

app.module.ts

import { StoreModule } from '@ngrx/store';
import { metaReducers, reducers } from './reducers';

@NgModule({
  imports: [
    StoreModule.forRoot(reducers, { metaReducers })
  ]
})
export class AppModule {}
0
Pan Piotr

En plus de la réponse de Dan Abramov, ne devrions-nous pas explicitement définir l'action comme action = {type: '@@ INIT'} aux côtés de state = undefined. Avec le type d'action ci-dessus, chaque réducteur renvoie l'état initial.

0
rupav jain

J'ai trouvé que la réponse acceptée fonctionnait bien pour moi, mais elle a déclenché l'erreur ESLint no-param-reassign - https://eslint.org/docs/rules/no-param-reassign

Voici comment je l'ai géré à la place, en m'assurant de créer une copie de l'état (ce qui, selon ma compréhension, est la chose à faire de 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)
}

Mais peut-être que créer une copie de l'état pour le passer simplement à une autre fonction de réduction qui en crée une copie est un peu trop compliqué? Cela ne se lit pas aussi bien, mais est plus pertinent:

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

dans le serveur, j'ai une variable est:global.isSsr = true et dans chaque réducteur, j'ai un const est:initialState Pour réinitialiser Avec les données du magasin, je fais ce qui suit avec chaque réducteur: exemple avec 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;
        }
    }
}

enfin sur le routeur du serveur:

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

Cette approche est très juste: détruisez tout état spécifique "NOM" pour ignorer et garder les autres.

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

Ma solution de contournement lorsque je travaille avec TypeScript, construite à partir de la réponse de Dan (les typages redux empêchent de passer undefined à réducteur en premier argument, je mets donc en cache l'état initial de la racine):

// store

export const store: Store<IStoreState> = createStore(
  rootReducer,
  storeEnhacer,
)

export const initialRootState = {
  ...store.getState(),
}

// root reducer

const appReducer = combineReducers<IStoreState>(reducers)

export const rootReducer = (state: IStoreState, action: IAction<any>) => {
  if (action.type === "USER_LOGOUT") {
    return appReducer(initialRootState, action)
  }

  return appReducer(state, action)
}


// auth service

class Auth {
  ...

  logout() {
    store.dispatch({type: "USER_LOGOUT"})
  }
}
0
Jacka

Pour que je puisse rétablir l'état initial, j'ai écrit le code suivant:

const appReducers = (state, action) =>
   combineReducers({ reducer1, reducer2, user })(
     action.type === "LOGOUT" ? undefined : state,
     action
);
0
JDev

Du point de vue de la sécurité, la chose la plus sûre lorsque vous déconnectez un utilisateur est de réinitialiser tous les états persistants (cookies.x., localStorage, IndexedDB, Web SQL, etc.) et d’actualiser la page en utilisant window.location.reload(). Il est possible qu'un développeur bâclé stocke accidentellement ou intentionnellement des données sensibles dans window, dans le DOM, etc. Supprimer tout état persistant et actualiser le navigateur est le seul moyen de garantir qu'aucune information provenant de l'utilisateur précédent ne soit divulguée à l'utilisateur suivant.

(Bien sûr, en tant qu'utilisateur sur un ordinateur partagé, vous devez utiliser le mode "navigation privée", fermer vous-même la fenêtre du navigateur, utiliser la fonction "effacer les données de navigation", etc., mais en tant que développeur, nous ne pouvons pas nous attendre à ce que tout le monde soit toujours que diligent)

0
tlrobinson

Mon intention de garder Redux de faire référence à la même variable de l'état initial:

// write the default state as a function
const defaultOptionsState = () => ({
  option1: '',
  option2: 42,
});

const initialState = {
  options: defaultOptionsState() // invoke it in your initial state
};

export default (state = initialState, action) => {

  switch (action.type) {

    case RESET_OPTIONS:
    return {
      ...state,
      options: defaultOptionsState() // invoke the default function to reset this part of the state
    };

    default:
    return state;
  }
};
0
Hani

La solution suivante fonctionne pour moi.

Tout d’abord surinitiationde notre application, l’état du réducteur estfreshetnewavec la valeur par défautInitialState.

Nous devons ajouter une action qui appelle la charge initiale de l'APP pour persisterdefault state.

Lors de la déconnexion de l'application, nous pouvons simplementreAssignledefault stateet réducteur fonctionneront commenew.

Conteneur principal APP

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

Main APP Réducteur

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

Action d'appel à la déconnexion pour réinitialiser l'état

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

J'espère que ceci résoudra votre problème!

0
Mukundhan