What is the best way to format CreateAsyncThunk response data to ensure it is stored and managed correctly in the state

While working on a project, I encountered a challenge regarding the Calculator. The task given to me was to divide the project into modules that could be easily replaced if needed. Consider the API as an example. The API code should be written in a way that makes it clear how it can be swapped with another one.

So, I started typing functions responsible for API requests. If I made any mistakes, I would appreciate it if you could point them out.

//./api/types.ts

export type HistoryItem = { //This defines the structure of an item for request, not response
  expression: string;
  result: string;
};

export type CalculationRequest<T> = (expression: string) => T[];
export type DeleteRequest = (id: string) => void;
export type GetAllRequest<T> = () => T[];

The purpose of creating generics was to allow the API to handle different types of input and output data. However, before passing this data to the state, it needs to be strongly typed to match the state's type.

Next, I created a useApi hook that checks the .env file to select the required API and provides fetch functions to the slice.

import { defaultApi } from "@/api";
import {
  CalculationRequest,
  DeleteRequest,
  GetAllRequest,
  HistoryItem,
} from "@/api/types";

export type ApiMethods = {
  calculateExpression: CalculationRequest<HistoryItem>;
  deleteHistoryItem: DeleteRequest;
  fetchHistory: GetAllRequest<HistoryItem>;
};

type UseApiHook = () => ApiMethods;

export const useApi: UseApiHook = () => {
  if (process.env.NEXT_PUBLIC_REACT_APP_API === "default") {
    return defaultApi;
  } else {
    throw new Error("API was not found!");
  }
};

In the slice, I encapsulated the fetch functions using createAsyncThunk and defined their functionality.

const fetchHistory = createAsyncThunk(
  "history/get",
  api.fetchHistory
);
const deleteHistoryItem = createAsyncThunk(
  "history/delete",
  api.deleteHistoryItem
);
const calculateExpression = createAsyncThunk(
  "calculator/get",
  api.calculateExpression
);

const maxNumberOfHistoryItems = 10;

const initialState: CalculatorHistoryState = {
  history: [],
  inputValue: "0",
  fetchError: null,
  status: "idle",
};

const calculatorHistorySlice = createSlice({
  name: "history",
  initialState,
  reducers: {
    addItemToState(state, action) {
      const updatedHistory = [action.payload, ...state.history];
      if (updatedHistory.length < maxNumberOfHistoryItems) {
        return { ...state, history: updatedHistory };
      }
      return {
        ...state,
        history: updatedHistory.slice(0, maxNumberOfHistoryItems),
      };
    },

    removeFromState(state, action) {
      const filteredHistory = state.history.filter((item) => {
        return item._id != action.payload;
      });
      return { ...state, history: filteredHistory };
    },

    setItemToInput(state, action) {
      return { ...state, inputValue: action.payload };
    },
  },
  extraReducers(builder) {
    builder.addCase(HYDRATE, (state, _action) => {
      return {
        ...state,
        // ...action.payload.subject,
      };
    });

    builder.addCase(fetchHistory.fulfilled, (state, { payload }) => {
      state.history = [...payload];
      state.status = "idle";
    });

    builder.addCase(calculateExpression.fulfilled, (state, _action) => {
      return state;
    });

    builder.addCase(deleteHistoryItem.fulfilled, (state, action) => {
      state.history.filter((item) => item._id != action.payload);
    });

    builder.addCase(deleteHistoryItem.rejected, (state, action) => {
      console.log(action);
      return state;
    });
  },
});

My question is about defining the data type for the state coming from the createAsyncThunk request. For instance, when swapping the API, the returned data's type may change. But before entering the state, the data must follow a certain format to prevent future errors and maintain clarity.

export interface StateItem {
  expression: string;
  result: string;
  _id: string;
  __v?: string;
}

At what stage in the code should I specify the data typing for the state retrieved from the API?

Answer №1

When considering whether to use the HistoryItem type or the StateItem type, which is an extension of HistoryItem with an additional _id, the key question arises.

Determining the appropriate type depends on how a variable is utilized and the TypeScript errors that are encountered.

After pasting your code into the TypeScript playground and simulating a api variable of type ApiMethods, a critical error was detected:

    builder.addCase(fetchHistory.fulfilled, (state, { payload }) => {
--->  state.history = [...payload];
      state.status = "idle";
    });

Type 'HistoryItem[]' is not assignable to type 'WritableDraft[]'.

Property '_id' is missing in type 'HistoryItem' but required in type 'WritableDraft'.

The presence of _id throughout your reducer suggests that state.history should be of type

StateItem[]</code instead of <code>HistoryItem[]
. This also implies that fetchHistory thunk should operate with the StateItem[] type. Therefore, if the assumption is incorrect, adjustments will have to be made accordingly.

To rectify this issue, the type of ApiMethods needs to be updated as follows:

export type ApiMethods = {
  calculateExpression: CalculationRequest<StateItem>;
  deleteHistoryItem: DeleteRequest;
  fetchHistory: GetAllRequest<StateItem>;
};

Alternatively, if you want to ensure that any generic API always includes an _id:

interface ApiAdded {
    _id: string;
    __v?: string;
}

export type CalculationRequest<T> = (expression: string) => (T & ApiAdded)[];
export type DeleteRequest = (id: string) => void;
export type GetAllRequest<T> = () => (T & ApiAdded)[];

export type ApiMethods = {
    calculateExpression: CalculationRequest<HistoryItem>;
    deleteHistoryItem: DeleteRequest;
    fetchHistory: GetAllRequest<HistoryItem>;
};

