20200805のReactに関する記事は5件です。

きじくんの備忘録 ~React編①

はじめに

これは,フロントエンジニアを目指している僕が勉強の過程をメモしていく記事です.
一応,僕自身の為に書いていますが,誰かの役に立ったら幸いです!

今回はJavaScriptライブラリであるReactの備忘録第1回です.

コンポーネント

コンポーネントとは見た目機能を足し合わせた,言わばWebサービスのページの部品に当たるもの.Reactはこのコンポーネントを再利用することで,コード量を減らすことができる.

仮想DOM

React内で管理するDOM
普通のDOM

sample.js
document.getElementById('foo').innerText='bar'

仮想DOM

sample.jsx
render(
  <div id='foo'>bar</div>
)

JSX

JavaScript内部でHTMLを書ける.

sample.jsx
ReactDOM.render(
  <div className={foo}>
    <h1>bar</h1>
  </div>
)

JSXの基礎文法

sample.jsx
import React from 'react';

// HTMLと同様に書ける.ただし,divタグで全体を囲うなどのしないとエラーが出る.
const App=()=>{
  return(
    <div className={foo}>
      <h1>bar</h1>
    </div>
  );
};

// {}内に変数や関数を埋め込める.
const foo = '<p>FOO!</p>'
const App=()=>{
  return(
    <div id='bar' className={baz}>
      {foo}
    </div>
  );
};

// 空要素は閉じる
const App=()=>{
  return(
    <div className={foo}>
      <input type="text" id="bar"/>
      <img src="/img/baz.png" />
    </div>
  );
};

あとがき

今回はReactの要素のJSXについて主に書きました.今後も少しずつ,Reactに関する備忘録を書いていくのでよろしくお願いします.

参考文献

トラハックのエンジニア学習ゼミ【とらゼミ】 - 日本一わかりやすいReact入門

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

React HooksでStackな構造を扱うカスタムフック

useStackRef.ts

スタック構造を持ったデータをスッキリ扱いたかったので作った。
作ってみてわかったのは、これはもうほとんどArrayにスタックのインターフェースを被せただけものになったということ。
それでも使う側はだいぶスッキリするから、ないよりはあった方が良くはある。

useStackRef.ts
import { useRef } from "react";

export interface Stack<T> {
    value: T | null
    size: number
    push: (t: T) => number
    pop: () => T
    reset: () => void
    clear: () => void
}

export const useStackRef = <T>(init: T | T[] | (() => T) | (() => T[]) = []): Stack<T> => {
    const initial: T[] = ((value) => Array.isArray(value) ? value : [value])(init instanceof Function ? init() : init)
    const stackRef = useRef<T[]>(initial)

    const push = (t: T): number => stackRef.current.push(t)
    const pop = (): T => stackRef.current.pop() || null
    const reset = (): void => { stackRef.current = initial }
    const clear = (): void => { stackRef.current = [] }

    return {
        value: stackRef.current.slice(-1)[0] || null,
        size: stackRef.current.length,
        push, pop, reset, clear
    };
};

ちなみに、useRefでなくuseStateのスタックを作って紹介している記事はこちら。
React Hooksのカスタムフックが実現する世界 - オブジェクト指向とOSS

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

WordPress製ホームページをGatsby+microCMS+Netlifyに移行してみた

プライベートでやっているバンドのホームページをレンタルサーバー+WordPressで運営していたのですが、最近Gatsby+microCMS+NetlifyのいわゆるJAMstack構成のホームページにリプレースしました。

ホームページ:男子校 Official Web Site

概要

今までかれこれ5年くらいレンタルサーバー+WordPressで運営してきていたのですが、

  • サーバー代が毎年かかる
  • 表示が重い
  • 「固定ページやフォームをちょっとだけ動的にしたい」みたいなことが難しい
  • プラグイン・バージョンのアップデートといった管理が面倒

といった点が課題でした。

これらの課題を解消したく、また最近気になっていたGatsbyを使ってみたかったのもあってホームページをリプレースしてみました。

構成

TypeScript+React+Gatsbyでコードを作成、microCMSでデータを管理してNetlifyでホスティングするという構成にしました。いわゆるJAMstackと呼ばれている構成で、動的なWebサーバーを必要とせず静的コンテンツを高速配信することができます。

Gatsby

React製のSSG(静的サイトジェネレーター)。MarkdownファイルやHeadless CMSと組み合わせてブログ等のサイトを簡単に作成でき、かつ爆速なサイトが簡単に作れると評判です。
「静的」とは言っていますがReactの機能はフルに使えるので、フロントエンドをフルに活用したサイトを作ることができます。

