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;
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.