Pomodoro / Reddit Thunks
Pomodoro Timer doesn't do any server requests, but if it had needed to make one, it would've used axios library to fetch data as the Reddit Poc application does.
Axios Library
The library is installed via command line:
npm install axios
Since axios is a promise-based HTTP Client,
a developer should handle both successful and erroneous responses himself with
then
and catch
methods chaining after the request.
Axios makes XMLHttpRequests from the browser
Request And Response Actions
In the example above, the response has type:
export interface IUserData {
name?: string;
iconImg?: string;
}
This type is used to collect data from the successful response and also to deliver the data to Redux reducer. To do that, Redux actions are needed:
export const ME_REQUEST = "ME_REQUEST";
export type MeRequestAction = {
type: typeof ME_REQUEST;
};
export const meRequest: ActionCreator<MeRequestAction> = () => ({
type: ME_REQUEST,
});
export const ME_REQUEST_SUCCESS = "ME_REQUEST_SUCCESS";
export type MeRequestSuccessAction = {
type: typeof ME_REQUEST_SUCCESS;
data: IUserData;
};
export const meRequestSuccess: ActionCreator<MeRequestSuccessAction> = (
data: IUserData
) => ({
type: ME_REQUEST_SUCCESS,
data,
});
export const ME_REQUEST_ERROR = "ME_REQUEST_ERROR";
export type MeRequestErrorAction = {
type: typeof ME_REQUEST_ERROR;
error: string;
};
export const meRequestError: ActionCreator<MeRequestErrorAction> = (
error: string
) => ({
type: ME_REQUEST_ERROR,
error,
});
The Service Layer
The next step is to use it in Redux store, for instance, to authenticate a user via Reddit API:
import axios from "axios";
export const meRequestAsync =
(): ThunkAction<void, RootState, unknown, Action<string>> =>
(dispatch, getState) => {
dispatch(meRequest());
axios
.get("https://oauth.reddit.com/api/v1/me", {
headers: { Authorization: `Bearer ${getState().token}` },
})
.then((resp) => {
const userData = resp.data;
dispatch(
meRequestSuccess({
name: userData.name,
iconImg: userData.icon_img,
})
);
})
.catch((error) => {
console.log(error);
dispatch(meRequestError(error));
});
};
This method uses dispatch
function three times: to start the request, to get the results or
to process an error.
Data Sync With Redux Store
Using dispatch
method the example above talks to Redux store and updates data there
once the request is initiated and completed. It is there the request and response
actions are used:
export type MeState = {
loading: boolean;
error: string;
data: IUserData
};
type MeActions =
| MeRequestAction
| MeRequestSuccessAction
| MeRequestErrorAction;
export const meReducer: Reducer<MeState, MeActions> = (state, action) => {
switch (action.type) {
case ME_REQUEST:
return {
...state,
loading: true,
};
case ME_REQUEST_ERROR:
return {
...state,
loading: false,
error: action.error,
};
case ME_REQUEST_SUCCESS:
return {
...state,
loading: false,
data: action.data,
};
default:
return state;
}
};
A smaller meReducer
becomes a part of a root reducer:
export const rootReducer: Reducer<RootState> = (
state = initialState,
action
) => {
switch (action.type) {
//..
case ME_REQUEST:
case ME_REQUEST_SUCCESS:
case ME_REQUEST_ERROR:
return {
...state,
me: meReducer(state.me, action),
};
//..
}
}
Where the root state contains MeState
:
export type RootState = {
//..
user: MeState;
//..
};
A Custom React Hook
It is now time to use meRequestAsync
function in a custom hook. This hook would be then
used by React function components the way the standard hooks are used:
import { useEffect } from "react";
import { useDispatch, useSelector } from "react-redux";
import { RootState } from "../../store/reducer";
import { IUserData, meRequestAsync } from "../../store/me/actions";
export function useUserData() {
const data = useSelector<RootState, IUserData>(state => state.me.data)
const token = useSelector<RootState, string>(state => state.token)
const loading = useSelector<RootState, boolean>((state) => state.me.loading);
const dispatch = useDispatch<any>();
useEffect( ()=> {
if (token !== "undefined" && token) {
dispatch(meRequestAsync())
}
}, [token])
return {
data,
loading
}
}
Example Usage In A Context Provider
This custom hook is using dispatch(meRequestAsync())
in useEffect
hook, and in turn,
any component can use it, for instance a user context provider which will make the user
information visible to all nested components
(as per React context definition):
import React from 'react';
import { useUserData } from '../../utils/react/useUserData';
import { useDispatch } from 'react-redux';
import { saveToken } from '../../store/me/actions';
export interface IUserContextData {
name?: string;
iconImg?: string;
}
export const userContext = React.createContext<IUserContextData>({})
export function UserContextProvider({ children }: Readonly<{ children: React.ReactNode }>) {
const dispatch = useDispatch<any>();
React.useEffect(() => {
dispatch(saveToken())
}, []);
const { data } = useUserData();
return (
<userContext.Provider value={data as IUserContextData}>
{ children }
</userContext.Provider>
)
}
Example Usage in A Component
Another example could be a simple component, displaying information from a user account:
export function SearchBlock() {
const { data, loading } = useUserData();
return (
<div className={styles.searchBlock}>
<UserBlock avatarSrc={data ? data.iconImg : undefined}
username={data ? data.name : undefined}
loading={loading}
/>
</div>
);
}