microCMS

日本製のHeadless CMS。
個人的には管理画面がシンプルで一番使いやすそうと感じたので採用しました。特にバンドのホームページは自分以外も更新するので、直感的に記事を作成できるのは大きなメリットです。もちろん全て日本語なのもGood。
Gatsbyには対応プラグインがあるので、これを使用することでGraphQLクエリで簡単にデータを取得できます。
無料でも10個までAPIを作成できるので、ブログ記事以外にも曲情報やライブ情報といったAPIを作成してホームページ内で利用しています。

Netlify

デプロイ先の静的ホスティングサービス。静的サイト・サーバーレスに特化したサービスとなっていて、以下のような機能を提供してくれます。

  • GitHubやHeadless CMSと連携した自動ビルド(プライベートリポジトリでも可)
  • フォーム機能
  • AWS Lambdaベースのfunctions機能

microCMSにはNetlifyとWebhook連携できる機能があるので、microCMSで記事の作成・更新時にNetlifyに通知し、更新後のコンテンツで自動ビルドすることができます。

効果

リプレースによって上にあげた課題がかなり改善しました。

  • サーバー代が毎年かかる
    • → Netlify / microCMSともにに無料枠で十分なのでサーバー代は0になり、ドメイン代だけで済むようになった
  • 表示が重い
    • → GatsbyやNetlify CDNのおかげでかなり爆速に
  • 「固定ページやフォームをちょっとだけ動的にしたい」みたいなことが難しい
    • → Reactをフルに利用できるので動的処理も自由に入れられる
      • 例えば「ライブ情報から、未来の日付のライブだけ予約できるフォームを自動で作る」のようなこともWordPressでは難しかったのが簡単にできた
  • プラグイン・バージョンのアップデートといった管理が面倒
    • → GitHub / npmで一括管理1
      • セキュリティアップデートなんかはGitHubが自動でプルリクを生成してくれたりする

Gatsbyの使い方もReactが分かっていれば割ととっつきやすく2、Gatsbyを初めて触ってからだいたい1ヶ月くらいでリプレースできました。

ポイント

主にGatsbyまわりのちょっとしたポイントやハマった点をいくつか紹介しようと思います。
(具体的な環境構築の手順等は今回はあまり触れません)

デザインライブラリ

1からデザインを作成するのは結構大変だったので、Reactのマテリアルデザインライブラリmaterial-uiを使用しました。
Gatsbyで使用する場合は、gatsby-theme-material-uiプラグインを使用すればよいです。
src/gatsby-theme-material-ui-top-layout/theme.tsを作成することで以下のようにテーマをカスタマイズできます。

src/gatsby-theme-material-ui-top-layout/theme.ts
import { createMuiTheme } from '@material-ui/core'
import { indigo } from '@material-ui/core/colors'

const theme = createMuiTheme({
  palette: {
    primary: { main: '#fbfb99' },
    secondary: { main: indigo.A400 },
  },
  props: {
    MuiLink: { color: 'secondary' },
    MuiTextField: { variant: 'outlined', color: 'secondary' },
  },
})

export default theme

TypeScript対応

GatsbyはデフォルトでTypeScript対応していて、tsconfig.json等でカスタマイズすることもできます。
また、gatsby-plugin-graphql-codegenプラグインを使用することでGraphQLの取得データにも型補完が効いてかなり開発しやすくなります。
gatsby-plugin-graphql-codegenを使用する場合は以下の設定が個人的におすすめです。

Gatsby+TypeScriptを快適にするためのgatsby-plugin-graphql-codegenの設定

microCMSデータ取得

GatsbyとmicroCMSを連携するには先ほど述べた通り対応プラグインgatsby-source-microcmsを利用できます。
microCMSはコンテンツごとに複数のAPIを作成できますが、その場合は以下のようにgatsby-configでプラグインを複数回読み込むことでそれぞれデータ取得できます。

gatsby-config.ts
import { ITSConfigFn } from 'gatsby-plugin-ts-config'

