這章節將進一步調整並優化 Custom Hook 的使用邏輯
Custom Http Hook
有兩個送出 http request 至 server 的 component 如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
| function App() { const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); const [tasks, setTasks] = useState([]);
const fetchTasks = async (taskText) => { setIsLoading(true); setError(null); try { const response = await fetch( '${server_url}/tasks.json' );
if (!response.ok) { throw new Error('Request failed!'); }
const data = await response.json();
const loadedTasks = [];
for (const taskKey in data) { loadedTasks.push({ id: taskKey, text: data[taskKey].text }); }
setTasks(loadedTasks); } catch (err) { setError(err.message || 'Something went wrong!'); } setIsLoading(false); };
useEffect(() => { 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> ); }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| const NewTask = (props) => { const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null);
const enterTaskHandler = async (taskText) => { setIsLoading(true); setError(null); try { const response = await fetch( '${server_url}/tasks.json', { method: 'POST', body: JSON.stringify({ text: taskText }), headers: { 'Content-Type': 'application/json', }, } );
if (!response.ok) { throw new Error('Request failed!'); }
const data = await response.json();
const generatedId = data.name; const createdTask = { id: generatedId, text: taskText };
props.onAddTask(createdTask); } catch (err) { setError(err.message || 'Something went wrong!'); } setIsLoading(false); };
return ( <Section> <TaskForm onEnterTask={enterTaskHandler} loading={isLoading} /> {error && <p>{error}</p>} </Section> ); };
|
很顯然地,因其中 http request 格式與 error handler 都非常相似,所以可以透過 custom hook 的方式來封裝這段程式碼
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| const useHttp = () => { const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null);
const sendRequest = async (requestConfig, applyData) => { 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); };
return { isLoading, error, sendRequest, }; };
|
接下來便可以在 App 中使用這個 custom hook
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| const App = () => { const [tasks, setTasks] = useState([]);
const transformTasks = (data) => { const loadedTasks = [];
for (const taskKey in data) { loadedTasks.push({ id: taskKey, text: data[taskKey].text }); }
setTasks(loadedTasks); };
const { isLoading, error, sendRequest: fetchTasks } = useHttp( { url: "https://react-http-77951-default-rtdb.firebaseio.com//tasks.json", }, transformTasks );
useEffect(() => { fetchTasks(); }, []);
const taskAddHandler = (task) => { setTasks((prevTasks) => prevTasks.concat(task)); };
return ( ... ); };
|
但目前為止,功能看似正常了,但在 useEffect()
會出現一個提示,因為凡是在其中所使用到的 component 內 function ,都需要加到 dependency array 之中
1 2 3
| useEffect(() => { fetchTasks(); }, [fetchTasks]);
|
但這麼一來,就會出現 infinite loop 的情況,因為當 fetchTasks
invoked,因其中改變 State,就會導致 component re-render,接著其中的 function 包含 fetchTasks
都會被重新產生,即便功能完全相同,因 function 也是 object,所以會在記憶體中另一個位置被新增,進而導致被判斷為不同 object 而使的這段程式碼被不斷重複執行。
為了解決這個問題,可以將 function 在 custom hook useHttp
中透過 useCallback()
來包裝它,藉此告訴 React 如果內容不變就可以不必重新生成這段 fucntion
1 2 3 4 5 6 7 8
| const useHttp = () => { ... const sendRequest = useCallback(async (requestConfig, applyData) => { ... }, []);
return (...); };
|
Adjusting the Custom Hook Logic
到這裡其實就已經完成預期的功能了,但我們還可以進一步優化這段程式碼 ; 在前面的程式碼中,參數都透過 custom hook 直接帶入,但其實都只有在一個地方被使用,所以其實可以在 costom hook 所回傳的 fucntion 中帶入即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| const App = () => { const { isLoading, error, sendRequest: fetchTasks } = useHttp();
useEffect(() => { const transformTasks = (data) => { const loadedTasks = [];
for (const taskKey in data) { loadedTasks.push({ id: taskKey, text: data[taskKey].text }); }
setTasks(loadedTasks); };
fetchTasks( { url: "https://react-http-77951-default-rtdb.firebaseio.com//tasks.json", }, transformTasks ); }, [fetchTasks]); };
const useHttp = () => { const sendRequest = useCallback(async (requestConfig, applyData) => { ... }); };
|
Using The Custom Hook in More Components
接著這個 custom hook 使用到另外一個 component 之中,但這個 component 送出 http request 的方式不太一樣,需透過使用者手動出發,而非在 useEffect()
自動送出,而且後面對於資料的應用需要多一個傳入的參數 teskText
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| const NewTask = (props) => { const { isLoading, error, sendRequest: sendTaskRequest } = useHttp();
const createTask = async (taskData) => { const generatedId = taskData.name; const createdTask = { id: generatedId, text: taskText }; props.onAddTask(createdTask); };
const enterTaskHandler = async (taskText) => { sendTaskRequest( { url: "https://react-http-77951-default-rtdb.firebaseio.com//tasks.json", method: "POST", body: JSON.stringify({ text: taskText }), headers: { "Content-Type": "application/json", }, }, createTask ); }; };
|
這種情況就可以透過原生 JavaScript 的 bind()
來帶入額外的參數
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| const NewTask = (props) => { const { isLoading, error, sendRequest: sendTaskRequest } = useHttp();
const createTask = async (taskText, taskData) => { const generatedId = taskData.name; const createdTask = { id: generatedId, text: taskText }; props.onAddTask(createdTask); };
const enterTaskHandler = async (taskText) => { sendTaskRequest( { url: "https://react-http-77951-default-rtdb.firebaseio.com//tasks.json", method: "POST", body: JSON.stringify({ text: taskText }), headers: { "Content-Type": "application/json", }, }, createTask.bind(null, taskText) ); }; };
|
資料參考
React - The Complete Guide (Incl Hooks, React Router, Redux)