Search code examples
javascriptreactjsreduxredux-toolkitredux-oidc

fire redux action from extraReducers


First, I know (or I think I've read) that you're never supposed to fire actions from reducers. In my situation, I'm using redux-oidc to handle authentication against my app. Once the user is logged in, redux-oidc fires a redux-oidc/USER_FOUND action and sets the user's profile in the state.oidc.user slice.

After login, I need to look up additional info about the user from my DB that isn't in the OIDC response. At the moment, I'm firing the fetchUserPrefs thunk from the redux-oidc.CallbackComponent.successCallback which works as expected.

My problem is when the user has an active session and opens a new browser, or manually refreshes the page and init's the app again, the callback isn't hit, so the additional user hydration doesn't happen. It seems like what I want to do is add an extraReducer that listens for the redux-oidc/USER_FOUND action and triggers the thunk, but this would be firing an action from a reducer.

Is there a better way to do this?

import {createAsyncThunk, createSlice} from '@reduxjs/toolkit';
import { RootState } from '../../app/store';
import {User} from "oidc-client";

export const fetchUserPrefs = createAsyncThunk('user/fetchUserPrefs', async (user: User, thunkAPI) => {
    // the call out to grab user prefs
    // this works as expected when dispatched from the CallbackComponent.successCallback
    return user;
})

function hydrateUserState(state: any, action: any) {
    // set all the state values from the action.payload
    // this works as expected
}

export interface UserState {
    loginId: string;
    firstName: string;
    lastName: string;
    email: string;
    photoUrl: string;
}

const initialState: UserState = {
    loginId: '',
    firstName: '',
    lastName: '',
    email: '',
    photoUrl: '',
};

export const userSlice = createSlice({
    name: 'user',
    initialState,
    reducers: {
    },
    extraReducers: (builder) => {
        builder
            .addCase('redux-oidc/USER_FOUND', fetchUserPrefs) // I want to do this, or something like it
            .addCase(fetchUserPrefs.fulfilled, hydrateUserState)
            .addDefaultCase((state, action) => {})
    }
});

export const selectUser = (state: RootState) => state.user;
export default userSlice.reducer;

Solution

  • You are correct that you cannot dispatch an action from a reducer. You want to listen for an action to be dispatched and dispatch another action in response. That is a job for middleware. Your middleware should look something like this:

    import { USER_FOUND } from 'redux-oidc';
    import { fetchUserPrefs } from "./slice";
    
    export const oicdMiddleware = (store) => (next) => (action) => {
      // possibly dispatch an additional action
      if ( action.type === USER_FOUND ) {
        store.dispatch(fetchUserPrefs);
      }
      // let the next middleware dispatch the 'USER_FOUND' action
      return next(action);
    };
    

    You can read the docs on custom middleware for more info.