const gatsbyConfig: ITSConfigFn<'config'> = ({ projectRoot }) => ({
  plugins: [
    // 中略
    {
      resolve: 'gatsby-source-microcms',
      options: {
        apiKey: process.env.GATSBY_MICROCMS_API_KEY, // APIキーは環境変数で管理するのが良いです
        serviceId: process.env.GATSBY_MICROCMS_ENDPOINT,
        endpoint: 'index', // APIのエンドポイント
        format: 'object', // オブジェクト形式の場合
      },
    },
    {
      resolve: 'gatsby-source-microcms',
      options: {
        apiKey: process.env.GATSBY_MICROCMS_API_KEY,
        serviceId: process.env.GATSBY_MICROCMS_ENDPOINT,
        endpoint: 'blogs', // APIのエンドポイント
        format: 'list', // リスト形式の場合
        readAll: true, // リスト形式の場合デフォルトでは最新10件のみ取得するので全件取得したい場合はこれをtrueにする
      },
    },
    // ...
  ]
})
export default gatsbyConfig

microCMSのリッチエディタ(HTML)→JSX変換

microCMSで使用できるリッチエディタはHTMLをGUIエディタで作成できるのですが、APIでは生のHTML文字列として取得されます。
HTML文字列をReactでレンダリングするには

const BlogPost: React.FC<{ body: string }> = ({ body }) => (
  <div dangerouslySetInnerHTML={{ __html: body }} />
)

のようにすればよいのですが、これだとHTMLそのままで出力されてしまうのでスタイルを適用するのがやや大変です。特にmaterial-uiのようなReactコンポーネントのライブラリの場合はこのままだと難しいです。
このHTMLにmaterial-uiのデザインを適用させるために、rehype-reactというライブラリでHTML→JSXに変換しました。

まず、以下のように各タグ(以下はh1を想定)に対応するコンポーネントを作成します。

Typography.tsx
import React from 'react'
import { Box, Typography, Divider } from '@material-ui/core'

type Props = { id?: string; className?: string }

export const Chapter: React.FC<Props> = ({ children, className, ...props }) => (
  <Box mt={2} mb={1} className={className}>
    <Typography {...props} variant="h4" component="h2">
      {children}
    </Typography>
    <Divider />
  </Box>
)
// ... 略

そして、rehype-parseでHTMLをパース・rehype-reactでHTMLタグをJSXコンポーネントに変換するrender関数を作成します。

render.tsx
import React from 'react'
import unified from 'unified'
import parser from 'rehype-parse'
import rehypeReact from 'rehype-react'
import { Chapter } from '../components/Typography'

const processor = unified()
  .use(parser, { fragment: true })
  .use(rehypeReact, {
    createElement: React.createElement,
    components: {
      h1: Chapter,
      // ...以下タグとコンポーネントの対応を並べていく
    },
  })

// HTML -> JSX に変換する関数
export const render = (html: string) => processor.processSync(html).result

これをReactコンポーネント内で

const BlogPost: React.FC<{ body: string }> = ({ body }) => (
  <div>{render(body)}</div>
)

といった感じで使用することで、microCMSから取得したHTMLにmaterial-uiのスタイルを適用してレンダリングできます。

フォーム

Netlify Formsを利用すると静的サイトでもフォーム機能を利用することができます。
基本は<form>タグのアトリビュートにnetlifydata-netlify="true"と記載するだけで使用できるのですが、Gatsbyのようにビルド時にHTMLを生成する場合はNetlifyがビルド時に上記アトリビュートを読み取れないので、以下のようにname="form-name", value=(formのnameの値)と置いたinputタグを挿入する必要があります。

const Form: React.FC = () => (
  <form name="contact" method="POST" data-netlify="true">
    <input type="hidden" name="form-name" value="contact" /> {/* この行を追加 */}
    <p>
      <label>Your Name: <input type="text" name="name" /></label>   
    </p>
    <p>
      <label>Your Email: <input type="email" name="email" /></label>
    </p>
    <p>
      <label>Your Role: <select name="role[]" multiple>
        <option value="leader">Leader</option>
        <option value="follower">Follower</option>
      </select></label>
    </p>
    <p>
      <label>Message: <textarea name="message"></textarea></label>
    </p>
    <p>
      <button type="submit">Send</button>
    </p>
  </form>
)

また、ReCAPTCHAを利用する場合もformタグにdata-netlify-recaptcha="true"を記述したうえで<div data-netlify-recaptcha="true"></div>という空のdivタグを置けばNetlify側でReCAPTCHAを作成してくれるのですが、これもGatsbyでは読み取ってくれないので、react-google-recaptcha等のライブラリで実装する必要があります。

この辺は以下のようにラップしたフォームコンポーネントを作成しておくのが便利です。

Form.tsx
import React from 'react'
import { makeStyles, Button } from '@material-ui/core'
import ReCAPTCHA from 'react-google-recaptcha'

