ロードマップ
React 16.8 で追加された機能であるReactのHooks
について書いてあります。
書きながら学ぶ React Hooks 入門シリーズとして書き下ろしました。
はじめに
Reactの組み込みフックであるContext
とuseContext
の説明をします。
また、useContext
は、ReactのContext
と併用するので、Context
の解説もします。
Contextとは
useContext
は、Context
というReactの仕組みを利用するために必要なフックです。
なので、まずはContext
について説明します。
Context
とは...と言っても様々な言い方ができるでしょう。
- 「状態」と「状態を変更するメソッド」を、propsを用いず、アプリケーション全体で取り回すことができるやつ
- Propsを利用せずに、様々な階層のコンポーネントに値を共有するReactのAPI
などなどですね。図で説明しましょう。
本をReactで表現するとします。
ホントは、もっといろいろなコンポネントがありえますが、今回は、Bookのデータ(仮にbookData
とする)を<Page />
と<Title />
で、使いたいとします。
すると、このようにpropsをバケツリレーして渡す方法もあります。ですが、<Body />
と<Cover />
はそんなデータを使用しない。とか、もっと多階層で中間のコンポネントはbookData
をまったく使わないので、バケツリレーで渡す意味がない時ありますよね?
もしくは、グローバルにどこでも取りまわせるstateを持ちたい時がありますよね?
そんな時は、Context
の出番です。
Context
がデータストアの役割をはたし、propsを使わず直接bookData
を下層階層のコンポネントが使うことができるようにしたものです。
useContext
は、Context
の機能をさらにシンプルに使うことができるやつです。
Contextを利用するために必要なもの
- Contextオブジェクト:
React.createContext
というReactのAPIの戻り値 - Provider: Contextオブジェクトが保持しているコンポネント
- Consumer: Contextオブジェクトから値を取得しているコンポネント
サンプルコード
基本の状態は以下にします。(Bookを表すには雑すぎますが...ツリー構造さえわかればOKなので許してください)
Contextを利用して、下層コンポネントに、データを受け渡します。
以下の順番で書いていきます。
- createContext(
BookContext
)してexportしておく(Book コンポネントで実装) - Providerコンポネントを作成し、valueにオブジェクト(state)をセットします(Book コンポネントで実装)
- 下層コンポネントで、先ほど作成したBookContextをimportします(Title コンポネントで実装)
- importしたBookContextを使ってconsumerを作成(Title コンポネントで実装)
- Consumerコンポネント内でbook state の中身にアクセスできます
赤枠が、変更点です👇
実行結果はこんな感じです。
propsで上の階層から渡していないのに、下階層でcreateContextしたデータが引っ張れていることがわかります。
それでは次に、useContext
を使っていきましょう。
useContextとは
useContext
を使った場合でも、Providerを使って値を渡す点は同じです。
構文はこんな感じです。
const Contextオブジェクトの値 = useContext(Contextオブジェクト)
Contextオブジェクトから、取得できる値は、Contextオブジェクトが保持しているProviderのvalueプロパティに指定された値です。
サンプルコード
<Cover />
とその子供の<Title />
は、createContextされたBookContext
とAuthorContext
コンテキストをPrivide
されているので、それぞれのデータを引っ張ることができます。このように、複数のコンテキストを扱うことも可能です。
サンプルコード
import React, { createContext, useState } from "react";
import Cover from "./Cover";
import Body from "./Body";
const bookData = {
author: "ryosuketter",
title: "how to use React context",
isbn: 12345,
yearOfPublication: 2021
};
const authorData = {
name: "ryosuketter",
age: 35,
gender: "male"
};
export const BookContext = createContext();
export const AuthorContext = createContext();
const Book = () => {
const [book, setBook] = useState(bookData);
const [author, setAuthor] = useState(authorData);
return (
<>
<BookContext.Provider value={book}>
<AuthorContext.Provider value={author}>
<Cover />
</AuthorContext.Provider>
<Body />
</BookContext.Provider>
</>
);
};
export default Book;
import React, { useContext } from "react";
import { BookContext, AuthorContext } from "../../Book";
const Title = () => {
const book = useContext(BookContext);
const author = useContext(AuthorContext);
return (
<div>
<p>this is 「{book.title}」</p>
<p>author is {author.name}</p>
<p>age is {author.age}</p>
</div>
);
};
export default Title;
特定のコンポネントで、Contextの追加や上書きをしたデータを作成して使うことも可能です。
サンプルコード
// 上と同じ
import Reac, { useContext } from "react";
import { BookContext } from "../../Book";
const Page = () => {
const book = useContext(BookContext);
const customedBookContext = {
...book,
author: "ryosuke",
publisher: "Qiita publications"
};
return (
<div>
<p>this is page</p>
<p>author is {customedBookContext.author}</p>
<p>publisher is {customedBookContext.publisher}</p>
</div>
);
};
export default Page;
上2つのコードの実行結果
Context利用時の注意点(Context更新時に不要な再レンダーを招く)
Contextは使い方によっては、パフォーマンスの問題を引き起こす可能性があります。
なぜから、Provider内のすべてのConsumerは、Providerのvalueプロパティが更新するたびに再レンダリングするからです。
特に、以下の場合は注意です
- 再レンダリングされる Consumer の数が多い場合
- Consumerの子コンポネントのレンダリングコストが高い場合
不要な再レンダリングを防ぐ方法は、次の3つです。
- Contextオブジェクトの分割
- React.memoの利用
- useMemoの利用
ためにし、1つ実際にやってみましょう。
useMemoの利用をすることで不要な再レンダリングを防ぐ方法
仮に、本の著者の情報に対して、いいねボタン(+1カウントアップ)があるとします。
変更を行うのは/components/Book.js
と/components/Cover/Title.js
です(他のコードは、これまでと変わりません)。
Provider内のすべてのConsumerは、Providerのvalueプロパティが更新するたびに再レンダリングされるので、
下記のままでは、+1カウントアップ されるたびに、<Title />
コンポネントも再レンダリングされます。
サンプルコード
import React, { createContext, useState } from "react";
import Cover from "./Cover";
import Body from "./Body";
const bookData = {
author: "ryosuketter",
title: "how to use React context",
isbn: 12345,
yearOfPublication: 2021
};
const authorData = {
name: "ryosuketter",
age: 35,
gender: "male",
like: 0
};
export const BookContext = createContext();
export const AuthorContext = createContext();
const Book = () => {
const [book] = useState(bookData);
const [author, setAuthor] = useState(authorData);
return (
<>
<BookContext.Provider value={book}>
<AuthorContext.Provider value={author}>
<Cover />
</AuthorContext.Provider>
<Body />
</BookContext.Provider>
<p>Like: {author.like}</p>
<button onClick={() => setAuthor({ ...author, like: author.like + 1 })}>
+
</button>
</>
);
};
export default Book;
import React, { useContext } from "react";
import { BookContext, AuthorContext } from "../../Book";
const Title = () => {
const book = useContext(BookContext);
const author = useContext(AuthorContext);
console.log("render");
return (
<div>
<p>this is 「{book.title}」</p>
<p>author is {author.name}</p>
<p>age is {author.age}</p>
</div>
);
};
export default Title;
useMemoの利用をすることで不要な再レンダリングを防ぐ方法は、以下
サンプルコード
import React, { useContext, useMemo } from "react";
import { BookContext, AuthorContext } from "../../Book";
const Title = () => {
const book = useContext(BookContext);
const author = useContext(AuthorContext);
return useMemo(() => {
console.log("render");
return (
<div>
<p>this is 「{book.title}」</p>
<p>author is {author.name}</p>
<p>age is {author.age}</p>
</div>
);
}, [book.title, author.name, author.age]);
};
export default Title;
useMemoの[deps]
に、[book.title, author.name, author.age]
を入れることで、こいつらが更新されない限り、メモ化されたデータが返されて、再レンダリングを防いでいます。
ぜひ、試しながらやってみてください。
今回は以上です。
参考