💻 useHover.js
import { useEffect, useRef, useState, useCallback } from "react";
const useHover = () => {
const [state, setState] = useState(false);
const ref = useRef(null);
const handleMouseOver = useCallback(() => setState(true), []);
const handleMouseOut = useCallback(() => setState(false), []);
useEffect(() => {
const element = ref.current;
if(!element) return;
element.addEventListener('mouseover', handleMouseOver);
element.addEventListener('mouseout', handleMouseOut);
return () => {
element.removeEventListener('mouseover', handleMouseOver);
element.removeEventListener('mouseout', handleMouseOut);
}
}, [ref, handleMouseOver, handleMouseOut]);
return [ref, state];
}
export default useHover;
💻 useHover.stories.js
import styled from "@emotion/styled"
import useHover from "../../hooks/useHover";
export default {
title: 'Hook/useHover',
}
const Box = styled.div`
width: 100px;
height: 100px;
background-color: red;
`
export const Default = () => {
const [ref, hover] = useHover();
return (
<>
<Box ref={ref}/>
{hover ? <div>ToolTip!</div> : null}
</>
)
}
🖨 완성된 훅 시연
특정 엘리먼트의 스크롤 위치를 추적하는 훅
useRafState
- useScroll에서 scroll을 움직일 때마다 리렌더링 되는 것을 막는 성능 최적화 훅
💻 useRafState.js
import { useRef, useState, useCallback } from "react";
const useRafState = (initialState) => {
const frame = useRef(0);
const [state, setState] = useState(initialState);
const setRafState = useCallback((value) => {
cancelAnimationFrame(frame.current);
frame.current = requestAnimationFrame(() => {
setState(value);
})
}, []);
return [state, setRafState];
};
export default useRafState;
💻 useScroll.js
import { useEffect, useRef } from "react"
import useRafState from "./useRafState";
const useScroll = () => {
const [state, setState] = useRafState({ x: 0, y: 0 });
const ref =useRef(null);
useEffect(() => {
const element = ref.current;
if(!element) return;
const handleScroll = () => {
setState({
x: ref.current.scrollLeft,
y: ref.current.scrollTop
})
}
// passive: true로 설정해놓을 경우 preventDefault의 경우와 같을 때 브라우저가 체크하지 않아 성능적인 이점을 가질 수 있다
element.addEventListener('scroll', handleScroll, { passive: true });
return () => {
element.removeEventListener('scroll', handleScroll);
}
}, [ref, setState]);
return [ref, state];
}
export default useScroll;
💻 useScroll.stories.js
import styled from "@emotion/styled"
import useScroll from "../../hooks/useScroll";
export default {
title: 'Hook/useScroll',
}
const Box = styled.div`
width: 100px;
height: 100px;
background-color: red;
overflow: auto;
`
const Inner = styled.div`
width: 10000px;
height: 10000px;
background-image: linear-gradient(180deg, #000 0%, #fff 100%);
`
export const Default = () => {
const [ref, coord] = useScroll();
return (
<>
<Box ref={ref}>
<Inner></Inner>
</Box>
<button onClick={() => {
ref.current.scrollTo({ top: 20000, left: 20000, behavior: 'smooth'})
}}>scroll</button>
{coord.x}, {coord.y}
</>
)
}
🖨 완성된 훅 시연
💻 useKey.js
import { useCallback, useEffect } from "react"
const useKey = (event = 'keydown', targetKey, handler) => {
const handleKey = useCallback(({ key }) => {
if(key===targetKey) {
handler();
}
}, [targetKey, handler]);
useEffect(() => {
window.addEventListener(event, handleKey);
return () => {
window.removeEventListener(event, handleKey);
}
}, [event, targetKey, handleKey]);
}
export default useKey;
💻 useKey.stories.js
import useKey from "../../hooks/useKey";
export default {
title: 'Hook/useKey',
}
export const Default = () => {
useKey("keydown", "f", () => {
alert("f key down");
});
useKey("keyup", "q", () => {
alert("q key up");
});
return <>f와 q를 눌러보세요</>;
}
🖨 완성된 훅 시연
💻 useKeyPress.js
import { useCallback, useEffect, useState } from "react";
const useKeyPress = (targetKey) => {
const [keyPressed, setKeyPressed] = useState(false);
const handleKeyDown = useCallback(({ key }) => {
if(key === targetKey) {
setKeyPressed(true);
}
}, [targetKey]);
const handleKeyUp = useCallback(({ key }) => {
if(key === targetKey) {
setKeyPressed(false);
}
}, [targetKey]);
useEffect(() => {
window.addEventListener('keydown', handleKeyDown);
window.addEventListener('keyup', handleKeyUp);
return () => {
window.removeEventListener('keydown', handleKeyDown);
window.removeEventListener('keyup', handleKeyUp);
}
})
return keyPressed;
}
export default useKeyPress;
💻 useKeyPress.stories.js
import useKeyPress from "../../hooks/useKeyPress";
export default {
title: 'Hook/useKeyPress',
}
export const Default = () => {
const pressed = useKeyPress('?');
return <>{pressed ? '아이린 조 아' : '물음표를 눌러보세요'}</>;
}
🖨 완성된 훅 시연
💻 useClickAway.js
import { useEffect, useRef } from "react";
// 모바일의 경우 touchstart
const events = ['mousedown', 'touchstart']
const useClickAway = (handler) => {
const savedHandler = useRef(handler);
const ref = useRef(null);
// useClickAway 이벤트가 발생할 때마다 렌더링되지 않고 ref 값만 바뀌게 설정한다.
useEffect(() => {
savedHandler.current = handler;
}, [handler])
useEffect(() => {
const element = ref.current;
if(!element) return;
const handleEvent = (e) => {
!element.contains(e.target) && savedHandler.current(e);
}
for(const eventName of events) {
document.addEventListener(eventName, handleEvent);
}
return () => {
for(const eventName of events) {
document.removeEventListener(eventName, handleEvent);
}
};
}, [ref]);
return ref;
}
export default useClickAway;
💻 useClickAway.stories.js
import styled from "@emotion/styled"
import { useState } from "react";
import useClickAway from "../../hooks/useClickAway";
export default {
title: 'Hook/useClickAway',
}
const Popover = styled.div`
width: 200px;
height: 200px;
border: 2px solid black;
background-color: #eee;
`
export const Default = () => {
const [show, setShow] = useState(false);
const ref = useClickAway((e) => {
if(e.target.tagName !=="BUTTON")
setShow(false);
})
return (
<div>
<button onClick={() => setShow(true)}>Show</button>
<Popover ref={ref} style={{display: show ? 'block' : 'none'}}>박스 바깥을 눌러봐요!</Popover>
</div>
)
}
🖨 완성된 훅 시연
💻 useResize.js
import { useEffect, useRef } from "react";
const useResize = (handler) => {
const savedHandler = useRef(handler);
const ref = useRef(null);
useEffect(() => {
savedHandler.current = handler;
}, [handler]);
useEffect(() => {
const element = ref.current;
if(!element) return;
// 크기 변화를 추적
const observer = new ResizeObserver((entries) => {
savedHandler.current(entries[0].contentRect);
});
observer.observe(element);
return () => {
observer.disconnect();
}
}, [ref])
return ref;
}
export default useResize;
💻 useResize.js
import styled from "@emotion/styled";
import { useState } from "react";
import Image from "../../components/Image";
import useResize from "../../hooks/useResize";
const Background = styled.div`
width: 100%;
height: 400px;
background-color: blue;
`;
export default {
title: 'Hook/useResize',
}
export const Default = () => {
const [imageSize, setImageSize] = useState({ width: 0, height: 0 });
const ref = useResize(rect => {
setImageSize({ width: rect.width, height: rect.height })
})
return (
<Background ref={ref}>
<Image
width={imageSize.width}
height={imageSize.height}
src="https://picsum.photos/1000"
mode="contain"></Image>
</Background>
)
}
🖨 완성된 훅 시연
💻 useLocalStorage.js
import { useState } from "react";
const useLocalStorage = (key, initialValue) => {
const [storedValue, setStoredValue] = useState(() => {
try {
const item = localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch(error) {
console.error(error);
return initialValue;
}
});
const setValue = (value) => {
try {
const valueToStore = typeof value === 'function' ? value(storedValue) : value;
setStoredValue(value);
localStorage.setItem(key, JSON.stringify(valueToStore));
} catch (error) {
console.error(error);
}
};
return [storedValue, setValue];
};
export default useLocalStorage;
💻 useLocalStorage.stories.js
import useLocalStorage from "../../hooks/useLocalStorage";
export default {
title: 'Hook/useLocalStorage',
}
export const Default = () => {
const [status, setStatus] = useLocalStorage('status', '404 NOT FOUND');
return (
<div>
<button onClick={() => setStatus("200 OK")}>Resend</button>
{status}
</div>
)
}
💻 useSessionStorage.js
import { useState } from "react";
const useSessionStorage = (key, initialValue) => {
const [storedValue, setStoredValue] = useState(() => {
try {
const item = sessionStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch(error) {
console.error(error);
return initialValue;
}
});
const setValue = (value) => {
try {
const valueToStore = typeof value === 'function' ? value(storedValue) : value;
setStoredValue(value);
sessionStorage.setItem(key, JSON.stringify(valueToStore));
} catch (error) {
console.error(error);
}
};
return [storedValue, setValue];
};
export default useSessionStorage;
💻 useSessionStorage.stories.js
import useSessionStorage from "../../hooks/useSessionStorage";
export default {
title: 'Hook/useSessionStorage',
}
export const Default = () => {
const [status, setStatus] = useSessionStorage('status', '404 NOT FOUND');
return (
<div>
<button onClick={() => setStatus("200 OK")}>Resend</button>
{status}
</div>
)
}
🖨 완성된 훅 시연
커스텀 훅을 만들어가며 이 훅들을 프로젝트에 어서 적용하고 싶다는 생각이 든다.