hooks を含んだ安定リリースが出て早1ヶ月ちょっと、私は先月から hooks を使い始めたのですが、よそ様のライブラリや自分のお手製 hooks で「これ便利やな〜」と感じたものを紹介していきます。
リファレンス
リファレンスは最後に書くものな気がしますが、この記事は存在自体がリファレンスのリファレンスって感じの立て付けというくらいリファレンスが主役なので、最初に貼っておきます。
react-use
GitHub - streamich/react-use: React Hooks — 👍
hooks の便利関数の寄せ集め系では一番スター数が伸びている気がする。お世話になってるし、色々着想も面白いと思います。
the-platform
GitHub - palmerhq/the-platform: Web. Components. 😂
かのFormikの作者である Jared Palmer さんが作ったライブラリ。確か hooks が React Conf で発表された次の日くらいにこのレポジトリができててビビった思い出がある。名前の通り useDeviceMotion
プラットフォーム依存の状態を便利に使えるhooksが集まっている。
awesome-react-hooks
おなじみの awesome
シリーズだが hooks にもある。眺めるだけで結構楽しい。
usehooks.com
名前の通り便利なhooksのキュレーションサイト。
コメント付きで中身のコードの説明まであって便利。
Ryan Florenceさんの Twitter
Reach Router の中の人。たまに小ネタ hooks を投稿してくれているのでフォローしておくとインスピレーションが受けられるかもしれない。
hooksの紹介
ちょっと記述が楽になる系
useBool
モーダルとか実装している時に「boolean の State って true にするか false にするかのどっちかしかないから、それ用の関数最初に定義できた方が微妙に楽やな」という風に感じて作った。
// hooks の実装
function useBool(initialValue: boolean): [boolean, () => void, () => void] {
const [value, setValue] = useState(initialValue);
return [value, () => setValue(true), () => setValue(false)];
}
// 使う側
function Hoge() {
const [showDropdown, open, close] = useBool(false);
return ...
}
最初は「これ俺以外使う人いるのかな…」と思っていたが、意外とチームメンバーも地味に使っていた。人によっては気にいるかもしれない一品。
useToggle
出典: https://github.com/streamich/react-use/blob/master/docs/useToggle.md
名前の通り boolean の State に対してそれを反転する関数が手に入る hooks である。
function Hoge() {
const [on, toggle] = useToggle(false);
return <Switch on={on} onClick={toggle} />
}
これもちょいちょい使う。
useArray
その名の通り配列の状態を扱い、それに対する変化を起こさせる便利関数を詰め合わせてる。
めっちゃ便利なのでオススメしたい。
import { useState, useCallback } from 'react';
interface Functions<T> {
set: (items: T[]) => void;
push: (item: T) => void;
unshift: (item: T) => void;
deleteIndex: (index: number) => void;
toggle: (newItem: T, func: (item: T) => boolean) => void;
findAndReplace: (newValue: T, func: (item: T) => boolean) => void;
findAndRemove: (func: (item: T) => boolean) => void;
clear: () => void;
}
export default function useArray<T>(initialValue: T[]): [T[], Functions<T>] {
const [array, setArray] = useState<T[]>(initialValue);
const push = useCallback((item: T) => {
setArray((_arr) => [..._arr, item]);
}, []);
const unshift = useCallback((item: T) => {
setArray((_arr) => [item, ..._arr]);
}, []);
const deleteIndex = useCallback((index: number) => {
setArray((_arr) => {
_arr.splice(index, 1);
return [..._arr];
});
}, []);
// 指定された item が存在する場合は配列から取り除き、存在しない場合は push する
const toggle = useCallback((newItem: T, func: (item: T) => boolean) => {
setArray((_arr) => {
const copy = [..._arr];
const index = copy.findIndex((item) => func(item));
if (index > -1) {
copy.splice(index, 1);
} else {
copy.push(newItem);
}
return copy;
});
}, []);
const findAndReplace = useCallback((newItem: T, func: (item: T) => boolean) => {
setArray((_arr) => {
const copy = [..._arr];
const index = copy.findIndex((item) => func(item));
if (index > -1) {
copy[index] = newItem;
}
return copy;
});
}, []);
const findAndRemove = useCallback((func: (item: T) => boolean) => {
setArray((_arr) => {
const copy = [..._arr];
const index = copy.findIndex((item) => func(item));
if (index > -1) {
copy.splice(index, 1);
}
return copy;
});
}, []);
const clear = useCallback(() => setArray([]), []);
return [
array,
{ set: setArray, push, unshift, deleteIndex, toggle, findAndReplace, findAndRemove, clear }
];
}
// 使う側例
function Hoge() {
const [items, updateItems] = useArray<SomeItem>([]);
return (
<ul>
{items.map((item, index) => (
<li key={item.id}>
<p>{item.description}</p>
<DeleteButton onClick={() => updateItems.deleteIndex(index)}>けす</DeleteButton>
</li>
))}
</ul>
)
}
今私が携わっているプロジェクトで必要になったものをつぎ足して行っているので、網羅性は特にないが育てていく感もなんか楽しい。自分だけの useArray
を育てよう。
usePrevious
出典: https://reactjs.org/docs/hooks-faq.html#how-to-get-the-previous-props-or-state
ドキュメントにも載っている以前の State にアクセスしたい時に使う hooks 。ref を用いて前回の State を保持しておく。
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
});
return ref.current;
}
function Counter() {
const [count, setCount] = useState(0);
const prevCount = usePrevious(count);
return <h1>Now: {count}, before: {prevCount}</h1>;
}
UI系
useLockBodyScroll
出典: https://usehooks.com/useLockBodyScroll/
モーダルやらドロワーやら空中戦を広げるUIの表示時に後ろをスクロールさせたくないということはよくあるだろう。そんな時に出現させるコンポーネントでこの hooks を呼ぶだけでスクロールを防ぐことができる。
function useLockBodyScroll() {
useLayoutEffect(() => {
const originalStyle = window.getComputedStyle(document.body).overflow;
document.body.style.overflow = 'hidden';
return () => document.body.style.overflow = originalStyle;
}, []);
}
// 使用例
function Modal(){
useLockBodyScroll();
return ...
}
ただ、document以外にも ref 渡して効かせたいとか、mount/unMount でなく状態によってロックするかどうか決めたいとか結構ユースケース色々足りてなさそうなのでそこは自分でカスタマイズしていきたい。
useOutsideClick
出典: https://github.com/streamich/react-use/blob/master/docs/useOutsideClick.md
ドロップダウンとかで自分以外の領域がクリックされたら閉じるために「透明なオーバーレイ表示させて、クリックされた時にDropdownのElementが範囲に含まれていないか判定して…」みたいなことをやると思うがアレをやってくれる hooks。
const Hoge = () => {
const ref = useRef<HTMLDivElement>(null);
useOutsideClick(ref, () => {
console.log('OUTSIDE CLICKED');
});
return (
<Fuga ref={ref}/>
);
};
とても便利で助かってます。
useMedia
出典: https://github.com/streamich/react-use/blob/master/docs/useMedia.md
media-query でいろいろ状態出し分けるやつ。色んなライブラリで実装されてる。
私が携わっているプロダクトでは雑に 「768px まではモバイルで、それ以外はPCレイアウト」とだけ判定できればいいので、次のように自作しました。
function isMobileWindowSize() {
if (typeof window !== 'undefined') {
if (window.matchMedia(`(max-width: ${MAX_MOBILE_WIDTH_PX}px)`).matches) {
return true;
}
}
return false;
};
export default function useMedia() {
const [isMobile, setIsMobile] = useState(isMobileWindowSize());
const resizeEvent = useCallback(() => {
setIsMobile(isMobileWindowSize());
}, []);
useEffect(() => {
window.addEventListener('resize', resizeEvent);
return () => {
window.removeEventListener('resize', resizeEvent);
};
}, []);
return { isMobile };
}
空中戦広げる系
Modal や Toast など空中に展開するUI要素は大体のユースケースにおいて、何かしらトリガーとなるイベント(ボタン押すとか)に応じて出現するので、そういったイベントにバインドして命令的に呼び出せる方が何かと便利。
なのでこんな感じで Context 作って
const ToastContext = React.createContext({
showToast: (toastType: TOAST_TYPE, message: string) => {}
});
export const ToastProvider: React.FC = (props) => {
const [state, update] = React.useState({
show: false,
message: '',
toastType: TOAST_TYPE.SUCCESS
});
const showToast = (toastType: TOAST_TYPE, message: string) => {
update({ show: true, message, toastType });
setTimeout(() => update({ ...state, show: false, message: '' }), 300);
};
return (
<>
<ToastContext.Provider value={{ showToast }}>{props.children}</ToastContext.Provider>
<ToastMessage toastType={state.toastType}>{state.message}</ToastMessage>
</>
);
};
export const useToast = () => React.useContext(ToastContext);
使う側ではこんな感じで呼び覚ます。
const Hoge = () => {
const { showToast } = useToast();
const onClick = useCallback(() => showToast(TOAST_TYPE.SUCCESS, 'トーストだよ'), []);
return (
<button onClick={onClick}>いでよトースト!</button>
)
}
このパティーンは Modal とか Drawer でも使っていて結構便利なのでオススメです。
トラッキング
usePageview
Ryan FlorenceさんがTwitterで紹介していた useLocation との合わせ技。
locationの変更が起こるたびにGAにイベントを送っている。
I mean ... its just too easy now. pic.twitter.com/dy2R4wxZmM
— Ryan Florence (@ryanflorence) 2019年2月26日
useLocationは何を使っているかは分からないが react-use にそんな感じのものがあった。
https://github.com/streamich/react-use/blob/master/docs/useLocation.md
(react-use の useLocation はリアルな位置情報取得でした)
結構細かいトラッキングもこう行ったlocationの変更を検知するところにロジックを寄せられる気がするので、結構応用が効きそうな発想だなと思っている。この辺のトラッキング x 振る舞い共有の設計とかは一度考えてみたい。