-
Custom hooks
커스텀 혹은 커스텀 훅을 포함한 다른 리엑트 훅을 사용할 수 있다
따라서 useState나 useReducer를 통해 관리하는 리엑트 상태를 활용할 수 있다
useEffect등에도 접근할 수 있다
프로그래밍 중 코드가 중복될 때 중복되는 코드를 갖는 함수를 만드는데 이것을 커스텀 훅으로 하려는 것
커스텀 훅 만들기
모든 훅을 독립된 파일에 저장하면 된다
hooks라는 폴더를 먼저 만들고 components 폴더 옆에 둔다
이 안에 use-counter.js 파일 생성 파일명은 사용자 설정
상수로 만들고 함수 이름은 use로 시작해야한다import { useState, useEffect } from 'react'; const useCountr = () => { //재사용하려는 로직 추가(중복되었던) //ForwardCounter에서 이것을 사용하는 것이 주 목적 const [counter, setCounter] = useState(0); useEffect(() => { const interval = setInterval(() => { setCounter((prevCounter) => prevCounter + 1); }, 1000); return () => clearInterval(interval); }, []); }; export default useCountr;
커스텀 훅 사용
import Card from "./Card"; //custom hook import import useCounter from "../hooks/use-counter"; const ForwardCounter = () => { //출력을 위해 커스텀 훅이 관리하는 counter상태에 접근해야 한다 //커스텀 훅은 함수이므로 어떤것이든 반환할 수 있다 //counter가 갖고 있는 것은 숫자이므로 숫자를 반환 //counter의 상태를 반환 이 상태는 커스텀 훅이 설정하고 관리 //따라서 ForwardCounter컴포넌트에서는 반환되는 값을 이용할 수 있다 //counter을 상수로 지정하고 이를 useCounter에 할당한다 const counter = useCounter(); // const [counter, setCounter] = useState(0); // useEffect(() => { // const interval = setInterval(() => { // setCounter((prevCounter) => prevCounter + 1); // }, 1000); // return () => clearInterval(interval); // }, []); return <Card>{counter}</Card>; }; export default ForwardCounter;
BackwardCounter.js에서도 사용하기 위해 수정해준다
use-couneter.js
import { useState, useEffect } from "react"; //true면 덧셈 false면 뺄셈을 하게 한다 //setInterval안에서는 forwards가 참인지 확인해서 참이면 setCounter를 덧셈으로 갱신 //forwards를 의존성으로 추가하여 의존성이 발생할 때마다 useEffect함수가 재실행하게 한다 //BackwardCounter로 가서 useCounter를 import const useCounter = (forwards = true) => { const [counter, setCounter] = useState(0); useEffect(() => { const interval = setInterval(() => { if (forwards) { setCounter((prevCounter) => prevCounter + 1); } else { setCounter((prevCounter) => prevCounter - 1); } }, 1000); return () => clearInterval(interval); }, [forwards]); }; export default useCountr;
BackwardCounter.js
import Card from "./Card"; import useCountr from "../hooks/use-counter"; //useCounter불러온 후 false를 인자로 전달 const BackwardCounter = () => { const counter = useCountr(false); return <Card>{counter}</Card>; }; export default BackwardCounter;
현실적인 예시
NewTask.js
import { useState } from "react"; import Section from "../UI/Section"; import TaskForm from "./TaskForm"; const NewTask = (props) => { const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); //NewTask 에서는 App.js와 달리 POST로 요청을 보내고 데이터를 추가하며 응답에 대한 로직 다른 형태 //두 컴포넌트 모두 로딩과 오류 상태를 관리하고 있으며 두 가지의 상태를 동일한 방법으로 설정한다 //또한 오류를 다루는 로직도 같다 이렇게 같은 코드가 중복되어 재사용 코드가 존재 //firebase에서 가져온 url을 추가하고 tasks.json남겨둔다 const enterTaskHandler = async (taskText) => { setIsLoading(true); setError(null); try { const response = await fetch( "https://react-app-ba5b1-default-rtdb.firebaseio.com/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; // firebase-specific => "name" contains generated id 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> ); }; export default NewTask;
use-http.js
import { useState } from "react"; const useHttp = (requestConfig, applyData) => { const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); const sendRequest = async () => { setIsLoading(true); setError(null); try { const response = await fetch(requestConfig.url, { method: requestConfig.method, headers: requestConfig.headers, body: JSON.stringify(requestConfig.body), }); if (!response.ok) { throw new Error("Request failed!"); } //훅에서 applyData함수로 데이터를 전달 const data = await response.json(); applyData(data); } catch (err) { setError(err.message || "Something went wrong!"); } setIsLoading(false); }; return { isLoading, error, sendRequest, }; }; export default useHttp;
사용자 정의 훅 사용
import React, { useEffect, useState } 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 { isLoading, error, sendRequest: fetchTasks } = useHttp(); useEffect(() => { const transformTasks = (tasksObj) => { const loadedTasks = []; for (const taskKey in tasksObj) { loadedTasks.push({ id: taskKey, text: tasksObj[taskKey].text }); } setTasks(loadedTasks); }; fetchTasks( { url: "https://react-app-ba5b1-default-rtdb.firebaseio.com/tasks.json", }, transformTasks ); }, [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;
NewtTask.js
import Section from "../UI/Section"; import TaskForm from "./TaskForm"; import useHttp from "../../hooks/use-http"; const NewTask = (props) => { //useHttp불러오고 적어준 후 객체 반환 const { isLoading, error, sendRequest: sendTaskRequest } = useHttp(); //함수 생성 const createTask = (taskText, taskData) => { const generatedId = taskData.name; // firebase-specific => "name" contains generated id const createdTask = { id: generatedId, text: taskText }; props.onAddTask(createdTask); }; const enterTaskHandler = async (taskText) => { //폼이 제출될 때마다 sendTaskRequest호출 sendTaskRequest( { url: "https://react-app-ba5b1-default-rtdb.firebaseio.com/tasks.json", method: "POST", headers: { "Content-Type": "application/json", }, body: { text: taskText }, }, createTask.bind(null, taskText) ); }; return ( <Section> <TaskForm onEnterTask={enterTaskHandler} loading={isLoading} /> {error && <p>{error}</p>} </Section> ); }; export default NewTask;