Как сбросить состояние магазина Redux?
Я использую Redux для управления состоянием.
Как мне вернуть магазин в исходное состояние?
Например, скажем, у меня есть две учетные записи пользователей (u1
а также u2
).
Представьте себе следующую последовательность событий:
пользователь
u1
входит в приложение и что-то делает, поэтому мы кешируем некоторые данные в магазине.пользователь
u1
выходит из системыпользователь
u2
входит в приложение без обновления браузера.
На этом этапе кэшированные данные будут связаны с u1
и я бы хотел его почистить.
Как я могу сбросить хранилище Redux в исходное состояние, когда первый пользователь выходит из системы?
36 ответов
Один из способов сделать это - написать корневой редуктор в вашем приложении.
Корневой редуктор обычно делегирует обработку действия редуктору, генерируемому combineReducers()
, Однако всякий раз, когда он получает USER_LOGOUT
действие, оно возвращает начальное состояние снова и снова.
Например, если ваш корневой редуктор выглядел так:
const rootReducer = combineReducers({
/* your app’s top-level reducers */
})
Вы можете переименовать его в appReducer
и написать новый rootReducer
делегировать ему:
const appReducer = combineReducers({
/* your app’s top-level reducers */
})
const rootReducer = (state, action) => {
return appReducer(state, action)
}
Теперь нам просто нужно учить новому rootReducer
вернуть исходное состояние после USER_LOGOUT
действие. Как мы знаем, редукторы должны возвращать исходное состояние при вызове с undefined
в качестве первого аргумента, независимо от действия. Давайте использовать этот факт для условного удаления накопленного state
когда мы передаем это appReducer
:
const rootReducer = (state, action) => {
if (action.type === 'USER_LOGOUT') {
state = undefined
}
return appReducer(state, action)
}
Теперь, когда бы USER_LOGOUT
пожары, все редукторы будут инициализированы заново. Они также могут вернуть что-то другое, чем они делали изначально, если они хотят, потому что они могут проверить action.type
также.
Повторюсь, полный новый код выглядит так:
const appReducer = combineReducers({
/* your app’s top-level reducers */
})
const rootReducer = (state, action) => {
if (action.type === 'USER_LOGOUT') {
state = undefined
}
return appReducer(state, action)
}
Обратите внимание, что я не изменяю здесь состояние, я просто переназначаю ссылку на локальную переменную с именем state
перед передачей его в другую функцию. Мутация объекта состояния будет нарушением принципов Redux.
В случае использования redux-persist вам также может понадобиться очистить хранилище. Redux-persist хранит копию вашего состояния в хранилище, и они будут загружены оттуда при обновлении.
Сначала необходимо импортировать соответствующий механизм хранения, а затем проанализировать состояние, прежде чем установить его в undefined
и очистите каждый ключ состояния хранилища.
const rootReducer = (state, action) => {
if (action.type === SIGNOUT_REQUEST) {
Object.keys(state).forEach(key => {
storage.removeItem(`persist:${key}`);
});
state = undefined;
}
return AppReducers(state, action);
};
Я хотел бы отметить, что принятый комментарий Дана Абрамова является правильным, за исключением того, что у нас возникла странная проблема при использовании пакета act-router-redux вместе с этим подходом. Нашим решением было не устанавливать состояние undefined
а точнее все равно использую текущий редуктор маршрутизации. Поэтому я бы предложил реализовать решение ниже, если вы используете этот пакет
const rootReducer = (state, action) => {
if (action.type === 'USER_LOGOUT') {
const { routing } = state
state = { routing }
}
return appReducer(state, action)
}
Определите действие:
const RESET_ACTION = {
type: "RESET"
}
Затем в каждом из ваших редукторов, если вы используете switch
или же if-else
для обработки нескольких действий через каждый редуктор. Я собираюсь взять дело для 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;
}
}
Таким образом, когда вы звоните RESET
действие, вы редуктор будет обновлять магазин с состоянием по умолчанию.
Теперь для выхода из системы вы можете выполнить следующее:
const logoutHandler = () => {
store.dispatch(RESET_ACTION)
// Also the custom logic like for the rest of the logout handler
}
Каждый раз, когда пользователь входит в систему, без обновления браузера. Магазин всегда будет по умолчанию.
store.dispatch(RESET_ACTION)
просто развивает идею. Скорее всего, у вас будет создатель действий для этой цели. Гораздо лучше будет то, что у вас есть LOGOUT_ACTION
,
Как только вы отправите это LOGOUT_ACTION
, Пользовательское промежуточное ПО может затем перехватить это действие либо с Redux-Saga, либо с Redux-Thunk. Однако в обоих случаях вы можете отправить другое действие "СБРОС". Таким образом, выход из магазина и сброс будут происходить синхронно, и ваш магазин будет готов для входа другого пользователя.
Просто упрощенный ответ на лучший ответ:
const rootReducer = combineReducers({
auth: authReducer,
...formReducers,
routing
});
export default (state, action) => (
action.type === 'USER_LOGOUT'
? rootReducer(undefined, action)
: rootReducer(state, action)
)
Используя Redux Toolkit и / или Typescript:
const appReducer = combineReducers({
/* your app’s top-level reducers */
});
const rootReducer = (
state: ReturnType<typeof appReducer>,
action: AnyAction
) => {
/* if you are using RTK, you can import your action and use it's type property instead of the literal definition of the action */
if (action.type === logout.type) {
return appReducer(undefined, { type: undefined });
}
return appReducer(state, action);
};
С точки зрения безопасности, наиболее безопасная вещь при выходе пользователя из системы - сбросить все постоянные состояния (например, файлы cookie, localStorage
, IndexedDB
, Web SQL
и т. д.) и выполнить жесткое обновление страницы, используя window.location.reload()
, Возможно, небрежный разработчик случайно или намеренно сохранил некоторые конфиденциальные данные на window
в DOM и т. д. Удаление всего постоянного состояния и обновление браузера - единственный способ гарантировать, что информация от предыдущего пользователя не будет передана следующему пользователю.
(Конечно, как пользователь на компьютере с общим доступом, вы должны использовать режим "частного просмотра", самостоятельно закрывать окно браузера, использовать функцию "очистки данных просмотра" и т. Д., Но как разработчик мы не можем ожидать, что все будут всегда прилежный)
const reducer = (state = initialState, { type, payload }) => {
switch (type) {
case RESET_STORE: {
state = initialState
}
break
}
return state
}
Вы также можете запустить действие, которое обрабатывается всеми или некоторыми редукторами, которые вы хотите сбросить в исходное хранилище. Одно действие может вызвать сброс к вашему состоянию в целом или просто к его части, которая кажется вам подходящей. Я считаю, что это самый простой и самый контролируемый способ сделать это.
С Redux if применили следующее решение, которое предполагает, что я установил initialState во всех моих редукторах (например, { user: { name, email }}). Во многих компонентах я проверяю эти вложенные свойства, поэтому с помощью этого исправления я предотвращаю сбои моих методов рендеринга в условиях связанных свойств (например, если state.user.email, который выдаст ошибку, не определен пользователем, если указано выше решения).
const appReducer = combineReducers({
tabs,
user
})
const initialState = appReducer({}, {})
const rootReducer = (state, action) => {
if (action.type === 'LOG_OUT') {
state = initialState
}
return appReducer(state, action)
}
Мой обходной путь при работе с машинописью, основанный на ответе Дэна (лишние вводы делают невозможным прохождение undefined
для редуктора в качестве первого аргумента, поэтому я кеширую начальное корневое состояние в константу):
// 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"})
}
}
ОБНОВЛЕНИЕ NGRX4
Если вы переходите на NGRX 4, вы, возможно, заметили в руководстве по миграции, что метод rootreducer для объединения ваших редукторов был заменен методом ActionReducerMap. Во-первых, этот новый способ действий может сделать сброс состояния вызовом. Это на самом деле просто, но способ сделать это изменился.
Это решение вдохновлено разделом API мета-редукторов документации NGRX4 Github.
Во-первых, предположим, что вы комбинируете ваши редукторы, как это, с помощью новой опции ActionGReducerMap в 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
}
Теперь допустим, что вы хотите сбросить состояние из 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 { }
`
И это в основном один из способов добиться того же эффекта с NGRX 4.
Просто очистите сеанс по ссылке на выход из системы и обновите страницу. Для вашего магазина не требуется дополнительный код. Каждый раз, когда вы хотите полностью сбросить состояние, обновление страницы - это простой и легко повторяемый способ справиться с этим.
Если вы используете redux-действия, вот быстрый обходной путь, используя HOF(функцию более высокого порядка) для handleActions
,
import { handleActions } from 'redux-actions';
export function handleActionsEx(reducer, initialState) {
const enhancedReducer = {
...reducer,
RESET: () => initialState
};
return handleActions(enhancedReducer, initialState);
}
А потом использовать handleActionsEx
вместо оригинального handleActions
обращаться с редукторами.
Ответ Дэна дает отличное представление об этой проблеме, но она не сработала для меня, потому что я использую redux-persist
,
При использовании с redux-persist
, просто мимоходом undefined
состояние не вызывало постоянное поведение, поэтому я знал, что должен был вручную удалить элемент из хранилища (например, React Native в моем случае AsyncStorage
).
await AsyncStorage.removeItem('persist:root');
или же
await persistor.flush(); // or await persistor.purge();
у меня тоже не получилось - они просто наорали на меня. (например, жалоба типа "Неожиданный ключ _persist...")
Затем я вдруг подумал, что все, что я хочу, это просто заставить каждый редуктор возвращать свое собственное исходное состояние, когда RESET
Тип действия встречается. Таким образом, упорство обрабатывается естественно. Очевидно, без вышеупомянутой функции полезности (handleActionsEx
), мой код не будет выглядеть сухим (хотя это всего лишь одна строка, т.е. RESET: () => initialState
), но я терпеть не могу, потому что я люблю метапрограммирование.
Объединяя подходы Дэна, Райана и Роба, чтобы учесть router
состояние и инициализация всего остального в дереве состояний, я закончил с этим:
const rootReducer = (state, action) => appReducer(action.type === LOGOUT ? {
...appReducer({}, {}),
router: state && state.router || {}
} : state, action);
Я создал действия, чтобы очистить государство. Поэтому, когда я отправляю создателя действий выхода из системы, я также отправляю действия для очистки состояния.
Действие записи пользователя
export const clearUserRecord = () => ({
type: CLEAR_USER_RECORD
});
Выйти из создателя действий
export const logoutUser = () => {
return dispatch => {
dispatch(requestLogout())
dispatch(receiveLogout())
localStorage.removeItem('auth_token')
dispatch({ type: 'CLEAR_USER_RECORD' })
}
};
редуктор
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
}
};
Я не уверен, что это оптимально?
Мое предположение, чтобы Redux не ссылался на ту же переменную исходного состояния:
// 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;
}
};
Я создал компонент, чтобы дать Redux возможность сброса состояния, вам просто нужно использовать этот компонент для улучшения вашего хранилища и отправки определенного action.type для запуска сброса. Мысль о реализации такая же, как и @Dan Abramov.
Следующее решение сработало для меня.
Я добавил функцию сброса состояния в мета редукторы. Ключ должен был использовать
return reducer(undefined, action);
установить все редукторы в исходное состояние. возврате undefined
вместо этого вызывал ошибки из-за того, что структура магазина была разрушена.
/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 {}
Принятый ответ помог мне решить мой случай. Однако я столкнулся со случаем, когда необходимо очистить не все состояние. Итак - я сделал это так:
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;
Надеюсь, это поможет кому-то еще:)
Быстрая и простая опция, которая работала для меня, использовала Redux-Reset. Который был прост и также имеет некоторые дополнительные параметры, для больших приложений.
Настройка в магазине создания
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)
Отправьте ваш "сброс" в функции выхода из системы
store.dispatch({
type: 'RESET'
})
Надеюсь это поможет
Просто расширение ответа Dan Abramov, иногда нам может потребоваться сохранить некоторые ключи от сброса.
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);
};
Я обнаружил, что принятый ответ хорошо работает для меня, но он вызвал ESLint no-param-reassign
ошибка - https://eslint.org/docs/rules/no-param-reassign
Вот как я справился с этим, убедившись, что создал копию состояния (что, на мой взгляд, нужно сделать в 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)
}
Но, может быть, создание копии состояния для простой передачи ее в другую функцию-редуктор, которая создает копию, которая немного сложнее? Это выглядит не так хорошо, но более конкретно:
export default (state, action) => {
return appReducer(action.type === "LOGOUT" ? undefined : state, action)
}
Следующее решение работает для меня.
Во-первых, при запуске нашего приложения состояние редуктора является новым и новым по умолчанию InitialState.
Мы должны добавить действие, которое вызывает начальную загрузку приложения для сохранения состояния по умолчанию.
При выходе из приложения мы можем просто переназначить состояние по умолчанию, и редуктор будет работать как новый.
Основной APP Контейнер
componentDidMount() {
this.props.persistReducerState();
}
Главный APP Редуктор
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);
};
При выходе из системы вызывается действие для сброса состояния
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
});
}
}
Надеюсь, что это решит вашу проблему!
Единственное, чего не делает решение в принятом ответе, - это очищать кеш для параметризованных селекторов. Если у вас есть такой селектор:
export const selectCounter1 = (state: State) => state.counter1;
export const selectCounter2 = (state: State) => state.counter2;
export const selectTotal = createSelector(
selectCounter1,
selectCounter2,
(counter1, counter2) => counter1 + counter2
);
Затем вам придется отпустить их при выходе из системы следующим образом:
selectTotal.release();
В противном случае мемоизированное значение для последнего вызова селектора и значения последних параметров все еще будут в памяти.
Примеры кода взяты из документации ngrx.
Для того чтобы сбросить состояние до его начального состояния, я написал следующий код:
const appReducers = (state, action) =>
combineReducers({ reducer1, reducer2, user })(
action.type === "LOGOUT" ? undefined : state,
action
);
Подход с Redux Toolkit:
export const createRootReducer = (history: History) => {
const rootReducerFn = combineReducers({
auth: authReducer,
users: usersReducer,
...allOtherReducers,
router: connectRouter(history),
});
return (state: Parameters<typeof rootReducerFn>[0], action: Parameters<typeof rootReducerFn>[1]) =>
rootReducerFn(action.type === appActions.reset.type ? undefined : state, action);
};
В дополнение к ответу Дана Абрамова, не следует ли нам явно установить действие как action = {type: '@@INIT'} рядом с state = undefined. При указанном выше типе действия каждый редуктор возвращает исходное состояние.
npm install redux-reset
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)
https://github.com/wwayne/redux-reset
На сервере у меня есть переменная: global.isSsr = true и в каждом редукторе у меня есть const: initialState Чтобы сбросить данные в хранилище, я делаю следующее с каждым редуктором:пример с 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;
}
}
}
наконец на маршрутизаторе сервера:
router.get('*', (req, res) => {
store.dispatch({type:'reset-all-blabla'});//<= unlike any action.type // i use Math.random()
// code ....render ssr here
});
просто отредактируйте файл, в котором объявлены редукторы
import { combineReducers } from 'redux';
import gets from '../';
const rootReducer = (state, action) => {
let asReset = action.type === 'RESET_STORE';
const reducers = combineReducers({
gets,
});
const transition = {
true() {
return reducers({}, action);
},
false() {
return reducers(state, action);
},
};
return transition[asReset] && transition[asReset]();
};
export default rootReducer;
Вы можете обнулить данные редукторов, добавив этот код в файл действия,
сначала импортируйте все типы:
import * as types from './types';
добавьте этот код для выхода из системы
for(let key of Object.values(types)) {
dispatch({ type: key, payload: [] });
}