如何在 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 发生变化时(setUserAfterSignInsignOut),在 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 不能确保它们执行的特定顺序。您可以使用 解决此问题。您不能确定 extraReducerssetUserAfterSignIn reducer 之后执行。

您的选择是:

  1. 将更新axios拦截器的代码放在setUserAfterSignInreducer中。

     setUserAfterSignIn: (state, action: PayloadAction<SignInResult>) => {
         // some logic...
         state.signedInUser = {...}
         // update axios
     }
    
  2. 创建 axios 拦截器并将其传递给连接到商店的供应商。通过这种方式,您可以轻松替换令牌的提供方式。

     const tokenSupplier = () => store.getState().signedInUser;
    
     // ...
    
     axios.interceptors.request.use(function (config) {
         const token = tokenSupplier();
         config.headers.Authorization =  token;
    
         return config;
     });
    
  3. 提取两个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
      })
    }
})