const useStyles = makeStyles(theme => ({
  button: { marginTop: theme.spacing(2) },
}))

type Props = {
  name: string
  action: string
  className?: string
}

const Form: React.FC<Props> = ({ children, name, action, className }) => {
  const classes = useStyles()
  return (
    <form
      name={name}
      action={action}
      method="POST"
      data-netlify="true"
      data-netlify-recaptcha="true"
      className={className}
    >
      <input type="hidden" name="form-name" value={name} />
      {children}
      <ReCAPTCHA sitekey={process.env.GATSBY_SITE_RECAPTCHA_KEY ?? ''} />
      <Button type="submit" variant="outlined" className={classes.button}>
        送信
      </Button>
    </form>
  )
}

export default Form

ビルド時・フォーム受信時等に通知する

Netlify Functionsにはビルドやフォーム受信等のイベントに反応して処理をトリガーする機能があります。
Trigger serverless functions on events

特定のファイル名(デプロイ成功時ならdeploy-succeeded.js等)でfunctionを作成することで、ビルド完了時にSlackに通知するといった連携機能を簡単に作成できます。

funcions/deploy-succeeded.js
// 例:デプロイ成功時にSlackに投稿する
// Slack App の作成方法等は省略
require('dotenv').config()
const { WebClient } = require('@slack/web-api')

const slack = new WebClient(process.env.SLACK_TOKEN)

exports.handler = async (event, context) => {
  const { deploy_time } = JSON.parse(event.body).payload
  try {
    await slack.chat.postMessage({
      text: `ホームページが更新されました! (ビルド時間 : ${deploy_time} 秒)`,
      channel: process.env.SLACK_CHANNEL_INFO,
    })
    return { statusCode: 200, body: 'ok' }
  } catch (error) {
    console.error(error)
    return { statusCode: 500, body: 'Internal server error' }
  }
}

まとめ

ホームページをWordPressからGatsbyに移行して幸せになったお話と、ちょっとしたポイント集でした(まとまりがない…)。
具体的な構築方法は今回はあまり触れなかったのですが、Gatsbyは最近話題のフレームワークということもあって結構ドキュメント・記事が充実している印象で、調べながら割と簡単にホームページを作れました(例:gatsby タグの記事一覧 - Qiita)。
公式ドキュメントは今のところ英語だけですが、日本語訳も現在作成中(2020/08/05現在)のようなので楽しみです。
gatsbyjs/gatsby-ja: Japanese translation of Gatsbyjs.org


  1. GatsbyもWordPressと似たようにプラグインで機能拡張していくのでそこまで劇的に改善した訳ではないですが、WordPressのダッシュボードにログインする度にアップデートしなきゃ…となるのが無くなっただけでかなり快適です 

  2. GraphQLが最初はとっつきにくいように思いますが、developmentサーバを立てたときに同時にGraphQLのGUI環境も立ち上がるので割と親切で分かりやすいです 

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

React 関数コンポーネントでスクロールイベントを実装するには?

Reactのfunctionコードでスクロールイベント等を実装すると、
イベント関数をuseCallbackでくくってメモ化しておかないとremoveEventListenerが働かないとか、
スクロールで使用するフラグはuseRefで再レンダリングされないようにする...など、
意外と気に掛ける点が多かったので、備忘録も込めてコードを載せておきます。

import React, {
  useState, useEffect, useRef, useCallback,
} from 'react'

