blog

Reactのフックは、一般的に使用されるフックと簡単な分析の原理

コンポーネント間の状態ロジックを再利用することは困難です。 クラスコンポーネントでは、多くのライフサイクル関数があり、各関数で対応することを行う必要があります。この方法の痛い点は、ロジックがあちこちに...

Aug 7, 2020 · 10 min. read
シェア

クラス・コンポーネントの問題点

  1. コンポーネント間でステート・ロジックを再利用することは困難です。
  2. クラスコンポーネントでは、多くのライフサイクル関数があり、それぞれの関数で対応する処理を行う必要があります。この方法の難点は、ロジックがあちこちに散らばってしまい、開発者はコードのメンテナンスに気を取られ、またコードのロジックを理解するのが非常に難しいことです。
  3. また、クラスベースのコンポーネントは最適化が困難です。

クックは上記の問題を解決する良い方法です。

hooks

フックは不思議なものではありません。より正確には、「ステートフルな機能コンポーネント」です。

useState

レンダリングされるたびに、関数は再実行されます。関数の実行が終了するたびに、すべてのメモリが解放されます。useStateは、クロージャを使用して関数内の機能コンポーネントの現在の状態を作成します。そして、その状態を変更するメソッドを提供します。

各JSモジュールは、独立したスコープとみなすことができ、コードが実行されると、辞書スコープは、実行コンテキストを作成するには、モジュールの内部では、関数の作成は、外部参照によってアクセスすることができる場合、それはクロージャの作成のための条件を提供し、関数は、モジュール内の他の変数にアクセスするために外部から実行される限り、クロージャが生成されます。

もう一つ例を挙げましょう。以下のコードでStateというモジュールを定義します:

// state.js let state = null; export const useState = (value: number) => { // 最初に呼び出すときは、初期値がないので、値を代入するために渡した初期値を使う。 state = state || value; function dispatch(newValue) { state = newValue; // このメソッドがページレンダリングをトリガーするとする。 render(); } return [state, dispatch]; }

他のモジュールで紹介・使用されています。

