如何在 Redux Toolkit 中订阅 React 组件外的状态?
How to subscribe to state outside React component in Redux Toolkit?
我有以下切片:
export const authenticationSlice = createSlice({
name: 'authentication',
initialState: {
isFirstTimeLoading: true,
signedInUser: null
},
reducers: {
signOut: (state) => {
state.signedInUser = null
},
setUserAfterSignIn: (state, action: PayloadAction<SignInResult>) => {
// some logic...
state.signedInUser = {...}
}
},
extraReducers: builder => {
// Can I subscribe to signedInUser changes here?
}
})
当 signedInUser
发生变化时(setUserAfterSignIn
和 signOut
),在 extraReducers
内,有什么方法可以订阅吗?
例如,每次调度 setUserAfterSignIn
操作时,我想在 axios 中添加一个拦截器,它使用用户的 accessToken 作为 Auth header。
我也可以从另一个切片订阅这个状态吗?如果不同切片中的某些状态取决于 signedInUser
?
编辑:这是登录用户的 thunk 和注销的 thunk
export const { signOut: signOutAction, setUserAfterSignIn: setUserAction } = authenticationSlice.actions
export const signInWithGoogleAccountThunk = createAsyncThunk('sign-in-with-google-account', async (staySignedIn: boolean, thunkAPI) => {
const state = thunkAPI.getState() as RootState
state.auth.signedInUser && await thunkAPI.dispatch(signOutThunk())
const googleAuthUser = await googleClient.signIn()
const signedInUser = await signInWithGoogleAccountServer({ idToken: googleAuthUser.getAuthResponse().id_token, staySignedIn })
thunkAPI.dispatch(setUserAction({ data: signedInUser.data, additionalData: { imageUrl: googleAuthUser.getBasicProfile().getImageUrl() } } as SignInResult))
})
export const signInWithLocalAccountThunk = createAsyncThunk('sign-in-with-local-account', async (dto: LocalSignInDto, thunkAPI) => {
const state = thunkAPI.getState() as RootState
state.auth.signedInUser && await thunkAPI.dispatch(signOutThunk())
const user = await signInWithLocalAccountServer(dto)
thunkAPI.dispatch(setUserAction({ data: user.data } as SignInResult))
})
export const signOutThunk = createAsyncThunk<void, void, { dispatch: AppDispatch }>('sign-out', async (_, thunkAPI) => {
localStorage.removeItem(POST_SESSION_DATA_KEY)
sessionStorage.removeItem(POST_SESSION_DATA_KEY)
const state = thunkAPI.getState() as RootState
const signedInUser = state.auth.signedInUser
if (signedInUser?.method === AccountSignInMethod.Google)
await googleClient.signOut()
if (signedInUser)
await Promise.race([signOutServer(), rejectAfter(10_000)])
.catch(error => console.error('Signing out of server was not successful', error))
.finally(() => thunkAPI.dispatch(signOutAction()))
})
Redux 实现了 flux architecture.
This structure allows us to reason easily about our application in a way that is reminiscent of functional reactive programming, or more specifically data-flow programming or flow-based programming, where data flows through the application in a single direction — there are no two-way bindings.
Reducer 不应该相互依赖,因为 redux 不能确保它们执行的特定顺序。您可以使用 解决此问题。您不能确定 extraReducers
在 setUserAfterSignIn
reducer 之后执行。
您的选择是:
将更新axios拦截器的代码放在setUserAfterSignIn
reducer中。
setUserAfterSignIn: (state, action: PayloadAction<SignInResult>) => {
// some logic...
state.signedInUser = {...}
// update axios
}
创建 axios 拦截器并将其传递给连接到商店的供应商。通过这种方式,您可以轻松替换令牌的提供方式。
const tokenSupplier = () => store.getState().signedInUser;
// ...
axios.interceptors.request.use(function (config) {
const token = tokenSupplier();
config.headers.Authorization = token;
return config;
});
提取两个reducer函数并确保它们的顺序。
function signInUser(state, action) {
state.signedInUser = {...}
}
function onUserSignedIn(state, action) {
// update axios interceptor
}
// ....
// ensure their order in the redux reducer.
setUserAfterSignIn: (state, action: PayloadAction<SignInResult>) => {
signInUser(state, action);
onUserSignedIn(state, action)
}
编辑
Given this architecture, What are my options if I have another slice that needs to react when signedInUser has changed?
我猜你不会喜欢这个答案。前段时间我也遇到过同样的问题。
另一个切片是store中的独立部分。您可以添加额外的 reducer 来监听来自其他切片的操作,但是您不能确定其他切片的 reducer 是否已经更新了状态。
假设您有一个切片 A
和一个缩减器 RA
以及一个带有缩减器 RB
的切片 B
。如果状态 B
依赖于 A
这意味着减速器 RB
应该在 A
改变时执行。
您可以 RA
调用 RB
,但这会引入对 RB
的依赖。如果 RA
可以分派一个像 { type: "stateAChanged", payload: stateA}
这样的动作,这样其他切片可以听那个动作,但 reducers 不能分派动作,那就太好了。您可以实现一个中间件,通过调度程序增强操作。例如
function augmentAction(store, action) {
action.dispatch = (a) => {
store.dispatch(a)
}
store.dispatch(action)
}
以便 reducer 可以分派操作。
setUserAfterSignIn: (state, action: PayloadAction<SignInResult>) => {
// some logic...
state.signedInUser = {...}
action.dispatch({type : "userSignedIn": payload: {...state}})
}
但这种方法不是标准方法,如果过度使用它,可能会引入循环,导致调度中出现无限循环。
有些使用不同的商店,而不是使用不同的切片,并使用商店的订阅连接它们。这是一个官方的AP,但如果你不够注意,它也会引入循环。
所以最后最简单的方法就是从 RA
调用 RB
。您可以通过反转它来稍微管理它们之间的依赖关系。例如
const onUserSignedIn = (token) => someOtherReducer(state, { type: "updateAxios", payload: token});
setUserAfterSignIn: (state, action: PayloadAction<SignInResult>) => {
// some logic...
state.signedInUser = {...}
onUserSignedIn(state.signedInUser.token)
}
现在您可以在测试中替换 onUserSignedIn
回调,或者用调用其他已注册回调的复合函数替换。
编辑
我目前正在开发一个中间件库来解决我们的问题。我在 Github and npm 上发布了我的图书馆的实际版本。这个想法是你描述状态和动作之间的依赖关系,应该在变化时分派。
stateChangeMiddleware
.whenStateChanges((state) => state.counter)
.thenDispatch({ type: "text", payload: "changed" });
是的,另一个切片可以侦听各种动作类型并将动作负载(或元数据)应用到它自己的状态对象。
但要小心,不要让镜像或派生状态分布在商店的各个部分。
export const signInThunk = createAsyncThunk('signIn', async (_: void, thunkAPI) => {
const authUser = await authClient.signIn()
return authUser
})
export const authenticationSlice = createSlice({
name: 'authentication',
initialState: {
isFirstTimeLoading: true,
signedInUser: null
},
reducers: {},
extraReducers: builder => {
builder.addCase(signInThunk.fulfilled, (state, action) => {
state.signedInUser = action.payload
})
}
})
export const anotherSlice = createSlice({
name: 'another',
initialState: {},
reducers: {},
extraReducers: builder => {
builder.addCase(signInThunk.fulfilled, (state, action) => {
// do something with the action
})
}
})
我有以下切片:
export const authenticationSlice = createSlice({
name: 'authentication',
initialState: {
isFirstTimeLoading: true,
signedInUser: null
},
reducers: {
signOut: (state) => {
state.signedInUser = null
},
setUserAfterSignIn: (state, action: PayloadAction<SignInResult>) => {
// some logic...
state.signedInUser = {...}
}
},
extraReducers: builder => {
// Can I subscribe to signedInUser changes here?
}
})
当 signedInUser
发生变化时(setUserAfterSignIn
和 signOut
),在 extraReducers
内,有什么方法可以订阅吗?
例如,每次调度 setUserAfterSignIn
操作时,我想在 axios 中添加一个拦截器,它使用用户的 accessToken 作为 Auth header。
我也可以从另一个切片订阅这个状态吗?如果不同切片中的某些状态取决于 signedInUser
?
编辑:这是登录用户的 thunk 和注销的 thunk
export const { signOut: signOutAction, setUserAfterSignIn: setUserAction } = authenticationSlice.actions
export const signInWithGoogleAccountThunk = createAsyncThunk('sign-in-with-google-account', async (staySignedIn: boolean, thunkAPI) => {
const state = thunkAPI.getState() as RootState
state.auth.signedInUser && await thunkAPI.dispatch(signOutThunk())
const googleAuthUser = await googleClient.signIn()
const signedInUser = await signInWithGoogleAccountServer({ idToken: googleAuthUser.getAuthResponse().id_token, staySignedIn })
thunkAPI.dispatch(setUserAction({ data: signedInUser.data, additionalData: { imageUrl: googleAuthUser.getBasicProfile().getImageUrl() } } as SignInResult))
})
export const signInWithLocalAccountThunk = createAsyncThunk('sign-in-with-local-account', async (dto: LocalSignInDto, thunkAPI) => {
const state = thunkAPI.getState() as RootState
state.auth.signedInUser && await thunkAPI.dispatch(signOutThunk())
const user = await signInWithLocalAccountServer(dto)
thunkAPI.dispatch(setUserAction({ data: user.data } as SignInResult))
})
export const signOutThunk = createAsyncThunk<void, void, { dispatch: AppDispatch }>('sign-out', async (_, thunkAPI) => {
localStorage.removeItem(POST_SESSION_DATA_KEY)
sessionStorage.removeItem(POST_SESSION_DATA_KEY)
const state = thunkAPI.getState() as RootState
const signedInUser = state.auth.signedInUser
if (signedInUser?.method === AccountSignInMethod.Google)
await googleClient.signOut()
if (signedInUser)
await Promise.race([signOutServer(), rejectAfter(10_000)])
.catch(error => console.error('Signing out of server was not successful', error))
.finally(() => thunkAPI.dispatch(signOutAction()))
})
Redux 实现了 flux architecture.
This structure allows us to reason easily about our application in a way that is reminiscent of functional reactive programming, or more specifically data-flow programming or flow-based programming, where data flows through the application in a single direction — there are no two-way bindings.
Reducer 不应该相互依赖,因为 redux 不能确保它们执行的特定顺序。您可以使用 extraReducers
在 setUserAfterSignIn
reducer 之后执行。
您的选择是:
将更新axios拦截器的代码放在
setUserAfterSignIn
reducer中。setUserAfterSignIn: (state, action: PayloadAction<SignInResult>) => { // some logic... state.signedInUser = {...} // update axios }
创建 axios 拦截器并将其传递给连接到商店的供应商。通过这种方式,您可以轻松替换令牌的提供方式。
const tokenSupplier = () => store.getState().signedInUser; // ... axios.interceptors.request.use(function (config) { const token = tokenSupplier(); config.headers.Authorization = token; return config; });
提取两个reducer函数并确保它们的顺序。
function signInUser(state, action) { state.signedInUser = {...} } function onUserSignedIn(state, action) { // update axios interceptor } // .... // ensure their order in the redux reducer. setUserAfterSignIn: (state, action: PayloadAction<SignInResult>) => { signInUser(state, action); onUserSignedIn(state, action) }
编辑
Given this architecture, What are my options if I have another slice that needs to react when signedInUser has changed?
我猜你不会喜欢这个答案。前段时间我也遇到过同样的问题。
另一个切片是store中的独立部分。您可以添加额外的 reducer 来监听来自其他切片的操作,但是您不能确定其他切片的 reducer 是否已经更新了状态。
假设您有一个切片 A
和一个缩减器 RA
以及一个带有缩减器 RB
的切片 B
。如果状态 B
依赖于 A
这意味着减速器 RB
应该在 A
改变时执行。
您可以 RA
调用 RB
,但这会引入对 RB
的依赖。如果 RA
可以分派一个像 { type: "stateAChanged", payload: stateA}
这样的动作,这样其他切片可以听那个动作,但 reducers 不能分派动作,那就太好了。您可以实现一个中间件,通过调度程序增强操作。例如
function augmentAction(store, action) {
action.dispatch = (a) => {
store.dispatch(a)
}
store.dispatch(action)
}
以便 reducer 可以分派操作。
setUserAfterSignIn: (state, action: PayloadAction<SignInResult>) => {
// some logic...
state.signedInUser = {...}
action.dispatch({type : "userSignedIn": payload: {...state}})
}
但这种方法不是标准方法,如果过度使用它,可能会引入循环,导致调度中出现无限循环。
有些使用不同的商店,而不是使用不同的切片,并使用商店的订阅连接它们。这是一个官方的AP,但如果你不够注意,它也会引入循环。
所以最后最简单的方法就是从 RA
调用 RB
。您可以通过反转它来稍微管理它们之间的依赖关系。例如
const onUserSignedIn = (token) => someOtherReducer(state, { type: "updateAxios", payload: token});
setUserAfterSignIn: (state, action: PayloadAction<SignInResult>) => {
// some logic...
state.signedInUser = {...}
onUserSignedIn(state.signedInUser.token)
}
现在您可以在测试中替换 onUserSignedIn
回调,或者用调用其他已注册回调的复合函数替换。
编辑
我目前正在开发一个中间件库来解决我们的问题。我在 Github and npm 上发布了我的图书馆的实际版本。这个想法是你描述状态和动作之间的依赖关系,应该在变化时分派。
stateChangeMiddleware
.whenStateChanges((state) => state.counter)
.thenDispatch({ type: "text", payload: "changed" });
是的,另一个切片可以侦听各种动作类型并将动作负载(或元数据)应用到它自己的状态对象。
但要小心,不要让镜像或派生状态分布在商店的各个部分。
export const signInThunk = createAsyncThunk('signIn', async (_: void, thunkAPI) => {
const authUser = await authClient.signIn()
return authUser
})
export const authenticationSlice = createSlice({
name: 'authentication',
initialState: {
isFirstTimeLoading: true,
signedInUser: null
},
reducers: {},
extraReducers: builder => {
builder.addCase(signInThunk.fulfilled, (state, action) => {
state.signedInUser = action.payload
})
}
})
export const anotherSlice = createSlice({
name: 'another',
initialState: {},
reducers: {},
extraReducers: builder => {
builder.addCase(signInThunk.fulfilled, (state, action) => {
// do something with the action
})
}
})