How to fetch data with useEffect
React hooks have been very promising solution for most of the coding issues that React framework had in old days. But for novice developers, it could be hard to understand. Especially if you are moving from old react to new. One of the main problem is how to fetch data with useEffect in a proper way.
1. Why fetch data with useEffect?
Often developers face scenarios where they need to fetch data or resolve a Promise when updating a state or prop. The savior is useEffect
. To first which we should understand what is useEffect.
Above is the state machine that depicts the component life cycle.
useEffect is a different than thinking this in life cycle modal. In useEffect
, an Effect is a lifecycle event.
The useEffect will be called for every lifecycle event
useEffect(() => { console.log('Called for every lifecycle event'); });
Above code block is an example for a use Effect listening to every event. But we really don’t have a useful purpose of having a effect listener like above. Rather than that if we can specifically select the effects that we need to call the function that would be more useful.
useEffect(() => { console.log('Called once for initial render'); return () => console.log('Called once when the component is unmounting'); }, []); useEffect(() => { console.log('Called every time when the state or prop is updated'); }, [stateVariable, propVariable]);
With above we can see that we cannot call an async function callback in the useEffect. Synchronous function to execute a task would make the listening to effects blocking the lifecycle flow.
2. Fetch data from useEffect
Rather than that we can execute a function that makes the async function to be executed separately, thus making the callback function not blocking the execution for an effect.
const fetchUser = userId => fetch(`https://api.example.com/user/${userId}`); // wrong implementation const [selectedUserId, setSelectedUserId] = useState(null); const [userData, setUserData] = useState(null); useEffect(async () => { // cannot use async const userData = await fetchUser(selectedUserId); }, [selectedUserId]); // correct implementation useEffect(async () => { const fetchUserData = async () => { const user = await fetchUser(selectedUserId); setUserData(user); } }, [selectedUserId]); // another way if you want to memoize or useCallback to initialize only once const fetchUserData = async (selectedUserId) => { const user = await fetchUser(selectedUserId); setUserData(user); }; useEffect(async () => { fetchUserData(selectedUserId); // passing parameter incase you want useCallback }, [selectedUserId]);
Note that both of the above examples are correct but the latter one has different benefits if you don’t want to initilize the function everytime that the effect is called.
2. Handling loading and error when fetching data
Common example is if how to show an error or how show a loading spinner when fetching data from a component.
const UserComponent = ({users}) => { const [selectedUserId, setSelectedUserId] = useState(null); const [userData, setUserData] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); useEffect(async () => { const fetchUserData = async () => { try { setLoading(true); setUserData(null); // to remove previous error const user = await fetchUser(selectedUserId); setUserData(user); } catch(errorMessage) { setError(errorMessage); setUserData(null); // in case we need to reset state } setLoading(false); } }, [selectedUserId]); return ( <div> { users.map((user) => <div key={user.userId}>{user.name}</div>) } { error && <div>Error occurred while fetching data: {error}</div> } { loading && <div>Loading user data...</div> } { userData && ( <> <h3>User Preference and Address</h3> <div>{userData.address}</div> <div>{userData.preference}</div> </> ) } </div> ); }
Yep, this is a way that we can do it. But its not a good coding structure. Lets try to improve this.
3. Custom hooks
We can do it by using a custom hook. To create a custom hook we need to always state the function with use
name.
const useUser = (initialUser) => { const [userData, setUserData] = useState(initialUser); const [selectedUserId, setSelectedUserId] = useState(initialUser.userId); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); useEffect(async () => { const fetchUserData = async () => { try { setLoading(true); setUserData(null); // to remove previous data const user = await fetchUser(selectedUserId); setUserData(user); } catch(errorMessage) { setError(errorMessage); setUserData(null); // in case we need to reset state } setLoading(false); } }, [selectedUserId]); return [{loading, error, userData}, setSelectedUserId]; } const UserComponent = ({users}) => { const [{loading, error, userData}, setSelectedUserId] = useUser(users[0]); return ( <div> { users.map((user) => <div key={user.userId}>{user.name}</div>) } { error && <div>Error occurred while fetching data: {error}</div> } { loading && <div>Loading user data...</div> } { userData && ( <> <h3>User Preference and Address</h3> <div>{userData.address}</div> <div>{userData.preference}</div> </> ) } </div> ); }
Here the custom hook takes the effect updates seperately and that allows the component code to be minimum and use the atomic effect to render the component
Is that it?
Nope; there are many other ways to utilze the effects and render the components very efficiently.
- Using useReducer
- Using Suspense and lazy loading
- Using redux, recoil or any other state management framework
Also one thing to note down is there are fetch libraries that really helpful in this.
- Apollo client(GraphQL) has useQuery, useMutation hooks that gives the
loading, error, data
etc as return variables. - Libraries like useFetch which also give custom hooks for http, https requests.
Ex:const { loading, error, data = [] } = useFetch('https://example.com/todos', options, [])
Let me know what else you have discovered to make this more easy to use and scale with the code.