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:
L'utilisateur u1
se connecte à l'application et fait quelque chose. Nous mettons donc certaines données en cache dans le magasin.
L'utilisateur u1
se déconnecte.
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?
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);
};
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)
}
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.
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.
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)
}
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.
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)
)
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);
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.
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 :)
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?
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.
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);
};
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
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.
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 {}
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.
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)
}
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
});
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)
}
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"})
}
}
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
);
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)
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;
}
};
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!