We're using React hooks for managing react state. Usually we separate hooks file to separate
.state.tsfile
As useState hook use
Object.isto compare, it needs to pass new data everytimeSo each hook should contain one state. If state uses as object, every change of the field, will trigger re-render of all components that depends on that state, so only if it used by one component at the time, then it make sense to use object.
Regular useState approach will have next look
import { useState, useEffect } from 'react';
import { fetchDate } from './component.api';
import { formatUser, handleError } from './component.utils';
import { IUser } from './component.typings';
// Typing for our hook state data
interface IComponentStateData {
users: IUser[];
isLoading: boolean
}
// Typing for our hook return values
interface IComponentState extends IComponentStateData {
error: string;
selectedUser: IUser;
onSelectUser: (user: IUser) => void;
}
const initialState = {
users: [];
isLoading: true,
}
// Custom hook
export const useComponentState = (id: string): IComponentState => {
const [state, setState] = useState<IComponentStateData>(initialState);
const [selectedUser, setSelectedUser] = useState<IUser | null>(null);
const [error, setError] = useState<string>('');
// Using instead componentDidMount
useEffect(() => {
const fetchUsers = async () => {
try {
const users = await fetchData(id);
// Every setState we pass new object
setState(currentState => ({
...currentState, // Just as example how to save previous state
users: formatUsers(users),
isLoading: false,
}))
} catch (e) {
// As it update every field on state, no need to pass previous one
setState(currentState => ({
error: handleError(e),
isLoading: false,
}))
}
}
fetchUsers();
}, []);
// Regular method that we can use for onClick
const onSelectUser = (selectedUser: IUser) => {
setSelectedUser(selectedUser)
}
// Returning data and needed method from hook
return {
...state,
error,
selectedUser,
onSelectUser
}
}And will use it in next way
import * as React from "react";
import { Spinner } from '@components/spinner";
import { useComponentState } from "./component.state";
import { IUser } from "./component.typings";
export interface IComponent {
id: string;
}
export const Component = (props: IComponent) => {
const { users, isLoading, users, selectedUser, onSelectUser } = useComponentState(props.id);
if (isLoading) {
return (<Spinner />)
}
/* ... */
};useEffect doesn't expect the callback to return promise, that's the reason why it's not allowed to make async useEffect. And solution to this, is create async / await functions. It can be done in different ways, but the one, which you need to avoid is creating self-invoking functions, better create functions inside hook or outside and just call it in useEffect:
Bad example
useEffect(() => {
(async () => {
const variable_name = await doSomething();
})();
}, []);Preferable way
const doSomething = async () => {
const variable_name = await request();
setState(variable_name);
};
useEffect(() => {
doSomething();
}, []);It works identically, but reads better and looks cleaner, if needed, we can reuse that functions later, for example in another hook.