dwook.record
Published on

Custom Hooks

Authors
  • avatar
    Name
    dwook

Table of Contents

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;

참조링크