reactjstypescriptredux-toolkit

TypeScript with RTK and builder cases


I followed this link for usage of TypeScript with redux toolkit but still couldn't figure out the issue when it comes to data type.

interface IDashboard {
    findGoals: { status: string; data: Goal[]; error: string | null; };
    findSchedule: { status: string; data: []; error: string | null; };
}

const initialState: IDashboard = {
    findGoals: { data: [], status: ApplicationConstants.IDLE, error: null },
    findSchedule: { data: [], status: ApplicationConstants.IDLE, error: null },
};

const dashboard = createSlice({
    name: "dashboard",
    initialState,
    reducers: {},
    extraReducers: (builder) => {
        builder
            .addCase(findGoals.pending, state => {
                state.findGoals = {data: [], status: ApplicationConstants.LOADING, error: null};
            })
            .addCase(findGoals.fulfilled, (state, action: PayloadAction<Goal[]>) => {
                state.findGoals = {data: action.payload, status: ApplicationConstants.SUCCEEDED, error: null};
            })
            .addCase(findGoals.rejected, (state, action) => {
                state.findGoals = {data: [], status: ApplicationConstants.FAILED, error: action.error};
            });

        builder
            .addCase(findSchedule.pending, state => {
                state.findSchedule = {data: [], status: ApplicationConstants.LOADING, error: null};
            })
            .addCase(findSchedule.fulfilled, (state, action) => {
                state.findSchedule = {data: action.payload, status: ApplicationConstants.SUCCEEDED, error: null};
            })
            .addCase(findSchedule.rejected, (state, action) => {
                state.findSchedule = {data: [], status: ApplicationConstants.FAILED, error: action.error};
            });
    }
});

ApplicationConstants

export class ApplicationConstants {
    static IDLE = "idle";
    static LOADING = "loading";
    static SUCCEEDED = "succeeded";
    static FAILED = "failed";
}

methods

export const findGoals = createAsyncThunk("dashboard/findGoals",

    async (client: ApolloClient<NormalizedCacheObject>) => {
        return new Promise((resolve, reject) => {
            const GET_GOALS = gql`
                query Goals {
                    goals {
                        id
                        title
                        description
                        percentage
                    }
                }
            `;

            client.query({ query: GET_GOALS })
                .then(response => resolve(response.data.goals))
                .catch(error => reject(error));
        });
    }
);

Goal

export interface Goal {
    id: string;
    title: string;
    description: string;
    percentage: string;
}

Store

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

export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;

Attempt

I used the type for action as action: PayloadAction<Goal[]> without any success for the first addCase.

Question:

Please advise on what type signature to use with the action for both fulfilled (action.payload) and rejected (action.error) cases.

Screenshot: See the red lines.

enter image description here


Edit

If I hover over data I see the following TypeScript warning

TS2740: Type RejectWithValue<unknown, unknown> is missing the following properties from type WritableDraft<Goal>[]: length, pop, push, concat, and 35 more.
dashboard. ts(7, 34): The expected type comes from property data which is declared here on type

enter image description here

and If I hover over error I see the following TypeScript warnings

TS2322: Type SerializedError is not assignable to type string
dashboard. ts(7, 48): The expected type comes from property error which is declared here on type

enter image description here


Solution

  • I was unable to reproduce the exact issue/error you describe here, but I think your implementation is not helping and might actually be working against you. The findGoals action logic can be simplified a bit using standard async/await instead of wrapping up the logic in an additional Promise chain.

    Example:

    export const findGoals = createAsyncThunk(
      "dashboard/findGoals",
      async (client: ApolloClient<NormalizedCacheObject>) => {
        const GET_GOALS = gql`
          query Goals {
            goals {
              id
              title
              description
              percentage
            }
          }
        `;
    
        const response = await client.query({ query: GET_GOALS });
        return response.data.goals as Goal[];
      }
    );
    

    The only additional change I needed to add was to cast the action.error to string from its default SerializedError type.

    const dashboard = createSlice({
      name: "dashboard",
      initialState,
      extraReducers: (builder) => {
        builder
          .addCase(findGoals.pending, (state) => {
            state.findGoals = {
              data: [],
              status: ApplicationConstants.LOADING,
              error: null,
            };
          })
          .addCase(findGoals.fulfilled, (state, action) => {
            state.findGoals = {
              data: action.payload,
              status: ApplicationConstants.SUCCEEDED,
              error: null,
            };
          })
          .addCase(findGoals.rejected, (state, action) => {
            state.findGoals = {
              data: [],
              status: ApplicationConstants.FAILED,
              error: action.error as string, // <--
            };
          });
    
        builder
          .addCase(findSchedule.pending, (state) => {
            state.findSchedule = {
              data: [],
              status: ApplicationConstants.LOADING,
              error: null,
            };
          })
          .addCase(findSchedule.fulfilled, (state, action) => {
            state.findSchedule = {
              data: action.payload,
              status: ApplicationConstants.SUCCEEDED,
              error: null,
            };
          })
          .addCase(findSchedule.rejected, (state, action) => {
            state.findSchedule = {
              data: [],
              status: ApplicationConstants.FAILED,
              error: action.error as string, // <--
            };
          });
      },
    });
    

    Also, FWIW, RTK utilizes Immer.js under the hood, so it allows you to write mutable state updates, so there's no need to create new findGoals and findSchedule object references, RTK takes care of this for you auto-magically.

    Example:

    const dashboard = createSlice({
      name: "dashboard",
      initialState,
      extraReducers: (builder) => {
        builder
          .addCase(findGoals.pending, (state) => {
            state.findGoals.status = ApplicationConstants.LOADING;
          })
          .addCase(findGoals.fulfilled, (state, action) => {
            state.findGoals.data = action.payload;
            state.findGoals.states = ApplicationConstants.SUCCEEDED;
          })
          .addCase(findGoals.rejected, (state, action) => {
            state.findGoals.status = ApplicationConstants.FAILED;
            state.findGoals.error = action.error as string;
          });
    
        builder
          .addCase(findSchedule.pending, (state) => {
            state.findSchedule.status = ApplicationConstants.LOADING;
          })
          .addCase(findSchedule.fulfilled, (state, action) => {
            state.findSchedule.data = action.payload;
            state.findSchedule.states = ApplicationConstants.SUCCEEDED;
          })
          .addCase(findSchedule.rejected, (state, action) => {
            state.findSchedule.status = ApplicationConstants.FAILED;
            state.findSchedule.error = action.error as string;
          });
      },
    });