Fetching Data in React
Pre Hooks
We can do it in the componentDidMound
method.
Post Hooks
Simplest way! Use effect + React state
Store the default data as state, e.g empty array / empty object
Use fetch/ajax inside the React's
useEffect
method, save the result into state, and set empty array[]
as the dependency. Leaving the dependency to be undefined (not passing the second argument to the useEffect) will make the method inside run every render, will then change the state, then rerender, change the state, and so on (DDOS).
/// In the functional componentconst [characters, setCharaters] = useState([]);useEffect(() => {fetch(endpoint).then(resp => resp.json()).then(resp => {setCharaters(resp.characters);}).catch(console.error);}, []);
But can we handle loading/error icon in the UI?
/// In the functional componentconst [characters, setCharaters] = useState([]);const [loading, setLoading] = useState(true);const [error, setError] = useState(null);useEffect(() => {setLoading(true);setCharaters([]);setError(null);fetch(endpoint).then(resp => resp.json()).then(resp => {setLoading(false);setCharaters(resp.characters);}).catch(error => {setError(error);setLoading(false);});}, []);
Custom Hooks
To make it generalizable, we use response as the state, instead of characters.
const useFetch = url => {const [response, setResponse] = useState(null);const [loading, setLoading] = useState(true);const [error, setError] = useState(null);useEffect(() => {setLoading(true);setResponse(null);setError(null);fetch(endpoint).then(resp => resp.json()).then(resp => {setLoading(false);setResponse(resp);}).catch(error => {setError(error);setLoading(false);});}, []);return [response, loading, error];}// In the componentconst [response, loading, error] = useFecth(endpoint);const characters = (response && response.characters) || [];
Why don't we use asnyc/await? We can't pass async function directly to useEffect. Thus, to use async await we need to wrap it in an inner function.
const useFetch = url => {const [response, setResponse] = useState(null);const [loading, setLoading] = useState(true);const [error, setError] = useState(null);useEffect(() => {setLoading(true);setResponse(null);setError(null);const fetchUrl = async () => {try {const response = await fetch(url);const data = await response.json();setResponse(data);} catch(error) {setError(error);} finally {setLoading(false);}}fetchUrl();}return [response, loading, error];}
Custom Reducer
const initialState = {result: null,loading: true,error: null,}const fetchReducer = (state, action) => {if (action.type === 'LOADING') {return {result: null,loading: true,error: null,};}if (action.type === 'RESPONSE_COMPLETE') {return {result: action.payload.response,loading: false,error: null,};}if (action.type === 'ERROR') {return {result: null,loading: false,error: action.payload.error,};}return state;}const useFetch = url => {const [state, dispatch] = React.useReducer(fetchReducer, initialState);useEffect(() => {dispatch({type: 'LOADING'});const fetchUrl = async () => {try {const response = await fetch(url);const data = await response.json();dispatch({type: 'RESPONSE_COMPLETE', payload: {response:data}});} catch(error) {dispatch({type: 'ERROR', payload: {error} });}}fetchUrl();}, [])return [state.result, state.loading, state.error];}
The reducer is just a function, just we can test it easily. We can use the reducer pattern when things get complicated, and we would like to abstract things out.