Search code examples
typescriptreduxredux-toolkit

Type mismatch of "root reducer"


In my Redux store, I am initializing a persistedReducer which takes the redux-persist config and "rootReducer":

client/src/redux/store.ts:

import { configureStore } from '@reduxjs/toolkit';
import { persistStore, persistReducer } from 'redux-persist';
import storage from 'redux-persist/lib/storage';
import autoMergeLevel2 from 'redux-persist/es/stateReconciler/autoMergeLevel2';
import rootReducer from './rootReducer'

const persistConfig = {
  key: 'root',
  storage,
  stateReconciler: autoMergeLevel2,
}

const persistedReducer = persistReducer(persistConfig, rootReducer)

export const store = configureStore({
  reducer: persistedReducer
});

export const persistor = persistStore(store);
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;

The rootReducer itself is being declared and exported like so:

client/src/redux/rootReducer.ts:

import { combineReducers } from "@reduxjs/toolkit";
import { IUserState } from "../interfaces/user";
import userSlice from "./features/userSlice";

export interface RootState {
  user: IUserState;
}

const rootReducer = combineReducers({
  user: userSlice.reducer,
});

export default rootReducer;

The userSlice reducer we are importing and adding to the rootReducer looks like this:

client/src/redux/features/userSlice.ts:

import { createAsyncThunk, createSlice, PayloadAction } from "@reduxjs/toolkit";
import { IUserState } from "../../interfaces/user";
import { useSelector } from "react-redux";
import userApi from "../../apis/user";
import { RootState } from "../store";

const initialState: IUserState = {
  _id: "",
  ipAddress: "",
  createdAt: "",
  alignment: "",
  loading: false,
  error: "",
};

export const getUser = createAsyncThunk("user/getUser", async (_, thunkApi) => {
  try {
    const response = await userApi.getUser();
    return response;
  } catch (error) {
    throw thunkApi.rejectWithValue({ error: "user not initialized" });
  }
});

export const updateUser = createAsyncThunk<IUserState, any, { state: RootState }>(
  "user/updateUser",
  async (data: any, thunkApi) => {
    console.log("document", data);
    try {
      const user = useSelector((state: RootState) => state.user);
      const response = await userApi.updateUser(user, data);
      return response;
    } catch (error) {
      throw thunkApi.rejectWithValue({ error: "user not updated" });
    }
  }
);

const userSlice = createSlice({
  name: "userData",
  initialState,
  reducers: {
    getUser: (state, action: PayloadAction<IUserState>) => {
      state = Object.assign(state, action.payload);

      return state;
    },
    updateUser: (state, action: PayloadAction<IUserState>) => {
      state = Object.assign(state, action.payload);

      return state;
    },
  },
  extraReducers: (builder) => {
    builder.addCase(getUser.fulfilled, (state, action) => {
      state = Object.assign(state, action.payload);
      state.loading = false;
      state.error = "";
    });
    builder.addCase(getUser.pending, (state) => {
      state.loading = true;
      state.error = "";
    });
    builder.addCase(getUser.rejected, (state, action) => {
      state.loading = false;
      state.error = action.error.message || "user not initialized";
    });
  },
});

export const { getUser: getUserAction, updateUser: updateUserAction } =
  userSlice.actions;

export default userSlice;

The user state interface is imported from client/src/interfaces/user.ts, and looks like this:

export type IUserState = {
  _id: string
  ipAddress: string
  createdAt: string
  alignment: string
  loading: boolean
  error: string
}

The error I'm encountering in my client output states:

Argument of type 'Reducer<{ user: IUserState; }, UnknownAction, Partial<{ user: IUserState | undefined; }>>' is not assignable to parameter of type 'Reducer<unknown, UnknownAction, unknown>'.
  Types of parameters 'state' and 'state' are incompatible.
    Type 'unknown' is not assignable to type '{ user: IUserState; } | Partial<{ user: IUserState | undefined; }> | undefined'

There is a separate error from the Typescript compiler in my updateUser asyncThunk which says Property 'user' does not exist on type 'PersistPartial'. I am not certain if this is related to the terminal error that's complaining about a type mismatch with the Reducer. Casting rootReducer as any obviously makes the error go away, but I'd like to avoid that, if possible. It seems like this may be an issue with the way I am defining the RootState interface.


Solution

  • I think the issue is in your updateUser Thunk, at least a couple issues.

    1. There's no need really to parameterize createAsyncThunk as it should already infer the correct types based on the store and the payload creator argument.
    2. updateUser isn't a React component or custom React hook, so it can't use the useSelector hook. Use thunkApi.getState to access the current state value.
    3. rejectWithValue should be returned, not thrown, from the Thunk.
    export const updateUser = createAsyncThunk<
      IUserState,
      any,
      {
        state: RootState,
        rejectValue: {
          error: string;
        }
      }
    >("user/updateUser", (data: any, thunkApi) => {
      try {
        const user = thunkApi.getState().user;
        return userApi.updateUser(user, data);
      } catch (error) {
        return thunkApi.rejectWithValue({ error: "user not updated" });
      }
    });
    

    or

    export const updateUser = createAsyncThunk(
      "user/updateUser",
      (data: any, thunkApi) => {
        try {
          const user = (thunkApi.getState() as RootState).user;
          return userApi.updateUser(user, data);
        } catch (error) {
          return thunkApi.rejectWithValue({ error: "user not updated" });
        }
      }
    );
    

    Additionally you have issues in the userSlice with the state updates. You should only ever mutate the state or return an entirely new state reference value, and you should never reassign the state. state = Object.assign(state, payload) may technically works since the reference is the same, but you should not make it a habit of doing state = ... in your RTK reducers.

    const userSlice = createSlice({
      name: "userData",
      initialState,
      reducers: {
        getUser: (state, action: PayloadAction<IUserState>) => {
          // Just mutate state
          Object.assign(state, action.payload);
    
          // or return new state value
          // return action.payload;
        },
        updateUser: (state, action: PayloadAction<IUserState>) => {
          // Just mutate state
          Object.assign(state, action.payload);
    
          // or return new state value
          // return action.payload;
        },
      },
      extraReducers: (builder) => {
        builder.addCase(getUser.fulfilled, (state, action) => {
          Object.assign(state, action.payload);
          state.loading = false;
          state.error = "";
        });
        builder.addCase(getUser.pending, (state) => {
          state.loading = true;
          state.error = "";
        });
        builder.addCase(getUser.rejected, (state, action) => {
          state.loading = false;
          state.error = action.payload || "user not initialized";
        });
      },
    });