import React from 'react'; import {useState} from './state'; function Demo() { // 変数を定義するには、配列の分解メソッドを使用する。 const [counter, setCounter] = useState(0); return ( <div onClick={() => setCounter(counter + 1)}>hello world, {counter}</div> ) } export default Demo();

実行コンテキストのstateとstateに作成された関数useStateがデモで実行され、stateの変数オブジェクトにアクセスすると、クロージャが作成されます。クロージャの性質上、stateモジュール内のstate変数は永続化されます。そのため、Demo関数が再び実行されると、前回のDemo関数の実行終了時のstateの値も取得されます。これが、関数コンポーネントに内部状態を持たせるReact Hooksの基本原理です。

クラスでもフックでも、状態の変更は非同期です。

setState(obj)objアドレスが変更されていない場合、Reactはデータが変更されていないとみなす。
// エラーコード const [user,setUser] = useState({name:'lifa', age: 18}) const onClick = () => { // 元の参照アドレスのname属性を変更しても動作しない。 user.name = 'jack' //あなたは、変更後にsetUserを介してビューの更新をトリガすることはできませんが、他のトリガは、ビュー上のジャックとして名前を表示する。 setUser(user) } // 正しいコード const [user,setUser] = useState({name:'lifa', age: 18}) const onClick = () => { // Reactは参照を生成する setUser({ ...user, name: 'jack' }) }

上記の機能を使用すると、param変数はDOMに影響を与えず、データを要求するためだけのものであり、非同期変数として定義するのは賢明ではありません。同期変数として定義する方が良いでしょう。

export default function AsyncDemo() { const [param] = useState<Param>({}); const [listData, setListData] = useState<ListItem[]>([]); function fetchListData() { // @ts-ignore listApi(param).then(res => { setListData(res.data); }) } function searchByName(name: string) { param.name = name; fetchListData(); } return [ <div>data list</div>, <button onClick={() => searchByName('Jone')}>search by name</button> ] }

useEffect

useEffect は、ほとんどの副作用を処理するために使用されます。コールバック関数は、レンダリングが実行された後に呼び出され、ブラウザのレンダリングを妨げないようにします。これは、レンダリング時に同期的に実行される componentDidMount や componentDidUpdate とは異なります。

使用の特徴効果:

let deps; // deps useEffectの最後の依存関係を記録する function useEffect(callback, depsArray) { const hasNoDeps = !depsArray; // 依存関係が存在しない場合 const hasChangedDeps = deps ? !depsArray.every((el, i) => el === deps[i]) // 依存関係は、両方のケースで全く同じであるかどうか : true; /* 依存関係が存在しない場合、または依存関係が変更された場合*/ if (hasNoDeps || hasChangedDeps) { callback(); deps = depsArray; } }

useLayoutEffect

ほとんどの場合、副作用はuseEffectを使用して処理できますが、副作用がDOMに関連する場合は、useLayoutEffectを使用する必要があります。useLayoutEffectの副作用は、DOMが更新された後に同期的に実行されます。

JSスレッドとブラウザのレンダリングスレッドは互いに排他的であるため、メモリ上の実際のDOMが変更されても、ブラウザはすぐに画面にレンダリングせず、対応するライフサイクルメソッドを同期的に実行することで、この時点で作業を終了します。ここで、componentDidMount、componentDidUpdate、useLayoutEffect( create, deps)のcreate関数が同期的に実行されます。create, deps) はすべてこの段階で同期的に実行されます。コミット段階が完了すると、ブラウザは変更された DOM を画面にレンダリングし、react は 1 回のリフローと再描画のコストで更新が必要なすべての DOM ノードの更新を完了します。ブラウザがレンダリングを終えると、ブラウザはreactにアイドルフェーズに入ったことを通知し、reactは自身のスケジューリングキューにあるタスクの実行を開始します。

function App() { const [width, setWidth] = useState(0); useLayoutEffect(() => { const title = document.querySelector('#title'); const titleWidth = title.getBoundingClientRect().width; if (width !== titleWidth) { setWidth(titleWidth); } }); return <div> <h1 id="title">hello</h1> <h2>{width}</h2> </div> }

useReducer

useStateの代替です。これは => newState という形式のリデューサを受け取り、現在の状態とそれに付随するディスパッチメソッドを返します。

ステートロジックが複雑で複数の子値を含む場合や、次のステートがステートに依存している場合など、useStateよりもuseReducerの方が良いシナリオもあります。また、useReducerを使用すると、コールバック関数の代わりに子コンポーネントにディスパッチを渡すことができるため、深い更新をトリガするコンポーネントのパフォーマンスが最適化されます。

function init(initialCount) { return {count: initialCount};} function reducer(state, action) { switch (action.type) { case 'increment': return {count: state.count + 1}; case 'decrement': return {count: state.count - 1}; case 'reset': return init(action.payload); default: throw new Error(); } } function Counter({initialCount}) { const [state, dispatch] = useReducer(reducer, initialCount, init); return ( <> Count: {state.count} <button onClick={() => dispatch({type: 'reset', payload: initialCount})}> Reset </button> <button onClick={() => dispatch({type: 'decrement'})}>-</button> <button onClick={() => dispatch({type: 'increment'})}>+</button> </> ); }

useContext

コンテキストオブジェクトを受け取り、そのコンテキストの現在の値を返します。コンテキストの現在値は、現在のコンポーネントに最も近い上位 コンポーネントの値プロップによって決定されます。

const themes = { light: { foreground: "#", background: "#eeeeee" }, dark: { foreground: "#ffffff", background: "#" } }; const ThemeContext = React.createContext(themes.light); function App() { return ( <ThemeContext.Provider value={themes.dark}> <Toolbar /> </ThemeContext.Provider> ); } function Toolbar(props) { return ( <div> <ThemedButton /> </div> ); } function ThemedButton() { const theme = useContext(ThemeContext); return ( <button style={{ background: theme.background, color: theme.foreground }}> I am styled by theme context! </button> ); }

useRef

useRef() と独自の {current: ...} を作成することの唯一の違いは、useRef はレンダリングのたびに同じ ref オブジェクトを返すことです。 useRef と {current: ...} オブジェクトの唯一の違いは、useRef はレンダリングのたびに同じ ref オブジェクトを返すということです。

function TextInputWithFocusButton() { const inputEl = useRef(null); const onButtonClick = () => { // `current` DOMに取り付けられたテキスト入力要素を指す inputEl.current.focus(); }; return ( <> <input ref={inputEl} type="text" /> <button onClick={onButtonClick}>Focus the input</button> </> ); }

useRefは、refオブジェクトの内容が変更されても通知しません。.current プロパティを変更しても、コンポーネントの再レンダリングは行われません。ReactがDOMノードにrefをバインドまたはアンバインドしたときに何らかのコードを実行したい場合は、コールバックrefを使用する必要があります。

useMemo

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

メモした値を返します。

create" 関数と依存関係の配列を useMemo に引数として渡すと、依存関係が変更されたときにのみ、メモ化された値を再計算します。この最適化により、レンダリングのたびにオーバーヘッドを計算する必要がなくなります。

useMemo に渡された関数は、レンダリング中に実行されることを覚えておいてください。この関数の内部でレンダリングに関連しない操作を行わないでください。副作用などの操作は useEffect でカバーされ、useMemo ではカバーされません。

パフォーマンスの最適化としてMemoを使用することはできますが、セマンティックな保証とは考えないでください。将来、Reactは以前のメモ化された値を「忘れ」、次のレンダリングで再計算することを選択するかもしれません。

useCallback

const memoizedCallback = useCallback( () => { doSomething(a, b); }, [a, b], );

メモ化されたコールバック関数を返します。

インラインコールバック関数と依存関係の配列を useCallback の引数として渡すと、そのコールバック関数のメモ化されたバージョンが返されます。これは、最適化されたサブコンポーネントにコールバック関数を渡し、必要でないレンダリングを避けるために参照等価を使用する場合に便利です。

useCallback(fn, deps) useMemoに相当する=> fn, deps)

Hooksこれは単なる

コードキーは

  1. 更新時には、memoizedStateからレコードの値を順番に取り出します。
let memoizedState = []; // hooks この配列に格納されている let cursor = 0; // 現在のmemoisedState添え字 function useState(initialValue) { memoizedState[cursor] = memoizedState[cursor] || initialValue; const currentCursor = cursor; function setState(newState) { memoizedState[currentCursor] = newState; render(); } return [memoizedState[cursor++], setState]; // 現在の状態を返し、カーソルに1を追加する。 } function useEffect(callback, depArray) { const hasNoDeps = !depArray; const deps = memoizedState[cursor]; const hasChangedDeps = deps ? !depArray.every((el, i) => el === deps[i]) : true; if (hasNoDeps || hasChangedDeps) { callback(); memoizedState[cursor] = depArray; } cursor++; }

useStateの使用例を参照してください:

function RenderFunctionComponent() { const [firstName, setFirstName] = useState("Rudi"); const [lastName, setLastName] = useState("Yardley"); return ( <Button onClick={() => setFirstName("Fred")}>Fred</Button> ); }

1.初期化

初期化時には、ステートとセッターの2つの配列が作成され、カーソルの位置はゼロに設定されます。

2.最初のレンダリング

useStateの呼び出しは、最初にレンダリングされるときに、set関数をsetters配列に入れ、初期状態をstate配列に入れます。

3.その後のレンダリング

再レンダリングされるたびに、カーソルは0にリセットされ、state関数とset関数が対応する配列から読み込まれます。

4 イベント処理

set関数が呼び出されるたびに、カーソルによって決定される状態配列の対応する状態値が変更されます。

同じ memoizedState を共有し、同じ順序を共有します。

実際のReactの実装

基本的に配列で使えるフックを実装し、フックの仕組みを理解することはできますが、Reactでの実装方法にはいくつかの違いがあります。配列の代わりに、Reactは単一のリンクリストのようなものを使用します。配列の代わりに、Reactは単一のリンクリストのようなものを使用し、すべてのフックは次から順番にリンクされています。

type Hooks = { memoizedState: any, // 現在レンダリングされているノードを指す ファイバー baseState: any, // 各ディスパッチ後にすでに newinitialStateを初期化する。 baseUpdate: Update<any> | null,// 更新が必要なUpdateは、各更新の後、reactがレンダリングエラーの端にデータをさかのぼることができるように、最後の更新に割り当てられる。 queue: UpdateQueue<any> | null,// UpdateQueue next: Hook | null, // link 次のフックに行くには、次のフックを介して各フックを接続する。 } type Effect = { tag: HookEffectTag, // effectTag 現在のフックがライフサイクルのどの段階にあるかを示すには、次のようにする。 create: () => mixed, // コールバックを初期化する destroy: (() => mixed) | null, // コールバックをアンインストールする deps: Array<mixed> | null, next: Effect, // }; memoizedStateカーソルはどこに存在するのか?それはどのように各機能のコンポーネントに対応している?

反応によってコンポーネントツリーが生成され、ツリーの各ノードはコンポーネントに対応し、フックデータはコンポーネント情報としてこれらのノードに格納されます。

Read next

plumeログがjava.net.UnknownHostExceptionの解決時にkafkaキューに接続している。

最近、plumelogを使ってロギングを行っているときに、kafkaを使ってメッセージキューを行うと、以下のエラーが発生することがわかりました。

Aug 7, 2020 · 2 min read