Fetching Data in React

Pre Hooks

We can do it in the componentDidMound method.

Post Hooks

Simplest way! Use effect + React state

  1. Store the default data as state, e.g empty array / empty object

  2. 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 component
const [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 component
const [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 component
const [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.