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?