フック
フック API は、状態と副作用を合成できる新しい概念です。フックを使用すると、コンポーネント間で状態付きロジックを再利用できます。
しばらく Preact を使用してきた方は、「レンダープロップス」や「高階コンポーネント」のような、これらの課題を解決しようとするパターンに精通しているかもしれません。これらの解決策は、コードの追跡を困難にし、より抽象的なものにする傾向がありました。フック API を使用すると、状態と副作用のロジックをきれいに抽出することが可能になり、それに依存するコンポーネントとは独立して、そのロジックの単体テストも簡素化されます。
フックはどのコンポーネントでも使用でき、クラスコンポーネント API に依存するthis
キーワードの多くの落とし穴を回避します。コンポーネントインスタンスからプロパティにアクセスする代わりに、フックはクロージャに依存します。これにより、値がバインドされ、非同期状態更新を処理する際に発生する可能性のある多くの古いデータの問題が解消されます。
フックをインポートする方法は2つあります。preact/hooks
またはpreact/compat
からです。
はじめに
フックを理解する最も簡単な方法は、同等のクラスベースのコンポーネントと比較することです。
例として、数値とそれを1増やすボタンを表示する単純なカウンターコンポーネントを使用します。
class Counter extends Component {
state = {
value: 0
};
increment = () => {
this.setState(prev => ({ value: prev.value +1 }));
};
render(props, state) {
return (
<div>
<p>Counter: {state.value}</p>
<button onClick={this.increment}>Increment</button>
</div>
);
}
}
REPL で実行次に、フックを使用して構築された同等の関数コンポーネントを示します。
function Counter() {
const [value, setValue] = useState(0);
const increment = useCallback(() => {
setValue(value + 1);
}, [value]);
return (
<div>
<p>Counter: {value}</p>
<button onClick={increment}>Increment</button>
</div>
);
}
REPL で実行この時点ではかなり似ていますが、フックのバージョンをさらに簡素化できます。
カウンターロジックをカスタムフックに抽出することにより、コンポーネント間で簡単に再利用できるようにします。
function useCounter() {
const [value, setValue] = useState(0);
const increment = useCallback(() => {
setValue(value + 1);
}, [value]);
return { value, increment };
}
// First counter
function CounterA() {
const { value, increment } = useCounter();
return (
<div>
<p>Counter A: {value}</p>
<button onClick={increment}>Increment</button>
</div>
);
}
// Second counter which renders a different output.
function CounterB() {
const { value, increment } = useCounter();
return (
<div>
<h1>Counter B: {value}</h1>
<p>I'm a nice counter</p>
<button onClick={increment}>Increment</button>
</div>
);
}
REPL で実行CounterA
とCounterB
はどちらも完全に独立しています。どちらもuseCounter()
カスタムフックを使用していますが、それぞれにそのフックに関連付けられた状態の独自のインスタンスがあります。
少し奇妙に見えると思いますか?あなただけではありません!
このアプローチに慣れるまで、私たち多くの時間がかかりました。
依存関係引数
多くのフックは、フックを更新するタイミングを制限するために使用できる引数を受け入れます。Preact は依存関係配列内の各値を検査し、フックが最後に呼び出されてから変更されたかどうかを確認します。依存関係引数が指定されていない場合、フックは常に実行されます。
上記のuseCounter()
の実装では、useCallback()
に依存関係の配列を渡しました。
function useCounter() {
const [value, setValue] = useState(0);
const increment = useCallback(() => {
setValue(value + 1);
}, [value]); // <-- the dependency array
return { value, increment };
}
ここでvalue
を渡すと、value
が変更されるたびにuseCallback
は新しい関数参照を返します。これは、「古いクロージャ」を回避するために必要です。古いクロージャでは、コールバックは常に作成された時点の最初のレンダリングのvalue
変数を参照するため、increment
は常に1
の値を設定することになります。
これにより、
value
が変更されるたびに新しいincrement
コールバックが作成されます。パフォーマンス上の理由から、依存関係を使用して現在の値を保持するのではなく、コールバックを使用して状態値を更新する方が多くの場合優れています。
状態管理フック
ここでは、関数コンポーネントに状態付きロジックを導入する方法について説明します。
フックが導入される前は、状態が必要な場所ではクラスコンポーネントが必要でした。
useState
このフックは引数を受け入れます。これは初期状態になります。呼び出されると、このフックは2つの変数の配列を返します。1つ目は現在の状態、2つ目は状態のセッターです。
セッターは、従来の状態のセッターと同様に動作します。値またはcurrentStateを引数とする関数を取得します。
セッターを呼び出し、状態が異なる場合、そのuseStateが使用されているコンポーネントから始まる再レンダリングがトリガーされます。
import { useState } from 'preact/hooks';
const Counter = () => {
const [count, setCount] = useState(0);
const increment = () => setCount(count + 1);
// You can also pass a callback to the setter
const decrement = () => setCount((currentCount) => currentCount - 1);
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>Increment</button>
<button onClick={decrement}>Decrement</button>
</div>
)
}
REPL で実行初期状態が高価な場合は、値の代わりに関数を渡す方が良いです。
useReducer
useReducer
フックは、reduxと非常によく似ています。useStateと比較して、次の状態が前の状態に依存する複雑な状態ロジックがある場合に、より簡単に使用できます。
import { useReducer } from 'preact/hooks';
const initialState = 0;
const reducer = (state, action) => {
switch (action) {
case 'increment': return state + 1;
case 'decrement': return state - 1;
case 'reset': return 0;
default: throw new Error('Unexpected action');
}
};
function Counter() {
// Returns the current state and a dispatch function to
// trigger an action
const [count, dispatch] = useReducer(reducer, initialState);
return (
<div>
{count}
<button onClick={() => dispatch('increment')}>+1</button>
<button onClick={() => dispatch('decrement')}>-1</button>
<button onClick={() => dispatch('reset')}>reset</button>
</div>
);
}
REPL で実行メモ化
UIプログラミングでは、計算コストの高い状態や結果が頻繁にあります。メモ化は、その計算の結果をキャッシュして、同じ入力が使用された場合に再利用できるようにします。
useMemo
useMemo
フックを使用すると、その計算の結果をメモ化し、依存関係の1つが変更された場合にのみ再計算できます。
const memoized = useMemo(
() => expensive(a, b),
// Only re-run the expensive function when any of these
// dependencies change
[a, b]
);
useMemo
内で副作用のあるコードを実行しないでください。副作用はuseEffect
に属します。
useCallback
useCallback
フックを使用すると、依存関係が変更されない限り、返される関数が参照的に等しくなるようにすることができます。これは、参照の等価性に依存して更新をスキップする(例:shouldComponentUpdate
)子コンポーネントの更新を最適化するために使用できます。
const onClick = useCallback(
() => console.log(a, b),
[a, b]
);
豆知識:
useCallback(fn, deps)
はuseMemo(() => fn, deps)
と同等です。
useRef
関数コンポーネント内でDOMノードへの参照を取得するには、useRef
フックがあります。createRefと同様に機能します。
function Foo() {
// Initialize useRef with an initial value of `null`
const input = useRef(null);
const onClick = () => input.current && input.current.focus();
return (
<>
<input ref={input} />
<button onClick={onClick}>Focus input</button>
</>
);
}
REPL で実行
useRef
とcreateRef
を混同しないように注意してください。
useContext
関数コンポーネントでコンテキストにアクセスするには、高階コンポーネントやラッパーコンポーネントを使用せずに、useContext
フックを使用できます。最初の引数は、createContext
呼び出しから作成されたコンテキストオブジェクトである必要があります。
const Theme = createContext('light');
function DisplayTheme() {
const theme = useContext(Theme);
return <p>Active theme: {theme}</p>;
}
// ...later
function App() {
return (
<Theme.Provider value="light">
<OtherComponent>
<DisplayTheme />
</OtherComponent>
</Theme.Provider>
)
}
REPL で実行副作用
副作用は、多くの最新のアプリの中核をなしています。APIからデータを取得するのか、ドキュメントに影響を与えるトリガーを作成するのかに関わらず、useEffect
がほぼすべてのニーズに適合することがわかります。これは、コンポーネントのライフサイクルではなく、効果的に考えるように思考を改革するフックAPIの主な利点の1つです。
useEffect
名前が示すように、useEffect
はさまざまな副作用をトリガーする主な方法です。必要に応じて、効果からクリーンアップ関数を返すこともできます。
useEffect(() => {
// Trigger your effect
return () => {
// Optional: Any cleanup code
};
}, []);
ブラウザのタブのアドレスバーに表示されるように、ドキュメントのタイトルを反映するTitle
コンポーネントから始めます。
function PageTitle(props) {
useEffect(() => {
document.title = props.title;
}, [props.title]);
return <h1>{props.title}</h1>;
}
useEffect
の最初の引数は、効果をトリガーする引数なしのコールバックです。ここでは、タイトルが実際に変更された場合にのみトリガーすることを目的としています。同じままである場合に更新しても意味がありません。そのため、2番目の引数を使用して依存関係配列を指定しています。
しかし、より複雑なユースケースもあります。マウント時にデータの購読が必要で、アンマウント時に購読解除が必要なコンポーネントを考えてみてください。これもuseEffect
で実現できます。クリーンアップコードを実行するには、コールバックで関数を返すだけです。
// Component that will always display the current window width
function WindowWidth(props) {
const [width, setWidth] = useState(0);
function onResize() {
setWidth(window.innerWidth);
}
useEffect(() => {
window.addEventListener('resize', onResize);
return () => window.removeEventListener('resize', onResize);
}, []);
return <p>Window width: {width}</p>;
}
REPL で実行クリーンアップ関数はオプションです。クリーンアップコードを実行する必要がない場合は、
useEffect
に渡されるコールバックで何も返す必要はありません。
useLayoutEffect
署名はuseEffectと同一ですが、コンポーネントの差分検出が完了し、ブラウザが描画できるようになるとすぐに実行されます。
useErrorBoundary
子コンポーネントでエラーが発生した場合、このフックを使用してエラーをキャッチし、ユーザーにカスタムエラーUIを表示できます。
// error = The error that was caught or `undefined` if nothing errored.
// resetError = Call this function to mark an error as resolved. It's
// up to your app to decide what that means and if it is possible
// to recover from errors.
const [error, resetError] = useErrorBoundary();
監視目的では、サービスにエラーを通知することが非常に役立つことがよくあります。そのため、オプションのコールバックを利用し、それをuseErrorBoundary
の最初の引数として渡すことができます。
const [error] = useErrorBoundary(error => callMyApi(error.message));
完全な使用例は以下のようになります。
const App = props => {
const [error, resetError] = useErrorBoundary(
error => callMyApi(error.message)
);
// Display a nice error message
if (error) {
return (
<div>
<p>{error.message}</p>
<button onClick={resetError}>Try again</button>
</div>
);
} else {
return <div>{props.children}</div>
}
};
過去にクラスベースのコンポーネントAPIを使用していた場合、このフックは基本的にcomponentDidCatchライフサイクルメソッドの代替手段です。このフックはPreact 10.2.0で導入されました。
ユーティリティフック
useId
このフックは、各呼び出しに対して一意の識別子を生成し、サーバー側とクライアント側の両方でレンダリングする際に、これらの識別子が一貫性を保つことを保証します。一貫性のあるIDの一般的なユースケースはフォームであり、そこで<label>
要素はfor
属性を使用して、特定の<input>
要素と関連付けられます。ただし、useId
フックはフォームのみに限定されず、一意のIDが必要な場合にいつでも使用できます。
フックの一貫性を確保するには、サーバーとクライアントの両方でPreactを使用する必要があります。
完全な使用例は以下のようになります。
const App = props => {
const mainId = useId();
const inputId = useId();
useLayoutEffect(() => {
document.getElementById(inputId).focus()
}, [])
// Display a nice error message
return (
<main id={mainId}>
<input id={inputId}>
</main>
)
};
このフックはPreact 10.11.0で導入され、preact-render-to-string 5.2.4が必要です。