In addition, there seems to be an issue in your deleteHistoryItem.fulfilled case reducer where the action does not possess a payload. If indeed DeleteRequest returns void, you must extract the id of the deleted item from the arguments rather than relying on the payload.

builder.addCase(deleteHistoryItem.fulfilled, (state, action) => {
    state.history.filter((item) => item._id != action.meta.arg);
});

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Ensure that at least one of two props is mandatory in a functional component while using typescript

Consider a scenario where we have a functional component: // my-component.tsx interface Props { propA?: string; propB?: number; } const MyComponent = ({propA, propB}: Props) => { return <div>Hello world</div> } Now, let's incorp ...

Creating a see-through effect in Three.js with React Fiber

I'm currently working with react fiber and struggling to make the background of my child scene transparent. Below is my root component containing the main Canvas element: export const Splash = () => { const { intensity, distance, colo ...

Adding Relative URLs Automatically to .angular-cli.json is a simple process that can be easily

Is there a way to automatically have Angular-Cli (Angular-4) append URL's to Styles or Scripts when adding external libraries with npm install --save into .angular-cli.json? Currently, we have to manually search through the node_modules folder to fin ...

Ensure that the TypeScript build process includes the addition of the .js extension

When importing a file using import * as test from './dir/file', I noticed that the .js extension is missing in the build output. I prefer the output file to be like this: import * as test from './dir/file.js' The issue at hand is that ...

What is the best way to choose a slug page using the useRouter in Next

Is there a way to include all my posts in this array so I can use them for conditional styling in the header component? Currently, the header is white on most pages, but dark on the homepage, 404 page, and project pages. However, I am experiencing issues ...

What is the best method for displaying file size in a user-friendly format using Angular 6?

Imagine having this snippet in an HTML file: *ngFor="let cell of dateFormat(row)">{{cell | filesize}}</td>, where the dateFormat function looks like this: dateFormat(row:string){ var today = new Date(row[4]); let latest_date = this.date ...

How to pass data/props to a dynamic page in NextJS?

Currently, I am facing a challenge in my NextJS project where I am struggling to pass data into dynamically generated pages. In this application, I fetch data from an Amazon S3 bucket and then map it. The fetching process works flawlessly, generating a se ...

Angular Nested Interface is a concept that involves defining an

Looking for guidance on creating a nested interface for JSON data like this: Any help is appreciated. JSON Structure "toto": { "toto1": [], "toto2": [], "toto3": [], } Interface Definition export interface Itot ...

The initial call to React's useSelector may result in returning undefined, but subsequent calls function properly

For my mini eCommerce app built with the MERN stack, I am implementing a feature where each seller can only edit or delete their own products. To achieve this, I fetch products based on the seller's id retrieved from the Redux state of the user. With ...

Declaring named exports dynamically in TypeScript using a d.ts file

I have developed a collection of VueJs components in TypeScript and I want to be able to use them globally in my Vue instance using Vue.use(library), or individually through named imports in Vue components. While everything functions correctly, I am facin ...

Angular - Sharing data between components with response value

I am currently in the process of restructuring my project, focusing on establishing communication between unrelated components while also waiting for a return value from a function call. Imagine having component1 with function1() and component2 with funct ...

Encountering a TypeError with Angular 5 Interceptor: The do function is not recognized when using next.handle(...)

While developing an angular interceptor to verify the validity of my auth tokens, I encountered an issue where the do method is not recognized by angular. The alternative solution involving subscribe was effective, but it led to duplicate requests being se ...

Replicating an array of typed objects in Angular2

I have a collection of specific object types and I'm looking to duplicate it so that I can work on a separate version. In order for the configuratorProduct to function correctly, I need to provide it with a copy of the listProducts values: listPro ...

Leveraging an AWS Lambda function equipped with a package.json file contained within a Lambda layer

I've encountered an issue with my AWS Lambda and Lambda layer setup. In the root of my Lambda's folder structure, I have a src-ts directory containing app.js and package.json files. src-ts │ app.js │ package.json │ └─── The ...

JavaScript - Trouble encountered while trying to use splice to insert one array into another array

I've been working on creating a Cache Hashtable using JavaScript. When I use the code cache.splice(0,0, ...dataPage);, it inserts my data starting from the first position up to the length of dataPage. Assuming that my dataPage size is always 10. Th ...

What is the best way to map elements when passing props as well?

In my code, I am using multiple text fields and I want to simplify the process by mapping them instead of duplicating the code. The challenge I'm facing is that these textfields also require elements from the constructor props. import React, { Compon ...

What is the best method for retrieving the complete path of a FormControl in Angular versions 4 and above

Is there a way to obtain the complete path of a FormControl in Angular 4+? Below is my reactive form structure: { name: '', address: { city: '', country: '' } } I urgently require the full path o ...

Mat Progress Bar for Tracking Fetch API Requests and Generating XLS Files

I am interested in implementing a mat progress bar to track the progress made while fetching data from the database and creating an XLSX file. The progress does not need to be exact, rough estimates and sudden changes are acceptable. Is it feasible to achi ...

Tips for excluding test files in next.js when building

I am currently developing a next.js application with tests integrated within the page directory structure. In order to achieve this, I have made necessary configurations in the next.config.js file. const { i18n } = require('./next-i18next.config' ...

Select a specific type of child

One of my preferences: type selectedType = { name: string, category: string, details: { color: string, material: string, size: string, }, } How do I select details.material only? Here is what I expect as output: type selectedTypePic ...