- Published on
Custom Hooks
- Authors

- Name
- dwook
Table of Contents
- useInput
- useTabs
- useTitle
- useClick
- useHover
- useConfirm
- usePreventLeave
- useBeforeLeave
- useFadeIn
- useNetwork
- useScroll
- useFullscreen
- useNotification
- useAxios
- useLocalStorage
- useUpdateLogger
- useTimeout
- usePrevious
- useInterval
- useFetch
- useDebounce
useInput
- 초기값과 유효성검사 함수를 인자로 받음
- onChange 이벤트에서 유효성검사 함수를 사용
const useInput = (initialValue, validator) => {
const [value, setValue] = useState(initialValue);
const onChange = event => {
const { target: { value } } = event;
let willUpdate = true; // 유효성검사를 통과 못하면 업데이트 안되게
if (typeof validator === 'function') {
willUpdate = validator(value); // willUpdate의 validator 결과를 업데이트
}
if (willUpdate) {
setValue(value);
}
};
return { value, onChange };
};
const App = () => {
const maxLen = value => value.length < 10; // 결과는 true 또는 false
const hasAt = value => value.includes('@');
const name = useInput('Hey', maxLen);
return (
<div>
<input palceholder='Name' value={name.value} onChange={name.onChange} />
<input palceholder='Name' {...name} />
</div>
);
};
import { Dispatch, SetStateAction, useCallback, useState } from 'react';
type Handler = (e: any) => void;
type ReturnTypes<T = any> = [T, Handler, Dispatch<SetStateAction<T>>];
const useInput = <T = any>(initialValue: T): ReturnTypes<T> => {
const [value, setValue] = useState(initialValue);
const handler = useCallback((e) => {
setValue(e.target.value);
}, []);
return [value, handler, setValue];
};
export default useInput;
useTabs
const useTabs = (initialTab, allTabs) => {
if (!allTabs || !Array.isArray(allTabs)) { // 배열이 아닐 때는 리턴
return;
}
const [currentIndex, setCurrentIndex] = useState(initialTab);
return {
currentItem: allTabs[currentIndex],
changeItem: setCurrentIndex
};
};
const content = [
{ tab: 'tab1', content: 'content1' },
{ tab: 'tab2', content: 'content2' }
]
const App = () => {
const { currentItem, changeItem } = useTabs(0, contnet);
return (
<div className="App">
{content.map(section, index) => {
<button onClick={() => changeItem(index)}>
{section.tab}
</button>
}}
<div>{currentItem.content}</div>
</div>
)
};
useTitle
const useTitle = initialTitle => {
const [title, setTitle] = useState(initialTitle);
const updateTitle = () => {
document.title = title;
};
useEffect(updateTitle, [title]);
// 컴퍼넌트가 마운트되면 updateTitle 함수 호출
// title이 업데이트되면 updateTitle 함수 호출.
return setTitle;
};
const App = () => {
const titleUpdater = useTitle('Loading...');
setTimeout(() => titleUpdater('Home'), 3000);
return <div className="App"></div>;
};
useClick
- 리액트 컴퍼넌트는 ref prop을 가짐. ref를 통해 HTML 엘리먼트에 접근.
- ref를 통해 접근하기 위한 변수를 useRef()의 리턴값을 이용.
- useEffect에서 element.current가 있는지 확인. 있다면, click 이벤트를 부여.
const useClick = onClick => {
if (typeof onClick !== 'function') return;
const element = useRef();
useEffect(() => {
if (element.current) {
element.current.addEventListener('click', onClick);
}
return () => {
if (element.current) {
element.current.removeEventListener('click', onClick);
}
};
}, []);
return element;
};
const App = () => {
const sayHello = () => console.log('say hello');
const title = useClick(sayHello);
return (
<div className="App">
<h1 ref={title}>Hi</h1>
</div>
);
};
useHover
const useHover = onHover => {
if (typeof onHover !== 'function') return;
const element = useRef();
useEffect(() => {
if (element.current) {
element.current.addEventListener('mouseenter', onHover);
}
return () => {
if (element.current) {
element.current.removeEventListener('mouseenter', onHover);
}
};
}, []);
return element;
};
useConfirm
- 이벤트를 실행하기전에 사용자의 확인을 받고 이벤트를 실행.
const useConfirm = (message = '', onConfirm, onCancel) => {
if (!onConfirm || typeof onConfirm !== 'function') return; // onConfirm이 존재하지 않거나 함수가 아닐 경우 return
if (onCancel && typeof onCancel !== 'function') return; // onCancel은 필수가 아님.
const confirmAction = () => {
if (window.confirm(message)) {
onConfirm(); // confirm 할 경우
} else {
onCancel(); // cancle 할 경우
}
};
return confirmAction;
};
const App = () => {
const deleteWorld = () => console.log('Deleting the world...');
const abort = () => console.log('Aborted');
const confirmDelete = useConfirm('Are you sure?', deleteWorld, abort);
return (
<div className="App">
<button onClick={confirmDelete}>Delete the world</button>
</div>
);
};
usePreventLeave
beforeunload이벤트는 window 창이 닫히기 전에 function을 실행- 새로고침하면, '사이트를 새로고침하시겠습니까? 변경사항이 저장되지 않을 수 있습니다.' 창이 뜬다.
- 창을끄면, '사이트에서 나가시겠습니까? 변경사항이 저장되지 않을 수 있습니다.' 창이 뜬다.
- 현재 대부분의 브라우저에서는 사용자 지정 문구를 사용하지 않으며 이 동작을 지원하지 않음
const usePreventLeave = () => {
const listener = event => {
event.preventDefault(); // 표준에 따라 기본 동작 방지. 크롬에서는 없어도 동작
event.returnValue = ''; // 크롬에서는 returnValue 설정 필요
};
const enablePrevent = () => window.addEventListener('beforeunload', listener);
const disablePrevent = () => window.removeEventListener('beforeunload', listener);
return { enablePrevent, disablePrevent };
};
const App = () => {
const { enablePrevent, disablePrevent } = usePreventLeave();
return (
<div className="App">
<button onClick={enablePrevent}>Protect</button>
<button onClick={disablePrevent}>Unprotect</button>
</div>
);
};
useBeforeLeave
- document 화면에서 벗어나면 발생하는 이벤트
const useBeforeLeave = onBefore => {
if (typeof onBefore !== 'function') return;
const handle = event => {
const { clientY } = event;
if (clientY <= 0) { // 위로만 이동했을 때
onBefore();
}
};
useEffect(() => {
document.addEventListener('mouseleave', handle);
return () => document.removeEventListener('mouseleave'.handle);
}, []);
};
const App = () => {
const begForLife = () => console.log('Do Not Leave');
useBeforeLeave(begForLife);
return (
<div className="App">
<h1>Hello</h1>
</div>
);
};
useFadeIn
- 커스텀 훅에서 객체를 리턴하고, 엘리먼트에서 객체를 spread 한다.
const useFadeIn = (duration = 1, delay = 0) => {
if (typeof duration !== 'number' || typeof delay !== 'number') return;
const element = useRef();
useEffect(() => {
if (element.current) {
const { current } = element;
current.style.transition = `opacity ${duration}s ease-in-out ${delay}s`;
current.style.opacity = 1;
}
}, [duration, delay]);
return { ref: element, style: { opacity: 0 } };
};
const App = () => {
const fadeInH1 = useFadeIn(1, 2);
const fadeInP = useFadeIn(5, 10);
return (
<div className="App">
<h1 {...fadeInH1}>Hello</h1>
<p {...fadeInP}>Blah Blah Blah</p>
</div>
);
};
useNetwork
- 네트워크 상태의 온라인/오프라인 상태를 navigator.onLine 으로 확인
- onChange 함수를 받아서 네트워크 상태값을 가지고 핸들링하도록 만듬
const useNetwork = onChange => {
const [status, setStatus] = useState(navigator.onLine || true);
const handleChange = () => {
if (onChange && typeof onChange === 'function') {
onChange(navigator.onLine);
}
setStatus(navigator.onLine);
};
useEffect(() => {
window.addEventListener('online', handleChange);
window.addEventListener('offline', handleChange);
return () => {
window.removeEventListener('online', handleChange);
window.removeEventListener('offline', handleChange);
};
}, []);
return status;
};
const App = () => {
const handleNetworkChange = online => {
console.log(online ? 'online' : 'offline');
};
const onLine = useNetwork(handleNetworkChange);
return (
<div className="App">
<h1>{onLine ? 'Online' : 'Offline'} </h1>
</div>
);
};
useScroll
const useScroll = () => {
const [state, setState] = useState({
x: 0,
y: 0,
});
const onScroll = () => {
setState({ x: window.scrollX, y: window.scrollY });
};
useEffect(() => {
window.addEventListener('scroll', onScroll);
return () => window.removeEventListener('scroll', onScroll);
}, []);
return state;
};
const App = () => {
const { y } = useScroll();
return (
<div className="App" style={{ height: '10000vh' }}>
<h1 style={{ position: 'fixed', color: y > 100 ? 'blue' : 'red' }}>hello</h1>
</div>
);
};
useFullscreen
- 확대할 엘리먼트를 ref로 지정
- 엘리먼트에
requestFullscreen, exitFullscreen메서드가 존재
const useFullscreen = callback => {
const element = useRef();
const runCb = isFull => {
if (callback && typeof callback === 'function') {
callback(isFull);
}
};
const triggerFull = () => {
if (element.current) {
if (element.current.requestFullscreen) {
element.current.requestFullscreen();
} else if (element.current.mozRequestFullScreen) {
element.current.mozRequestFullScreen();
} else if (element.current.webkitRequestFullscreen) {
element.current.webkitRequestFullscreen();
} else if (element.current.msRequestFullscreen) {
element.current.msRequestFullscreen();
}
runCb(true);
}
};
const exitFull = () => {
if (document.exitFullscreen) {
document.exitFullscreen();
} else if (document.mozCancelFullScreen) {
document.mozCancelFullScreen();
} else if (document.webkitExitFullscreen) {
document.webkitExitFullscreen();
} else if (document.msExitFullscreen) {
document.msExitFullscreen();
}
runCb(false);
};
return { element, triggerFull, exitFull };
};
const App = () => {
const onFullScreen = isFull => {
console.log(isFull ? 'We are full' : 'We are small');
};
const { element, triggerFull, exitFull } = useFullscreen(onFullScreen);
return (
<div className="App" style={{ height: '10000vh' }}>
<div ref={element}>
<img src="" />
<button onClick={exitFull}>Exit fullscreen</button>
</div>
<button onClick={triggerFull}>Make fullscreen</button>
</div>
);
};
useNotification
- Notification API
const useNotification = (title, options) => {
if (!('Notification' in window)) {
return;
}
const fireNotification = () => {
if (Notification.permission !== 'granted') {
Notification.requestPermission().then(permission => { // 허락요청
if (permission === 'granted') {
new Notification(title, options);
}
});
} else {
new Notification(title, options);
}
};
return fireNotification;
};
const App = () => {
const triggerNotification = useNotification('Can I steal yours?', {
body: 'I like yours',
});
return (
<div className="App" style={{ height: '10000vh' }}>
<button onClick={triggerNotification}>Hello</button>
</div>
);
};
useAxios
- 넘겨주는 axios 인스턴스가 없으면 defaultAxios를 사용
- 옵션 url은 필수
- refetch 하는 방법은 약간의 트릭
import defaultAxios from 'axios';
import { useState, useEffect } from 'react';
const useAxios = (opts, axiosInstance = defaultAxios) => {
const [state, setState] = useState({
loading: true,
error: null,
data: null,
});
const [trigger, setTrigger] = useState(0);
if (!opts.url) {
return;
}
const refetch = () => {
setState({
...state,
loading: true,
});
setTrigger(Date.now());
};
useEffect(() => {
axiosInstance(opts)
.then(data => {
setState({
...state,
loading: false,
data,
});
})
.catch(error => {
setState({
...state,
loading: false,
error
});
});
}, [trigger]);
return { ...state, refetch };
};
const App = () => {
const { loading, data, error, refetch } = useAxios({ url: 'https://api.naver.com' });
return (
<div className="App" style={{ height: '10000vh' }}>
<div>{data && data.status}</div>
<div>{loading && 'Loading...'}</div>
<button onClick={refetch}>Refetch</button>
</div>
);
};
useLocalStorage
function getSavedValue(key, initialValue) {
const savedValue = JSON.parse(localStorage.getItem(key))
if(savedValue) return savedValue;
if(initialValue instanceof Function) return initialValue();
return initialValue;
}
export default function useLocalStorage(key, initialValue) {
const [value, setValue] = useState(() => getSavedValue(key, initialValue));
useEffect(() => {
localStorage.setItem(key, JSON.stringify(value)) // string만 localStorage에 들어가도록
}, [key, value])
return [value, setValue]
}
export default function App() {
const [name, setName] = useLocalStorage('name', '');
return (
<input
type="text"
value={name}
onChange={e => setName(e.target.value)}
/>
)
}
useUpdateLogger
export default function useUpdateLogger(value) {
useEffect(() => {
console.log(value);
}, [value])
}
export default function App() {
const [name, setName] = useLocalStorage('name', '');
useUpdateLogger(name); // name이 바뀔때마다 log가 찍힘
return (
<input
type="text"
value={name}
onChange={e => setName(e.target.value)}
/>
)
}
useTimeout
import { useEffect, useRef } from 'react';
const useTimeout = (callback, timeout) => {
const savedCallback = useRef(null);
savedCallback.current = callback;
useEffect(
() => {
savedCallback.current = callback;
},
[callback]
);
useEffect(
() => {
if (timeout) {
const timeoutId = setTimeout(() => {
savedCallback.current();
}, timeout);
return () => clearTimeout(timeoutId)
}
},
[timeout]
)
}
import { useState } from 'react';
const ExampleComponent = () => {
const [message, setMessage] = useState('');
useTimeout(() => {
setMessage('Hello World');
}, 7500);
return (<p>{message}</p>);
}
usePrevious
import { useEffect, useRef } from 'react';
const usePrevious = (state) => {
const ref = useRef();
useEffect(() => {
ref.current = state;
});
return ref.current; // 처음에는 undefined를 리턴.
}
import { useState } from 'react';
const ExampleComponent = () => {
const [counter, setCounter] = useState(0);
const previousCounter = usePrevious(counter);
console.log(counter, previousCounter); // 0, undefined
return (
<>
<p>Counter: {counter}</p>
<p>Previous Counter: {previousCounter}</p>
<button onClick={() => setCounter(counter + 1)}>Next</button>
</>
);
}
useInterval
import { useEffect, useRef } from 'react';
const useInterval = (callback, delay) => {
const savedCallback = useRef(null);
savedCallback.current = callback;
useEffect(
() => {
savedCallback.current = callback;
},
[callback]
);
useEffect(
() => {
if (delay) {
const intervalId = setInterval(() => {
savedCallback.current();
}, delay);
return () => clearInterval(intervalId)
}
},
[delay]
)
}
import { useState } from 'react';
const ExampleComponent = () => {
const [seconds, setSeconds] = useState(0);
useInterval(() => {
setSeconds(seconds + 1);
}, 1000);
return <p> Seconds passed: {seconds}</p>;
}
useFetch
// type1
const json = await fetch(url, options).then(response => response.json());
// type2
const response = await fetch(url, options);
const json = await response.json();
import { useState, useEffect } from 'react';
const useFetch = (initialUrl, initialOptions = {}) => {
const [url, setUrl] = useState(initialUrl);
const [options, setOptions] = useState(initialOptions);
const [data, setData] = useState(null);
const [error, setError] = useState(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
const fetchData = async() => {
try {
setIsLoading(true);
const response = await fetch(url, options);
const json = await response.json();
setData(json);
} catch (err) {
setError(err);
} finally {
setIsLoading(false);
}
}
fetchData();
}, [url, options]);
return ({data, error, isLoading, setUrl, setOptions});
const URL = 'https://jsonplaceholder.typicode.com/todos';
const ExampleComponent = () {
const { data, error, isLoading } = useFetch(URL);
if(isLoading) {
return (<p>Loading...</p>)
}
if (error) {
return <p>{error?.message}</p>;
}
const renderItem = ({id, title})=> (
<div key = {`item-${id}`}>
<p>{id} - {title}</p>
</div>
);
return data.map(renderItem);
}
useDebounce
import { useState, useEffect } from "react";
//* debounce 된 값을 return
const useDebounce = (value: string, delay: number) => {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => {
clearTimeout(handler);
};
}, [value, delay]);
return debouncedValue;
};
export default useDebounce;
참조링크