Custom React Hooks
This is a note for a great React course on Udemy developed by Maximilian Schwarzmüller
Demo 6: Counter & Tasker (Custom Hooks)
Completed code in Github Repository
Introduction
-
Outsource stateful logic into re-usable functions.
Unlike "regular functions", custom hooks can use other React hooks and React state.
-
Use customized hook does not mean we share state or effects across components. Just share the logic, not concrete state.
Intuitive Example: Counter
- The only difference of Backward and forward is + and -. Use custom hook to get rid of code duplication.
- Always name hook with 'use-xx.js'
- Return the needed var, and accepted needed var like normal functions.
import { useState, useEffect } from 'react'; const useCounter = (forwards = true) => { const [counter, setCounter] = useState(0); useEffect(() => { const interval = setInterval(() => { forwards ? setCounter((prevCounter) => prevCounter + 1) : setCounter((prevCounter) => prevCounter - 1); }, 1000); return () => clearInterval(interval); // That's potential bugs mentioned at 5, useCallback() use case. }, [forwards]); return counter; }; export default useCounter;
import useCounter from '../hooks/use-counter'; const ForwardCounter = () => { const counter = useCounter(); return <Card>{counter}</Card>; }; export default ForwardCounter;
Realistic Example: Tasker
Both HTTP request feature has similar pattern (fetchTasks() in App.js & enterTaskHandler() in NewTask.js): isLoading, error, send request logic...
But since both of them involves hooks (useState, useEffect), normal function to remove duplication can not work.
useHttp customized hooks is a very typical customized hook example.
import { useCallback, useState } from 'react'; const useHttp = (applyData) => { const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); const sendRequest = useCallback( async (requestConfig) => { setIsLoading(true); setError(null); try { const response = await fetch(requestConfig.url, { method: requestConfig.method ? requestConfig.method : 'GET', headers: requestConfig.headers ? requestConfig.headers : {}, body: requestConfig.body ? JSON.stringify(requestConfig.body) : null, }); if (!response.ok) { throw new Error('Request failed!'); } const data = await response.json(); applyData(data); } catch (err) { setError(err.message || 'Something went wrong!'); } setIsLoading(false); }, [applyData] ); return { isLoading, error, sendRequest }; }; export default useHttp;
Two way to avoid infinite loop
- Wrap with useCallback().
- Like stated in the previous, when useEffect() in App.js, we need to add fetchTasks() into dependency. But sendRequest() in use-http.js is depended on transformTasks() in App.js, which means infinite re-render loop exits.
- To fix it, kind of a bottom-up approach:
- Wrap useCallback at transformTasks()(it depends on nothing)
- Wrap useCallback at sendRequest(), and add applyData to its dependency.
- add fetchTasks to useEffect() dependency.
- Personally, it is the most complicated way, but it reserve the flexibility of customized hook.
- Move received para down to return function. In this case, requestConfig is moved from useHttp() into sendRequest(). So useEffect() does not depend on that. </br> In this case, it is the better option, even applyData can be moved down to Comp.
import React, { useEffect, useState, useCallback } from 'react'; import Tasks from './components/Tasks/Tasks'; import NewTask from './components/NewTask/NewTask'; import useHttp from './hooks/use-http'; function App() { const [tasks, setTasks] = useState([]); const transformTasks = useCallback((taskObj) => { const loadedTasks = []; for (const taskKey in taskObj) { loadedTasks.push({ id: taskKey, text: taskObj[taskKey].text }); } setTasks(loadedTasks); }, []); const { isLoading, error, sendRequest: fetchTasks } = useHttp(transformTasks); useEffect(() => { fetchTasks({ url: 'https://review-react-default-rtdb.firebaseio.com/tasks.json', }); }, [fetchTasks]); const taskAddHandler = (task) => { setTasks((prevTasks) => prevTasks.concat(task)); }; return ( <React.Fragment> <NewTask onAddTask={taskAddHandler} /> <Tasks items={tasks} loading={isLoading} error={error} onFetch={fetchTasks} /> </React.Fragment> ); } export default App;