カスタムフック入門 !
この記事の内容
前半 : Hooksの紹介
後半 : 以下のような、ドラッグでDOMを動かすカスタムフックの解説
Hooks!!
Hooksとは?
Hooksとは、Reactに最近追加されたAPI群です。
Hooksを使えば、関数型コンポーネントでも従来のクラス型コンポーネントとほぼ同等(筆者は困ったことないです)の処理ができるようになります。
というか、より簡潔にコードが書けるしめちゃくちゃ使いやすいので、正直もうクラス型には戻れないです。
Good bye, クラス型コンポーネント。
クラス型は勉強しなくていい?
クラス型も勉強した方がいいです。
なぜなら、既存のコードの大多数はクラス型で書かれているから。
他人のコードを参考にしたり、ソースコードを読む時に必要になるでしょう。
Hooksのメリットは「いい感じの疎結合さ」
Hooksを使った関数型コンポーネントの利点は本当にたくさんあります。
粒度を気にせずに雑多に少し列挙すると
- 渡ってくるpropsを明示できる
- thisに束縛されない(bindを使う必要がない)
- this.state.hogeって書かなくていい(hogeだけ)
- カスタムフックを作ることで、ロジックをコンポーネントから分離できる
- テストが書きやすい
- 状態と状態のバインディングが簡単にできる
これらをまとめると、Hooksのメリットは**「いい感じの疎結合さ」**だと思います。
疎結合なので変数やイベントがthisに束縛されませんし、ロジックを分離でき、テストもしやすい。
また、単に疎結合が進むだけでなく、ロジックなどはまとめられたり、状態と状態のバインディングがしやすくなったりと、扱いやすい塊にいい感じに切り分けられます。
Hooksの例
useStateがHooksの代表例です。
クラス型のthis.stateに替わるものです。
以下をご覧ください。
import { useState } from "react"
const component=()=>{
const [ hoge, setHoge ] = useState("初期値")
}
useStateの引数が初期値で、返り値の一つ目が状態変数(クラス型で言うthis.state.hoge)、二つ目がそのセッター関数です。
この返り値の名前は自分で決められますが、セッターはset{大文字から始めた状態変数名}にすることが多いです。
コードを見ると、なんとなくコンポーネントから分離している感じがあると思います。
useEffectがすごい
もう一つご紹介。
HooksAPIの1つにuseEffectという関数があります。
副作用を扱うAPIなんですが、これが本当に便利。
例えば、あるコンポーネントがAとBっていう状態変数をもっていた時に、Aが変わった時にBも変えたいってときありますよね。
そういうとき、クラス型だと、Aを変える処理の後に毎回Bを変える処理を書く必要がある。
AやBの値で分岐があったらそれも書く必要がある。
それを、useEffectだと一箇所に書くだけでいいのです。
import { useEffect } from "react"
useEffect(()=>{
// Bに関する処理
},[A])
こんな感じで、useEffectの
第一引数に行いたい処理を、
第二引数には、変更をその処理のトリガーにする変数を
配列で入れます(複数指定可能)。
Reactでは状態変数変化に伴うリレンダリングによって状態とグラフィックのバインディングを実現しています。
HooksのuseEffectを使えば、状態と状態のバインディング(状態Aが変われば状態Bも変わる)も簡単に実装できるのです。
ライフサイクルの代替とだけ思われがちですが、本質は状態と状態のバインディングだと思います。
カスタムフックって何?
さて、Hooksの良さも伝わってきたところで、カスタムフックの説明に入ります。
カスタムフックとは、Hooksを使ったユーザーオリジナルの新しい関数を指します。
ただの関数といえばそれまでですが、HooksAPIを使うことで、差分レンダリングや先述の状態-状態バインディングなどを利用することができます。
DOMをドラッグで動かせるカスタムフックを作る!
さて、ではDOMをドラッグで動かせるようなカスタムフックを作りたいと思います。
クラス型だと、クラスとロジックが密結合して再利用性に乏しい実装になってしまいがちですが、Hooksなら分離できます!
このカスタムフックを構成するHooks
使うHooksは、先述のuseState, useEffectです。
このカスタムフックの設計
- まず、対象となるDOM上でMouseDownされた時に、その時のマウス座標(初期マウス座標)を保存します。
- 次に、MouseMoveされた時、そのときのマウスがどれくらい先ほど保存した初期マウス座標からズレているかを計算します。
- そのベクトルを、DOMにstyleのtransform3dに入れて渡してあげます
- MouseUpされたら、初期マウス座標をリセット
少し簡略化して描いたので、後のコードを見て実装を確認してください。
コード
まず完成コードをお見せします。
まずは、objectのカスタムフック(コードは後述)を適用するところから。
// ごめんなさいカスタムフック外で新しいHooks、useRef使います
import React, { useRef } from 'react';
import useMoveObject from "./useMoveObject"
function App() {
// 動かしたいDOMに参照を張る currentには最新のDOMが入る 初期値はnullにしてある
const boxRef = useRef(null)
const mouseEventAndStyle = useMoveObject(boxRef.current)
return (
<div>
<div {...mouseEventAndStyle}>
<div style={{backgroundColor:"red", width:30, height:30}} ref={boxRef}>
</div>
</div>
</div>
);
}
export default App;
useObjectMoveがカスタムフックで、その返り値(eventTriggerAndStyle)をobject.jsで(仮想)DOMの中に展開しています(useRefを使ってboxRef.currentにはobjectのDOMを入れています)。
後述のuseObjectMove.jsを見てもらえればわかる通り、useObjectMoveは返り値として、onMouseDownとonMouseUp、そしてstyleをkeyに持つオブジェクトを返しています。
このオブジェクトをDOMの中で展開しているので、DOMにそれらをインラインで書き込んでいるのと同じです。
例えば、下記のsample1.jsとsample2.jsは結果としてDOMに同じ処理をしています。
const returnObject = {
onMouseDown={hoge}
onMouseUp={hogege}
style={hogegege}
}
<div {...returnObject}>
</div>
<div onMouseDown={hoge} onMouseUp={hogege} style={hogegege}>
</div>
続いて、ここで使われているカスタムフックuseObjectMoveはこんな感じ。
import { useState, useEffect } from "react"
function useMoveObject(object) {
// オブジェクトをオリジナルの座標から動かすベクトルを表す変数 XとYをkey、動かすピクセル数をvalueに取る
const [ objectMoveVector, setObjectMoveVector ] = useState( {X: 0, Y: 0} )
// ドラッグし始めた(MouseDownした)時のマウス座標 MouseDown時に保存し、MouseUp時にnullを入れてリセット
const [ mouseMoveStartCoordinates, setMouseMoveStartCoordinates ] = useState( null )
useEffect(()=>{
if( !object )return // DOMのマウント前はobjectがnullなので
if( !mouseMoveStartCoordinates ) return // ドラッグし始めてないので
// ドラッグし始めたならMouseMoveをイベントリスナーに登録し、onMouseMoveでobjectを動かす用のベクトルを計算する
document.addEventListener('mousemove', onMouseMove)
// MouseUpしたらドラッグ開始座標をリセット かつ MouseMoveのイベントリスナーも解除
document.addEventListener('mouseup', ()=>{
setMouseMoveStartCoordinates( null )
document.removeEventListener('mousemove', onMouseMove)
})
}, [ mouseMoveStartCoordinates ])
const onMouseMove=(e)=>{
// ドラッグ開始地点から現在のマウスまでのベクトルを計算し、最後にドラッグ終了した時のobjectが動いたベクトルに加算
// イベントリスナーに登録したこの関数はクロージャなので、objectMoveVectorは、この中では常に最後にドラッグ終了した時のもの
const vector = {
X : e.clientX - mouseMoveStartCoordinates.X + objectMoveVector.X,
Y : e.clientY - mouseMoveStartCoordinates.Y + objectMoveVector.Y
}
setObjectMoveVector(vector)
}
return {
// MouseDownでドラッグ開始時の座標を保存
onMouseDown : (e)=>setMouseMoveStartCoordinates( { X : e.clientX, Y : e.clientY } ),
// MouseUp時にマウス位置をリセット
onMouseUp : ()=>setMouseMoveStartCoordinates(null),
// 動かすべきベクトルをstyleとして返す
style: { transform:`translate3d(${objectMoveVector.X}px, ${objectMoveVector.Y}px, 0)` }
}
}
export default useMoveObject
DOMが欲しいのは、マウスイベントと、その結果である自身を動かすためのstyleであって、ドラッグ開始地点の座標などは不必要です。
そういった媒介となるだけの変数をカスタムフック内部に隠蔽できるので、コードがきれいになり、再利用性も高まります。
クラス型だと、こういった媒介となる変数もコンポーネントに持たせる必要がありました。
ポイント
ポイントは、先述の返り値の展開の他だと、クロージャの部分ですね。
イベントリスナーで登録した関数はクロージャになっていて、中で使う変数の値は、イベントリスナーの登録をした時の値です。
たとえその変数が更新されていようと、古い値が参照されます。
なので、イベントリスナを使う時は基本useRefを使って最新の値を参照します。
しかし今回は、マウスをドラッグし始めた座標を、クロージャの中で固定にしたかったので、そのままにしました。
まとめ
と言うわけで、Hooksでロジックを分離できるしそもそも書きやすいし最高!っていう記事でした!
ちなみに、クラスコンポーネントとHooksを使った関数コンポーネントは一緒に使うことができます(コンポーネント指向的に当然と言えば当然ですが)。
既存のコードも少しずつ移行できるのがいいですね。