const TestDom = () => {
  const [isDisplay, setIsDisplay] = useState(false)

  const isRunning = useRef(false) // スクロール多発防止用フラグ

  // リスナに登録する関数
  const isScrollToggle = useCallback(() => {
    if (isRunning.current) return
    isRunning.current = true
    const scrollTop = window.pageYOffset || document.documentElement.scrollTop
    requestAnimationFrame(() => {
      if (scrollTop > 100) {
        setIsDisplay(true)
      } else {
        setIsDisplay(false)
      }
      isRunning.current = false
    })
  }, [])

  // 登録と後始末
  useEffect(() => {
    document.addEventListener('scroll', isScrollToggle, { passive: true })
    return () => {
      document.removeEventListener('scroll', isScrollToggle, { passive: true })
    }
  }, [])

 // バツボタンでリスナ削除~ などはこのように
  const onClickClose = () => {
    document.removeEventListener('scroll', isScrollToggle, { passive: true })
    setIsDisplay(false)
  }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

React.memo, useCallback, useMemoを理解する

メモ化

React.memo, useCallback, useMemoに共通する目的はメモ化である。
メモ化とは広範な言い方をするとキャッシュのこと。

React.memo

ラップした関数コンポーネントの初回レンダー結果を記憶し、次回以降同じpropsが与えられた時に、記憶した結果を使用することで、無駄なレンダーをスキップできパフォーマンスを向上させることができる。

React.memoを使用しない場合

const CounterItem = ({ count, label }) => {
  console.log(`${label}がレンダリング`);
  return <div>{label}: {count}</div>;
};

const CounterList = () => {
  const [count1, setCount1] = useState(0);
  const increment1 = () => setCount1(c => c + 1);
  const [count2, setCount2] = useState(0);
  const increment2 = () => setCount2(c => c + 1);
  return (
    <>
      <button onClick={increment1}><CounterItem count={count1} label="左" /></button>
      <button onClick={increment2}><CounterItem count={count2} label="右" /></button>
    </>
  );
};

左右ボタンのどっちを押しても、CounterListコンポーネント内のstateが更新されるので左右のCounterItemコンポーネントを両方再レンダーしてしまう。左ボタンを押した場合は数字が変更される左のCounterItemコンポーネントのみレンダーしたい。右のコンポーネントは変更されてないのでレンダーの必要がない。

c.gif

React.memoを使用する場合

// React.memoで関数コンポーネントをラップ
const CounterItem = React.memo(({ count, label }) => {
  console.log(`${label}がレンダリング`);
  return <div>{label}: {count}</div>;
});

const CounterList = () => {
  const [count1, setCount1] = useState(0);
  const increment1 = () => setCount1(c => c + 1);
  const [count2, setCount2] = useState(0);
  const increment2 = () => setCount2(c => c + 1);
  return (
    <>
      <button onClick={increment1}><CounterItem count={count1} label="左" /></button>
      <button onClick={increment2}><CounterItem count={count2} label="右" /></button>
    </>
  );
};

propsが変更されたコンポーネントしかレンダーされない。

b.gif

useCallback

第一引数に渡したコールバック関数をメモ化する。第二引数に渡した値の配列が変更された場合のみ、コールバック関数が作り直される。React.memoと併用することでパフォーマンスを向上させることができる。

useCallbackを使用しない場合

const CounterItem = React.memo(({ count, label, onClick }) => {
  console.log(`${label}がレンダリング`);
  return <button onClick={onClick}><div>{label}: {count}</div></button>;
});


const CounterList = () => {
  const [count1, setCount1] = useState(0);
  const increment1 = () => setCount1(c => c + 1);
  const [count2, setCount2] = useState(0);
  const increment2 = () => setCount2(c => c + 1);
  return (
    <>
      <CounterItem count={count1} label="左" onClick={increment1} />
      <CounterItem count={count2} label="右" onClick={increment2} />
    </>
  );
};

左ボタンを押した場合、左右のCounterItemコンポーネントを両方再レンダーしてしまう。CounterItemコンポーネントをReact.memoでラップしてるのでpropsの変更がないコンポーネント(右ボタン)はレンダーをスキップして欲しいができていない。

d.gif

上記の問題は参照の同一性が関係している。

"a" === "a" // true
1 === 1 // true

const a = () => {}
const b = () => {} 
// 同じ処理であってもfalseが返る
a === b //false

stateの更新によりCounterListコンポーネントの再レンダリングが起きると、CounterListコンポーネント内のincrement1関数や increment2関数が再度定義される。再定義された関数は上記で示したように同じ参照ではなくなる。つまりCounterItemコンポーネントに渡す関数が同じ参照にはならない。よってReact.memoでラップしてもporpsの参照が違うのでレンダーがスキップされない。

useCallbackを使用する場合

useCallbackを使用することで上記の問題が解決される

const CounterItem = React.memo(({ count, label, onClick }) => {
  console.log(`${label}がレンダリング`);
  return <button onClick={onClick}><div>{label}: {count}</div></button>;
});

const CounterList = () => {
  const [count1, setCount1] = useState(0);
  const increment1 = useCallback(() => setCount1(c => c + 1), []);
  const [count2, setCount2] = useState(0);
  const increment2 = useCallback(() => setCount2(c => c + 1), []);
  return (
    <>
      <CounterItem count={count1} label="左" onClick={increment1} />
      <CounterItem count={count2} label="右" onClick={increment2} />
    </>
  );
};

うまくメモ化できていることが確認できる。

b.gif

useMemo

useCallbackはコールバック関数をメモ化したが、useMemoは値をメモ化する。
useMemoのユースケースは2つある。

useMemoのユースケース1

オブジェクトをコンポーネントのpropsとして渡す前に、オブジェクトをメモ化する

useMemoを使用しない場合

const CounterItem = React.memo(({ params }) => {
  console.log(`${params.label}がレンダリング`);
  return <button onClick={params.handler}><div>{params.label}: {params.value}</div></button>;
});

const CounterList = () => {
  const [count1, setCount1] = useState(0);
  const increment2 = useCallback(() => setCount2(c => c + 1), []);
  const [count2, setCount2] = useState(0);
  const increment1 = useCallback(() => setCount1(c => c + 1), []);

  const params1 = {
    label: '',
    value: count1,
    handler: increment1
  }

  const params2 = {
    label: '',
    value: count2,
    handler: increment2
  }

  return (
    <>
      <CounterItem params={params1} />
      <CounterItem params={params2} />
    </>
  );
};

上記のコードの場合も片方のボタンを押すと両方のボタンが再レンダリングされており、うまくメモ化できていない。左ボタンを押した際にはparams1のcount1が変更されるので左ボタンは再レンダリングされるべきだが、右ボタンの再レンダリングはスキップしたい。

d.gif

上記の問題も参照の同一性が関係している。オブジェクトの中身が同じでも同じ参照にはならない。

{'a': 1} === {'a': 1} // false
[1,2,3] === [1,2,3] // false

stateの更新によりCounterListコンポーネントの再レンダリングが起きると、params1とparams2が再度定義される。つまりCounterItemコンポーネントに渡すparamsが同じ参照にはならないため、例えuseCallbackでコールバック関数をメモ化しても、最終的に渡されるpropsの参照が違うのでレンダリングがスキップされない。

useMemoを使用する場合

上記の問題をuseMemoを使用することで解決できる。

const CounterItem = React.memo(({ params }) => {
  console.log(`${params.label}がレンダリング`);
  return <button onClick={params.handler}><div>{params.label}: {params.value}</div></button>;
});

const CounterList = () => {
  const [count1, setCount1] = useState(0);
  const increment2 = useCallback(() => setCount2(c => c + 1), []);
  const [count2, setCount2] = useState(0);
  const increment1 = useCallback(() => setCount1(c => c + 1), []);

  const params1 = useMemo(() => ({
    label: '',
    value: count1,
    handler: increment1
  }), [count1, increment1])

  const params2 = useMemo(() => ({
      label: '',
      value: count2,
      handler: increment2
  }), [count2, increment2])

  return (
    <>
      <CounterItem params={params1} />
      <CounterItem params={params2} />
    </>
  );
};

うまくメモ化できているのが確認できる。

b.gif

useMemoのユースケース2

重い計算結果をメモ化する。

useMemoを使用しない場合

// 何かしらの重い計算
const heavyCalc = (count) => {
  let i = 0
  while (i < 2000000000) i++
  return count
}

const Counter = () => {
  const [count1, setCount1] = useState(0);
  const increment1 = () => setCount1(c => c + 1);
  const [count2, setCount2] = useState(0);
  const increment2 = () => setCount2(c => c + 1);

  const result = heavyCalc(count1)

  return (
    <>
      <button onClick={increment1}><div>重い計算あり: {result}</div></button>
      <button onClick={increment2}><div>重い計算なし: {count2}</div></button>
    </>
  )
};

どちらのボタンを押しても、コンポーネント内の重い計算処理のせいで再レンダリングが遅れる。重い計算があるheavyCalc関数に渡しているのはcount1だけなので、右ボタンを押した場合はすぐ再レンダリングして欲しい。

c.gif

useMemoを使用する場合

上記の問題をuseMemoを使用することで解決できる。

// 何かしらの重い計算
const heavyCalc = (count) => {
  let i = 0
  console.log('重い計算開始')
  while (i < 2000000000) i++
  return count
}

const Counter = () => {
  const [count1, setCount1] = useState(0);
  const increment1 = () => setCount1(c => c + 1);
  const [count2, setCount2] = useState(0);
  const increment2 = () => setCount2(c => c + 1);

  const result = useMemo(() => heavyCalc(count1), [count1])

  return (
    <>
      <button onClick={increment1}><div>重い計算あり: {result}</div></button>
      <button onClick={increment2}><div>重い計算なし: {count2}</div></button>
    </>
  )
};

左ボタンを押した時は重い計算処理が走るが、右ボタンを押した場合はメモ化した値を返すため無駄な重い計算処理は走っていないので再レンダリングが早い。

a.gif

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む