Handling the request in redux saga have no many steps more!
The Problem
In the article on the official docs for redux-saga, we handle the request like that:
// saga.ts
export function* fetchData(action) {
try {
yield put({ type: 'FETCHING' })
const data = yield call(Api.fetchUser, action.payload.url)
yield put({ type: 'FETCH_SUCCEEDED', data })
} catch (error) {
yield put({ type: 'FETCH_FAILED', error })
}
}
While the reducer must has the code block like switch...case
to catch these actions
// reducer.ts
export const reducer (state = {}, action) {
switch (action.type) {
case "FETCHING":
return state = {
...state,
loading: true,
}
case "FETCH_SUCCEEDED":
return state = {
...state,
data: action.payload.data,
}
case "FETCH_FAILED":
return state = {
...state,
error: action.payload.error,
}
default:
return state
}
}
If in the case we have many the request we must use the many the case with many the action like Fetch to handle the request
The Solution
We will write the reducer act like a middleware to handle the request like FETCH
The used api is random-user-api
The structure for the store folder
store
|--- actions
|--- reducers
|--- sagas
|--- utils
Types for Action
Firstly, we write the type for action
// actions/types.ts
export type TFunctionAction<T = any, S = string> = () => {
type: S;
payload?: T;
};
export type TObjectAction<T = any, S = string> = ReturnType<
TFunctionAction<T, S>
>;
Reducers
Writing the func fetchReducer - handle the fetch dispatched action
// reducers/fetchReducer.ts
import { TObjectAction } from "../actions/types";
export const fetchReducer = (reducer: any) => (
state: any,
action: TObjectAction<any>
) => {
const { type } = action;
if (type.includes("FETCHING")) {
return (state = {
...state,
isLoading: true
});
}
if (type.includes("SUCCESS")) {
const { dataKey, data } = action.payload;
return (state = {
...state,
[dataKey]: data,
isLoading: false
});
}
if (type.includes("FAILED")) {
return (state = {
...state,
error: action.payload.error
isLoading: true
});
}
return reducer(state, action);
};
The function fetchReducer
is curry function. The first func receive the child reducer, the second func receive the state and action of child reducer. This func with check the action.type
and decide how to impact to the state.
In the code block if (type.includes("SUCCESS"))
we notice the dataKey
, the dataKey
is key object was sent by dispatched action (saga). We use it like a variable to store a fetched data. The dataKey
must be unique.
We will delcare the dataKey in reducer.
// reducers/RandomUser.ts
import { User } from "../../types";
import { RandomUserAction } from "../actions/randomUser";
import { TObjectAction } from "../actions/types";
export const randomUserDataKey = "randomUserData";
export type TState = {
[randomUserDataKey]: {
result: User[];
};
isLoading: boolean;
};
const initialState: TState = {
[randomUserDataKey]: {
result: []
},
isLoading: false
};
export const randomUserReducer = (
state = initialState,
action: TObjectAction
) => {
switch (action.type) {
case RandomUserAction.CLEAR_FETCH_RESULTS:
state = initialState;
return state;
default:
return state;
}
};
randomUserDataKey
above is the dataKey to store fetched data.
Summary all, we have the root reducer.
// reducers/index.ts
import { combineReducers } from "redux";
import { fetchReducer } from "./fetchReducer";
import { randomUserReducer, TState } from "./randomUser";
const rootReducer = combineReducers({
randomUser: fetchReducer(randomUserReducer) as () => TState,
});
export type AppState = ReturnType<typeof rootReducer>;
export default rootReducer;
The prepared reducer is done!
Actions
Next to is actions
Writing functions to create common actions
// utils/createActionCommon.ts
import { TFunctionAction } from "../actions/types";
type TActionCommon = ReturnType<TFunctionAction<any, string>>;
export const createActionFetching = (rootName: string): TActionCommon => ({
type: `${rootName}/FETCHING`
});
export const createActionSuccess = <T>(
rootName: string,
payload: T
): TActionCommon => ({
type: `${rootName}/SUCCESS`,
payload
});
export const createActionFail = (
rootName: string,
error: any
): TActionCommon => ({
type: `${rootName}/FAILED`,
payload: error
});
export const createNormalAction = <T extends { [x: string]: any }>(
action: T,
rootName: string
): T => {
return Object.keys(action).reduce((acc, cur) => {
acc[cur] = `${rootName}/${action[cur]}`;
return acc;
}, {} as any);
};
What is a rootName? The root name is prefix name for each action for a individual reducer, likes: @user, @employee.
The root name will be written in actions/randomUser.ts
.
import { createNormalAction } from "../utils/createCommonAction";
import { TFunctionAction } from "./types";
export const randomUserActionRootName = "@randomUser";
export const RandomUserAction = createNormalAction(
{
FETCH: "FETCH_USERS",
CLEAR_FETCH_RESULTS: "CLEAR_FETCH_RESULTS"
},
randomUserActionRootName
);
export const fetchUserRandom: TFunctionAction = () => ({
type: RandomUserAction.FETCH
});
export const resetUserRandom: TFunctionAction = () => ({
type: RandomUserAction.CLEAR_FETCH_RESULTS
});
Saga
Now we will write the function handle these fetch actions above.
// utils/handleSagaRequest.ts
import axios, { AxiosRequestConfig } from "axios";
import { put } from "redux-saga/effects";
import {
createActionFetching,
createActionFail,
createActionSuccess
} from "./createCommonAction";
export function* handleSagaRequest(
request: AxiosRequestConfig,
dataKey: string,
actionRootName: string
) {
try {
yield put(createActionFetching(actionRootName));
const response = yield axios(request);
yield put(
createActionSuccess(actionRootName, { dataKey, data: response.data })
);
} catch (error) {
yield put(createActionFail(actionRootName, error));
}
}
in the saga file.
// sagas/randomUser.ts
import { AxiosRequestConfig } from "axios";
import { handleSagaRequest } from "../utils/handleSagaRequest";
import { randomUserDataKey } from "../reducers/randomUser";
import { takeLatest } from "redux-saga/effects";
import {
RandomUserAction,
randomUserActionRootName
} from "../actions/randomUser";
function* fetchRandomUser() {
const request: AxiosRequestConfig = {
method: "GET",
url: "https://randomuser.me/api"
};
yield handleSagaRequest(request, randomUserDataKey, randomUserActionRootName);
}
export function* randomUserSaga() {
yield takeLatest(RandomUserAction.FETCH, fetchRandomUser);
}
Config store and component
Config store and apply saga.
import { applyMiddleware, createStore } from "redux";
import createSagaMiddleware from "redux-saga";
import rootReducer from "./reducers";
import randomUser from "./sagas/randomUser";
// Create the saga middleware
const sagaMiddleware = createSagaMiddleware();
// Mount it on the Store
const store = createStore(rootReducer, applyMiddleware(sagaMiddleware));
// Run the saga
sagaMiddleware.run(randomUser);
export type RootState = ReturnType<typeof store.getState>;
export default store;
In the root of your app.
// src/index.tsx
import { render } from "react-dom";
import { Provider } from "react-redux";
import store from "./store";
import App from "./App";
const rootElement = document.getElementById("root");
render(
<Provider store={store}>
<App />
</Provider>,
rootElement
);
Write the component.
// src/components/RandomUser.tsx
import React from "react";
import { useDispatch, useSelector } from "react-redux";
import { fetchUserRandom, resetUserRandom } from "../store/actions/randomUser";
import { AppState } from "../store/reducers";
export const RadomUser = () => {
const { randomUserData, isLoading } = useSelector(
(state: AppState) => state.randomUser
);
const dispatch = useDispatch();
const handleFetch = () => {
dispatch(fetchUserRandom());
};
const handleClear = () => {
dispatch(resetUserRandom());
};
return (
<>
<button onClick={handleFetch}>Fetch the random user</button>
{isLoading ? (
<p>Fetching the user random list</p>
) : (
<div>
<br />
<button onClick={handleClear}>Reset the result</button>
<pre>{JSON.stringify(randomUserData, null, 2)}</pre>
</div>
)}
</>
);
};
The result
When the button Fetch the random user is clicked, we will dispacth these actions:
- @randomUser/FETCHING.
- @randomUser/SUCCESS and if failed is will be @randomUser/FAILED.
The function handleSagaRequest will handle all - add/update the fetched data to the state.
Playground
I prepared codesanbox with add more the todo api example.
The conclusion
With this way we don't need care more about the many requests have the same acting.
Comments