20200629のReactに関する記事は6件です。

コンポーネント設計で悩んでいる人へ。簡単にコンポーネント設計できるESLint Plugin作りましたよ?‍?

この記事について

この記事は、筆者が作ったESLint プラグインの紹介記事なります。
「 なぜ作ったのか? 」「 コンポーネント設計とは何か? 」という議題に触れ、コンポーネント設計の難しさこのプラグインの有意性について語っています。
筆者は専門家では無いので間違っている所や認識のズレなどがあるかもしれませんが、もしあった場合は編集リクエストやコメントにて指摘してくれると嬉しいです。

後、現在この記事で紹介するプラグインは「 コンポーネントは JSX で記述されている 」ことを想定しています。

作ったモノ

Specific Component Designを導入するためのELint プラグインです。
プラグインの詳細は、github のドキュメントに日本語で書いていますので、是非見てみて下さい。
以下の画像は、github へのリンクカードです。


Githubリポジトリカード

インストール方法

npmに公開しているので、以下のコマンドでインストールする事が出来ます。

npmでインストール
$> npm install eslint-plugin-scd

yarn でも大丈夫です。

yarnでインストール
$> yarn add eslint-plugin-scd

eslint-plugin-scd の使い方

使い方は、他のESLint プラグインと同じです。この記事では、.eslintrc.jsで設定する方法を紹介します。

.eslintrc.jsで設定
module.exports = {
  extends: ["plugin:scd/recommended"], // デフォルトのルールを適用
  plugins: ["scd"], // eslint-plugin-scdを適用
  rules: {
    // オプションを渡してカスタマイズ出来ます。
    // 詳しくはgihubリポジトリに書いています。( 日本語ドキュメントあります )
    "scd/nue-element-size": [
      "error",
      {
        max: 10,
      },
    ],
  },
};

現在のところjsxtsxの方では動作している事を確認しています。
typescriptでは、以下の設定で動くことを確認しました。

.eslintrc.js(typescriptに対応)
module.exports = {
  env: {
    browser: true,
    es2020: true,
  },

  extends: ["eslint:recommended", "plugin:scd/recommended"],

  parser: "@typescript-eslint/parser",

  parserOptions: {
    ecmaFeatures: {
      jsx: true,
    },
    sourceType: "module",
  },

  plugins: ["scd", "@typescript-eslint"],
};

Specific Component Design について

Specific Component Design( 以下 SCD と表記します )は、具体的なコンポーネント基準によってコンポーネントを分類して設計するための設計思想です。

SCDの目的は、コンポーネントに明確な基準を設け、より設計しやすく・より管理しやすくすることです。
またSCDは、コンポーネントを「 フロントエンドとしてのコンポーネント 」と捉え、フロントエンドフレームワークでのコンポーネント設計で役立つことを想定しています。なので、CSS などの設計についてはSCDに含まれていませんので、ご注意ください。

具体的なコンポーネント基準とは?

SCD はコンポーネント単位が小さい順に、

  1. Nue
  2. Otem
  3. Pafe
  4. Page
  5. Module

の 5 つの分類があり、それぞれの分類には明確な分類基準があります。
それら明確な基準は以下のようになります。

Nue

Nue は最小単位のコンポーネントを分類します。

規約

  • コンポーネントファイルをnuesまたはnueフォルダー下に入れる
  • 要素数( html タグやコンポーネントの数 )は、0~5 個以内する
  • State は持たせない
  • Hooks の使用禁止

Otem

Nueよりも大きなコンポーネントを分類します。

規約

  • コンポーネントファイルをotemsまたはotemフォルダー下に入れる
  • 要素数( html タグやコンポーネントの数 )は、5~10 個以内する
  • State は持たせない
  • Hooks の使用禁止

Pafe

NueOtemよりも大きなコンポーネント、または State を保持ているコンポーネントを分類します。

規約

  • コンポーネントファイルをpafesまたはpafeフォルダー下に入れる
  • 要素数( html タグやコンポーネントの数 )が10 個以上、または State を保持している
  • Hooks を使用してもよい

Page

Pageは、ページを構成するコンポーネントを分類します。

規約

  • コンポーネントファイルをpagesまたはpageフォルダー下に入れる
  • <main></main>を最低一つ持たせる
  • Hooks を使用してもよい

Module

Moduleは、上記のどれにも分類できない例外的なコンポーネントを分類します
Moduleには明確な規約はありません。自由に設計できます。

※ Module に分類するのは、どうしても他のモノに分類出来ない時です。なるべくNueOtemPafePageに分類できるようにして下さい。

使ってみた感じ

以下の gif 画像は、プラグインを導入してNueコンポーネントを定義した時の挙動です。( VSCode の拡張を使ってます )

scd-play.gif

Nueコンポーネントは、要素数が 5 個以下かつHooks を使ってはダメなので、要素数が 6 個以上またはHooks を使った時にエラーになります。

また、分かりにくいですがnueフォルダー下にコンポーネントファイルがあることも確認できます。( src > nue > sample.jsx ってところ )

コンポーネント設計の難しいさ

ここからはコンポーネント設計の難しさについて解説したいと思いますが、皆さんはコンポーネント設計は簡単だと思いますか?
私は難しいと思っています。そして、その理由は大きく分けて再利用性拡張性の二つがあると思ってます。
これら二つはコンポーネントが求めるもの・作る意義であり、より良いコンポーネントを作るためには両者は必要になってきます。しかし、再利用性拡張性相反しており双方を維持することはできません。なので、私たちがコンポーネント設計する場合は、必ず再利用性拡張性のどちらかを捨て、どちらかを高めるという設計を状況に応じてする必要があります

この「 状況に応じて再利用性拡張性のどちらかを捨てる 」という事が、コンポーネント設計における最大の難しさだと思います。

さて、その難しいさを作っている再利用性拡張性ですが具体的にはどんなモノなのでしょうか?
それを理解するために実際に具体例を出して見てみましょう。

コンポーネント設計における再利用性

再利用性というのは、名前の通り再利用しやすいかどうかを主眼に置いており、コンポーネントを扱いやすくするためにはとても大事な要素です。ですが、「 再利用しやすい 」とはどういう事でしょうか?そこをちゃんと考えてみましょう。

以下に具体例を示します。

// 記事をカードで表示
const EntryCard = ({ entry }) => (
  <li className="entry-card">
    <div className="thumbnail">
      <img src={entry.thumbnail} />
    </div>

    <h3>{entry.title}</h3>

    <div className="author">
      <p>{entry.author.name}</p>
    </div>
  </li>
);

// 記事一覧ページ
const ExplorePage = ({ entries }) => (
  <main>
    <ul>
      {entries.map((entry) => (
        <EntryCard entry={entry} />
      ))}
    </ul>
  </main>
);

// 投稿した記事一覧ページ
const MyEntriesPage = ({ entries }) => (
  <main>
    <ul>
      {entries.map((entry) => (
        <EntryCard entry={entry} />
      ))}
    </ul>
  </main>
);

上記のソースコードは、記事情報からカードを表示するコンポーネント<EntryCard />があり、そのコンポーネントを<ExplorePage /><MyEntriesPage />の二つのコンポーネントが使っています。
注目すべき点は、ExplorePageMyEntriesPageの双方ともに<EntryCard />同じ方法で使っていて、同じ表示をすることが出来ています。これは普通に hmlt タグで書いた時よりもシンプルに書けているのが分かりますね。

この「 同じ方法で同じ表示が出来ている 」という所が非常に重要です。これはつまり、同じ方法でも違う挙動( 表示 )をしてたり、同じ挙動( 表示 )でも違う使い方をしている場合は、「 再利用しやすい 」とは言えません。

例えば、以下のようなコンポーネントがそれです。

// ランダムで記事をカードで表示
const RandomEntryCard = ({ entry }) => {
  const isShow = Math.floor(Math.random() * 100) < 50;

  if (!isShow) return null; // falseならnullを返す

  return (
    <li className="entry-card">
      <div className="thumbnail">
        <img src={entry.thumbnail} />
      </div>

      <h3>{entry.title}</h3>

      <div className="author">
        <p>{entry.author.name}</p>
      </div>
    </li>
  );
};

<RandomEntryCard />ランダムで渡された entry 情報を表示します。このコンポーネントはさっきの<EntryCard />と同じ方法で使うことができますが、明らかに使いにくい事が分かります。

これが「 同じ方法でも違う挙動( 表示 )をしている 」という事です。

では次に、「 同じ挙動( 表示 )でも違う使い方をしている 」はどうでしょうか?
以下のコンポーネントがそれです。

// 記事をカードで表示
const EntryOrBlogCard = ({ entry, blog }) => {
  entry = entry || blog;

  return (
    <li className="entry-card">
      <div className="thumbnail">
        <img src={entry.thumbnail} />
      </div>

      <h3>{entry.title}</h3>

      <div className="author">
        <p>{entry.author.name}</p>
      </div>
    </li>
  );
};

<EntryOrBlogCard />は、<EntryOrBlogCard entry={entry} />または<EntryOrBlogCard blog={entry} />で同じ挙動を示します。
しかし、コンポーネントの内部を知らない人が<EntryOrBlogCard />を使っている所を見た時、Props が違う事から連想して、何か違う挙動をするように見えてしまいます。そうなると、このコンポーネントを使うのにわざわざ内部構造を理解しなければならなくなり、非常に利用しにくい状態になってしまいます。

これが「 同じ挙動( 表示 )でも違う使い方をしている 」という事です。

コンポーネント設計における拡張性

それでは、次に拡張性について見ていきましょう。
この記事での拡張性とは「 後付けがしやすい 」という事ではなく、「 挙動( 表示 )を変化させやすい 」という事です。1

言葉だけじゃ分かりにくいと思うので、例を見てみましょう。

// 記事をカードで表示
const EntryCard = ({ entry }) => (
  <li className="entry-card">
    <div className="thumbnail">
      <img src={entry.thumbnail} />
    </div>

    <h3>{entry.title}</h3>

    <div className="author">
      <p>{entry.author.name}</p>
    </div>
  </li>
);

// 記事一覧ページ
const ExplorePage = ({ entries }) => (
  <main>
    <ul>
      {entries.map((entry) => (
        <EntryCard entry={entry} />
      ))}
    </ul>
  </main>
);

// 投稿した記事一覧ページ
const MyEntriesPage = ({ entries }) => (
  <main>
    <ul>
      {entries.map((entry) => (
        <EntryCard entry={entry} />
      ))}
    </ul>
  </main>
);

上記のコードは、再利用性の時と同じソースコードです。現状では<EntryCard />は表示のみをしており、その他の事はしていません。
それを Author 情報の表示非表示を出来るように修正したいと思います。
理由は、このコードの<MyEntriesPage />は自分が投稿した記事しか表示されない為、Author 情報を表示する必要が無いためです。( Author 情報が全て自分( ページを見ている人 )になるため )

<EntryCard />を以下のように修正します。

EntryCardを修正
const EntryCard = ({ entry, showAuthor }) => (
  <li className="entry-card">
    <div className="thumbnail">
      <img src={entry.thumbnail} />
    </div>

    <h3>{entry.title}</h3>

    {showAuthor ? (
      <div className="author">
        <p>{entry.author.name}</p>
      </div>
    ) : null}
  </li>
);

showAuthor という Props によって、author 情報の表示を切り替えられるようにしました。
この修正によって、<EntryCard />を使っているコンポーネントは以下のように修正します。

ExplorePageとMyEntriesPageを修正
// 記事一覧ページ
const ExplorePage = ({ entries }) => (
  <main>
    <ul>
      {entries.map((entry) => (
        <EntryCard entry={entry} showAuthor={true} /> // Author情報を表示
      ))}
    </ul>
  </main>
);

// 投稿した記事一覧ページ
const MyEntriesPage = ({ entries }) => (
  <main>
    <ul>
      {entries.map((entry) => (
        <EntryCard entry={entry} showAuthor={false} /> // Author情報を非表示
      ))}
    </ul>
  </main>
);

上記の修正により、author 情報は必ず表示していた所をshowAuthorの値によって表示非表示を出来るように<EntryCard />を修正しました。

これにより<EntryCard />の出来ることが増え、より対応できる場面が増えたと分かります。

再利用性と拡張性は相反する

さて、再利用性拡張性を説明してきましたが、ここで一つ問題が発生しました。
それはExplorePageとMyEntriesPageを修正のソースコードの以下の二つ部分です。

ExplorePageコンポーネントより抜粋
(
  /* ... */
  <EntryCard entry={entry} showAuthor={true} /> // Author情報を表示
  /* ... */
)
MyEntriesPageコンポーネントより抜粋
(
  /* ... */
  <EntryCard entry={entry} showAuthor={false} /> // Author情報を非表示
  /* ... */
)

上記の二つのコードは<EntryCard />拡張性を上げるためにshowAuthorを Props に追加したため、<EntryCard />showAuthorの値を渡しています。これは特段問題が無いように見えますが、再利用性という観点から見ると問題が発生しています。
以下のソースコードを見てください。

showAuthorにtrueを渡す例
(
  /* ... */
  <EntryCard entry={entry} showAuthor={true} />
  /* ... */
)

(
  /* ... */
  <EntryCard entry={entry} showAuthor />
  /* ... */
)
showAuthorにfalseを渡す例
(
  /* ... */
  <EntryCard entry={entry} showAuthor={false} />
  /* ... */
)

(
  /* ... */
  <EntryCard entry={entry} /> // showAuthorはundefinedとなるが、undefinedはfalseとして扱われるため同じ挙動を示す
  /* ... */
)

上記の書き方でも、<EntryCard />は同じ挙動をします。
再利用性とは「 同じ方法で同じ挙動( 表示 )を扱える 」という事でした。
しかしどうでしょう?上記のコードは、 同じ方法でしょうか?
showAuthorに boolean を渡すのに、truefalseそれぞれ二通りずつあります。これは明らかに、再利用性が損なわれていることが分かります。

つまり、拡張性を上げた結果、再利用性が失われています!

これが再利用性拡張性が相反すると言った所以です。
また、これらはboolean型だけの話ではなく、Propsの数が多くなったり、別のデータ型になったとしても同じです。

再利用性と拡張性にどう向き合うか?

再利用性拡張性はコンポーネント作るための意義であり、双方とても大事な要素です。
しかし双方が相反しているため両者を高く保つことは困難で、状況に応じてどちらかを高く保つ必要があるかという事にコンポーネント設計者は頭を悩ませています。

私はこの問題について再利用性と拡張性を維持することを諦め明確な基準を設けることで、再利用性拡張性を最低限保持することで対応しています。

そして、この「 明確な基準を設ける 」というのがこの記事で紹介している ESLint プラグインSCDを作った理由です。

長々と話してきましたが、結局のところ再利用性拡張性を高く保つ事は難しく、考えた末の結果は最低限の再利用性拡張性は保持するという所に落ち着きました。これが良いか悪いかで言えば、それはたぶん人それぞれでしょう。

あなたは eslint-plugin-scd を使うべきか?

もしあなたがコンポーネント設計を複数人で行う場合は、この記事で紹介している ESLint プラグインのようなコンポーネント設計を強制するESLint プラグインを使うべきであると思います。
なぜなら、人によってコンポーネント設計の基準は違っており、それが色々な人為的ミスにつながるからです。
もしそうでないなら、この記事で紹介している ESLint プラグインを使っても使わなくても大丈夫でしょう。

つまり、このプラグインを使うべき人は以下のような人たちだと思います。

  • コンポーネント設計に自信が無い人
  • コンポーネント設計に労力をかけたくない人
  • 複数人でコンポーネント設計を行う必要がある人

あとがき

ここまでコンポーネント設計について語ってきましたが、正直説明するのが難しく、私の考えている事の一割も説明できてないように思えて、自分の説明力の無さに落胆していますが、あくまでESLint プラグインの紹介記事なので、プラグインの事だけでも知ってもらえたら幸いです。

コンポーネント設計の闇は、State とかキャッシュが入ってきた途端に一気に深くなりますよね ?
私はフロントエンドで一番難しいのがコンポーネント設計だと思ってますが、皆さんはどうでしょうか?そこまで難しくないでしょうか?
ここら辺の認識は、使うフレームワークなどによって結構変わってくるかもしれませんね。Anguler だと悩まないかも ?

またフロントエンドはデザインのところばかり注目されて、こういった設計があるって事を外部の人から理解してもらえない事が多々あるので、この記事で少しでもコンポーネント設計の難しさについて理解してもらえたらいいなと思っています。

最後に、今回作ったESLint プラグインですが、もしかしたら既存のプラグインで同じような事は出来るかもしれません。
ただ今回は調べるのが面倒くさかったのと、前々から ESLint プラグイン作ってみたかったので作ってみました。
色々と学べたのでいい機会だったと思います ?

もしSCDのルールが気に入らない人は.eslintrc.jsなどの設定ファイルで、ルールをカスタマイズして使うこともできますので、是非使ってみて下さい。

それでは ?


  1. 拡張性という言葉はあまり良くないと思ってますが、他にいい言葉が思い浮かばなかったので拡張性としています。 

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

3Dシーンを作ってすぐ公開 Three.js+Gatsby+TypeScriptによるモダンWebXRテンプレート

この記事はWebXR Tech Tokyo #1の発表で使用いたしました。
イベントを企画いただいた運営の皆様、会場を盛り上げていただいた参加者の皆様、ありがとうございました!

この記事でできるようになること

Three.jsを使った3Dウェブページをモダンな技術スタックを使って簡単に開発・公開できるようになります。オリジナルテンプレートを使うことでスムーズに始められ、またGatsbyを用いることで自分好みのカスタマイズを簡単に加えることが可能です。

以下のサンプルシーンは予めテンプレートに組み込まれております。本記事での解説に加え、必要に応じてテンプレート内の実装を参考にしていただき、ご自身で3Dシーンを作成する際のお役立ていただければ幸いです。

Coaster.gif
3dobj2.gif
obj3.gif

使用する技術スタック

:white_check_mark: Three.js
:white_check_mark: TypeScript
:white_check_mark: Gatsby
:white_check_mark: Netlify
:white_check_mark: TailwindCSS

テンプレート

初期開発の手間を省くため、テンプレートを事前に作成しておきました。以下のコマンドを実行すると必要なモジュールが組み込まれたベースを利用することができます。

> yarn global add gatsby-cli
> gatsby new 3d-template https://github.com/shunp/gatsby-three-ts-plus

※Node.jsはv12系を推奨しております。バージョンによってはgatsby newがうまく動作しない可能性があります。

動作確認

> cd 3d-template
> yarn
> yarn dev

今回のトップにはジェットコースターの搭乗者視点によるWebXR画面を用意しました。Three.jsのサンプルページにあるものを少しカスタマイズしたものになります。

Coaster.gif

ベースシーンの解説

src/scenes/BaseScene.tsxにカスタム用のシーンを追加しました。最低限の必要なコンポーネントのみを記載しています。

BaseScene.tsx
import React, { useEffect, createRef } from 'react'
import * as THREE from 'three'
import { css } from '@emotion/core'

const newScene = () => {
  const scene = new THREE.Scene()
  return scene
}

const newCamera = () => {
  const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000)
  camera.position.z = 400
  return camera
}

const newRenderer = (mount: React.RefObject<HTMLInputElement>) => {
  const renderer = new THREE.WebGLRenderer({ antialias: true })
  renderer.setPixelRatio(window.devicePixelRatio)
  renderer.setSize(window.innerWidth, window.innerHeight)
  renderer.autoClear = true
  if (mount.current) {
    mount.current.appendChild(renderer.domElement)
  }
  return renderer
}

const BaseScene = () => {
  const mount = createRef<HTMLInputElement>()
  useEffect(() => {
    // scene
    const scene = newScene()

    // camera
    const camera = newCamera()

    // renderer
    const renderer = newRenderer(mount)

    // mesh
    const geometry = new THREE.BoxGeometry(20, 20, 20)
    const material = new THREE.MeshNormalMaterial()
    const mesh = new THREE.Mesh(geometry, material)
    scene.add(mesh)

    // render
    const render = () => {
      renderer.render(scene, camera)
    }

    // animation
    const animate = () => {
      requestAnimationFrame(animate)
      render()
    }
    animate()
  }, [])
  return (
    <>
      <div css={css``} ref={mount} />
    </>
  )
}
export default BaseScene

3DシーンをWebで実装する場合に最も重要なコンポーネントとして、Camera, Scene, Rendererの3つがあります。Meshは表示されるオブジェクトを表しており、基本的にGeometryMaterialの2つから構成されてます。今回はシンプルな立方体オブジェクトのMeshが表示対象となっています。実行してコンテンツを確認してみましょう。localhost:8000から確認できます。

Screen Shot 2020-06-25 at 21.54.57.png

カスタムシーンを追加する

上記のベースシーンをカスタマイズしてみます。カメラとオブジェクトを以下のように修正します。

CustomScene.tsx
...

const newCamera = () => {
  const camera = new THREE.PerspectiveCamera(40, window.innerWidth / window.innerHeight, 1, 3000)
  camera.position.z = 4
  return camera
}
...
const BaseScene = () => {
    ...
    // mesh
    const geometry = new THREE.BoxBufferGeometry(0.75, 0.75, 0.75)
    const material = new MeshNormalMaterial()
    const mesh = new THREE.Mesh(geometry, material)
    mesh.position.x = 0
    mesh.position.y = 0
    scene.add(mesh)
    ...

中央に大きな立方体が1つ現れます。

Screen Shot 2020-06-26 at 0.11.43.png

正面からだとよく分からないので、立方体だとわかるように回転を加えてみます。

CustomScene.tsx
    ...

    // renderer
    const renderer = newRenderer(mount)

    // clock
    const clock = new THREE.Clock()

    // render
    const render = () => {
      const delta = clock.getDelta()

      mesh.rotation.x += delta * 0.5
      mesh.rotation.y += delta * 0.5
      renderer.render(scene, camera)
    }
    ...

Screen Shot 2020-06-26 at 0.13.58.png

ここまでで動きのある3Dオブジェクトを作成できましたが、少し味気ないため次節で表面に動きを付けていきます。

GLSLの追加

WebGLで何かを描画するためには2つのシェーダが必要になります。シェーダを描くための言語GLSL(グラフィクス・ライブラリ・シェーダー言語)を利用したサンプルもThree.jsのコードにはありますが、あくまで素のHTMLを読み込むことを想定した書き方なのでそのまま利用することができません。Reactを利用する場合は以下のように記述していきます。

頂点シェーダ

CustomScene.tsx
const vert = `
varying vec2 vUv;
void main() {
  vUv = uv;
  vec4 mvPosition = modelViewMatrix * vec4( position, 1.0 );
  gl_Position = projectionMatrix * mvPosition;
}
`

フラグメントシェーダ

CustomScene.tsx
const frag = `
uniform float time;
varying vec2 vUv;
void main( void ) {
  vec2 position = - 1.0 + 2.0 * vUv;
  float red = abs( sin( position.x * position.y + time / 5.0 ) );
  float green = abs( sin( position.x * position.y + time / 4.0 ) );
  float blue = abs( sin( position.x * position.y + time / 3.0 ) );
  gl_FragColor = vec4( red, green, blue, 1.0 );
}
`

最初に用意したMeshNormalMaterialShaderMaterialに置き換えます。

CustomScene.tsx
const BaseScene = () => {
    ...
    // mesh
    const geometry = new THREE.BoxBufferGeometry(0.75, 0.75, 0.75)
    // const material = new MeshNormalMaterial()
    const uniforms = {
      time: { value: 1.0 }
    }
    const material = new THREE.ShaderMaterial({
      uniforms,
      vertexShader: vert, // 頂点シェーダ
      fragmentShader: frag // フラグメントシェーダ
    })
    const mesh = new THREE.Mesh(geometry, material)
    mesh.position.x = 0
    mesh.position.y = 0
    scene.add(mesh)
    ...
}

最後に、アニメーションとして動きが出るようrender()関数に以下を追加します。

CustomScene.tsx
    // render
    const render = () => {
      const delta = clock.getDelta()
      uniforms.time.value += delta * 5 // 追加
      mesh.rotation.x += delta * 0.5
      mesh.rotation.y += delta * 0.5
      renderer.render(scene, camera)
    }

3dobj.gif

表面の鮮やかな模様が時間と共に変化している動的な描画になりました。

シェーダとGLSLに関しては以下のサイトがわかりやすかったためリンクを添付します。
WebGLのシェーダーとGLSL

ここまでのソースコードは以下のようになります。

CustomScene.tsx
import React, { useEffect, createRef } from 'react'
import * as THREE from 'three'
import { css } from '@emotion/core'

const vert = `
varying vec2 vUv;
void main() {
  vUv = uv;
  vec4 mvPosition = modelViewMatrix * vec4( position, 1.0 );
  gl_Position = projectionMatrix * mvPosition;
}
`

const frag = `
uniform float time;
varying vec2 vUv;
void main( void ) {
  vec2 position = - 1.0 + 2.0 * vUv;
  float red = abs( sin( position.x * position.y + time / 5.0 ) );
  float green = abs( sin( position.x * position.y + time / 4.0 ) );
  float blue = abs( sin( position.x * position.y + time / 3.0 ) );
  gl_FragColor = vec4( red, green, blue, 1.0 );
}
`

const newScene = () => {
  const scene = new THREE.Scene()
  return scene
}

const newCamera = () => {
  const camera = new THREE.PerspectiveCamera(40, window.innerWidth / window.innerHeight, 1, 3000)
  camera.position.z = 4
  return camera
}

const newRenderer = (mount: React.RefObject<HTMLInputElement>) => {
  const renderer = new THREE.WebGLRenderer({ antialias: true })
  renderer.setPixelRatio(window.devicePixelRatio)
  renderer.setSize(window.innerWidth, window.innerHeight)
  renderer.autoClear = true
  if (mount.current) {
    mount.current.appendChild(renderer.domElement)
  }
  return renderer
}

const BaseScene = () => {
  const mount = createRef<HTMLInputElement>()
  useEffect(() => {
    // scene
    const scene = newScene()

    // camera
    const camera = newCamera()

    // renderer
    const renderer = newRenderer(mount)

    // clock
    const clock = new THREE.Clock()

    // mesh
    const geometry = new THREE.BoxBufferGeometry(0.75, 0.75, 0.75)
    const uniforms = {
      time: { value: 1.0 }
    }
    const material = new THREE.ShaderMaterial({
      uniforms,
      vertexShader: vert,
      fragmentShader: frag
    })
    const mesh = new THREE.Mesh(geometry, material)
    mesh.position.x = 0
    mesh.position.y = 0
    scene.add(mesh)

    // render
    const render = () => {
      const delta = clock.getDelta()
      uniforms.time.value += delta * 5
      mesh.rotation.x += delta * 0.5
      mesh.rotation.y += delta * 0.5
      renderer.render(scene, camera)
    }

    // animation
    const animate = () => {
      requestAnimationFrame(animate)
      render()
    }
    animate()
  }, [])
  return (
    <>
      <div css={css``} ref={mount} />
    </>
  )
}
export default BaseScene

Glitchの追加

Post Processing(後処理)を追加することでシーンにプラスアルファの味を加えてみます。今回は画面が割れるような振動を表現できるGlitchというフィルターを追加してみます。

Post Processingを追加するためにはEffectComposerをインポートする必要があります。Three.jsではこのようにサンプルとして便利なライブラリを提供してくれていますので、Reactを使用する場合は次のようにインポートします。

CustomScene.tsx
import React, { useEffect, createRef } from 'react'
import * as THREE from 'three'
import { css } from '@emotion/core'
import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer.js' // 追加
import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass.js' // 追加
import { GlitchPass } from 'three/examples/jsm/postprocessing/GlitchPass.js' // 追加

フィルターをシーンに導入するには、EffectComposerRenderPassGlitchPassを以下のように追加したあと、animation内でcomposerを呼び出します。

CustomScene.tsx
    ...
    // post processing
    const composer = new EffectComposer(renderer)
    composer.addPass(new RenderPass(scene, camera))
    const glitchPass = new GlitchPass()
    composer.addPass(glitchPass)

    ...

    // animation
    const animate = () => {
      requestAnimationFrame(animate)
      render()
      composer.render()
    }
    ...

テキストを重ねる

最後に3Dシーンの上にテキストを重ねてみます。シーン内にテキストを埋め込むこともできますが、今回はテキストのレイヤーを3DCanvasの上に乗せる形にします。

CustomScene.tsx
  ...
  return (
    <>
      <div>
        <span>Three.js × Gatsby Template</span>
      </div>
      <div css={css``} ref={mount} />
    </>
  )
  ...

Screen Shot 2020-06-28 at 16.03.35.png

右上に小さく文字が表示されているため、CSSを当てて見た目を調整します。スタイルの追加には方法がいくつかありますが、今回のテンプレートにはTailwindCSSを入れているので、これを使ってデザイン調整する方法をみていきましょう。TailwindCSSとは

CustomScene.tsx
  ...
  return (
    <>
      <div className="absolute z-10 w-full h-full">
        <div className="flex justify-center mt-32">
          <span className="font-serif text-white text-4xl">Three.js × Gatsby Template</span>
        </div>
      </div>
      <div ref={mount} />
    </>
  )
  ...

以下のようにグリッチフィルターの上からテキストレイヤーを重ねています。Three.jsではマウスのクリックイベントやスクロールを拾うこともできますが、今回のように上からSceneを覆い隠してしまうとそれらが向こうになる場合がありますので、実現したい表現によっては注意が必要です。

3dobj2.gif

OBJファイルのロード

新しいシーンで3Dオブジェクトファイルの取り扱いを説明していきます。3Dモデルフォーマットにはいくつか種類がありますが、今回は.obj形式のファイルをOBJLoaderを使用して取り込んで見たいと思います。

CustomScene2.tsx
import React, { useEffect, createRef } from 'react'
import * as THREE from 'three'
import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader.js'

const MALE_OBJ_PATH = './models/male02.obj'
const FEMALE_OBJ_PATH = './models/female02.obj'
...

Filter同様、OBJLoaderもThree.jsのExamplesに含まれているので、上記のようにインポートしてきます。また、取り込みたいOBJファイルをローカルにコピーしておきましょう。相対パスでファイルを指定します。

Screen Shot 2020-06-28 at 22.54.50.png

OBJファイルはstaticディレクトリ以下におきます。ここに置くことでビルド時にpublicディレクトリ以下にコピーが生成されて相対パスでアクセスできるようになります。

CustomScene2.tsx
    ...
    // OBJ loader
    const loader = new OBJLoader()
    loader.load(MALE_OBJ_PATH, (object: THREE.Group) => {
      ...
    })
    loader.load(FEMALE_OBJ_PATH, (object: THREE.Group) => {
      ...
    })

今回は2種類のオブジェクトがロードされ、動的なポイントクラウドとしてモデルを表現しています。シーンの回転、ポイント郡の上下運動、親3DObjectと子オブジェクトの紐付けなど、他のシーンでも応用可能な実装が含まれておりますので、さらに挑戦したい方はCustomScene2.tsxの実装をご参照ください。localhost:8000/custom2を開くと以下のシーンが表示されます。

obj3.gif

ここまでで3Dシーンに関する説明は以上です。同様にpageディレクトリ以下にファイルを作成することで新たなカスタムページを作成することができます。Three.jsはWeb上にたくさんのサンプルが落ちていますので、上記の基本をベースにサンプルを動かしつつ自身のポートフォリオを作成していってください。

Webページの公開

GitHubへ変更をプッシュ

最後に、ここまでの変更をWeb上で公開するため、まずは自身のGitHubリポジトリに対してソースコードをプッシュしておきましょう。

Netlifyの設定

Netlifyを使うことで簡単にポートフォリオサイトを公開できます。リンクからNetlifyのページに飛び、自身のページにログインをしてください。初めての方はサインアップが必要になります。右上にある「New site from Git」から新しいサイトを作成するページに飛びます。ソースコードがGitHubにある場合はGitHubボタンを選択して次に進みます。

Screen Shot 2020-06-25 at 17.00.35.png

今回保存されたソースコードを含むリポジトリを選択します。ビルド設定はデフォルトで問題ありません。「Deploy site」ボタンを押するとビルドが始まり、しばらくするとサイトが公開されます。

Screen Shot 2020-06-25 at 17.01.34.png

また、Netlify公開用にランダムで割り当てられた文字列のURLではなく、独自の名前を設定することも可能です。
Setting→Domain Management→Domains→Custom domains→Options→Edit site nameから選択できます。

Screen Shot 2020-06-25 at 22.07.10.png

設定したURLからページが公開されていることを確認してみましょう。

あとがき

Three.jsの公式サンプルページには、様々な表現方法を用いたカタログが一覧になっています。サンプルコードとセットになっており、新しく3Dシーンを作成する際にはとても参考になるでしょう。

一方、長い歴史を持つThree.jsは、今ではあまり使われない記法で記述されたコードが多いのも現状です。
今回のテンプレートではTypeScriptやES6以降の記法を取り入れつつ、現場で使われることの多いReactをベースに作成しました。react-three-fiberというライブラリも存在しますが、Three.jsのバージョンアップに追随できる、かつ特定のフレームに依存しないなるべく素のThree.jsを扱えるようにカスタマイズ性の高いテンプレートとして公開しています。オープンソースですのでお気軽にPR、スター等いただけると今後の活動の励みになります。

GitHubソースコード

また、3Dのポートフォリオジェネレータとは別に、ソーシャルメディアのポートフォリオジェネレータを別途開発中です。今後はSNSだけではなく3Dアバターなどもクリップできるようにしていく予定です。現在α版としてテストユーザを募集しておりますので、ご興味がありましたらこちらまでご連絡をお願いいたします。

Screen Shot 2020-06-28 at 22.40.06.png

お気に入りのポストを好きなSNSから、好きな数だけNoCodeでクリップできるポートフォリオサイトです。「複数のSNSを1つのURLで管理したい」「埋もれてしまった過去のポストをクリップしておきたい」場合に便利です。SNS版前略プロフィール?のようなイメージでお楽しみください。
https://storygate.info

最後までご一読いただきありがとうございました。

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

【React Hooks】兄弟間でイベントを飛ばす

TL;DR

  • React で、兄弟コンポーネント間でイベントを送りたい。
  • useEffect()ってイベントっぽくない?
  • useState()の本来と違う使い方なのでカスタムフックに抽出しよう。

背景

  • Functional Component で書きます。
  • 親コンポーネントPとその子コンポーネントABがあります。
  • Aは複数選択可なアイテムで、新たに選択されたアイテムに対して、
  • Web APIを叩いて得られた結果を全て保持し、Bでグラフとして表示します。

このようなアプリを作ろうとすると、非同期処理が入るので、選択された値の配列をPに保管して、Bに Attribute として渡してその中で処理しようと考えると、結果をキャッシュしないといけません。
キャッシュを実装するのが面倒だったので、そこで、イベント駆動プログラミングの出番です。Aで新たに選択された値をBに渡して、B内で処理させましょう。子コンポーネントでのデータの使い方について親が知る必要がないほうがたぶん楽なので。
以下は単純化されたサンプルアプリケーションで話を進めます。
reactive.gif

useEffectって、イベント駆動なのではないか

Reactには、useEffect(effect, deps)というHookがあります。この引数depsは、引数effectとして渡される関数は、前回のrender時と今回のrender時でdeps(配列)の値のどれかが変わるときに実行されます。(初回render時にも実行されます。)
これって、EventやReactive ExtentionのStreamと似ていませんか?似てますよね。

index.tsx
interface NumberEvent { // numberそのものを使うと、同じ値が連続で出た時にコールバックが呼ばれない
    value: number
}

function newRandomNum(): NumberEvent {
    return { value: Math.round(Math.random() * 10) }
}

// 送信側のコンポーネント
// 0 ~ 10 のランダムな数を整数に丸めて送信します。
const Inputs: FC<{effect: Dispatch<SetStateAction<NumberEvent>>}> = (props) => {
    return <button onClick={() => props.effect(newRandomNum())}>Fire Event!!</button>
}

// 受信側のコンポーネント
// 送信された数の累計を表示するコンポーネント
const DispSum: FC<{currentNum: NumberEvent}> = (props) => {
    const [sum, setSum] = useState(0)
    useEffect(() => { 
        setSum(p => p + props.currentNum.value) 
    }, [props.currentNum])
    return <div>Sum : {sum}</div>
}

// 送信された数の履歴を新しい順で表示するコンポーネント
const DispHistory: FC<{currentNum: NumberEvent}> = (props) => {
    const [history, setHistory] = useState<number[]>([])
    useEffect(() => { 
        setHistory(p => [props.currentNum.value, ...p])
    }, [props.currentNum])
    return <div>History : [{history.join(", ")}]</div>
}

// 親コンポーネント

const App: FC = () => {
    const [current, emit] = useState({value: 0})
    return (
        <div>
            <Inputs effect={emit}></Inputs>
            <DispSum currentNum={current}></DispSum>
            <DispHistory currentNum={current}></DispHistory>
        </div>
    )
}

こうすると、DispSumDispHistory各コンポーネントは独立にイベントを受け取ってコンポーネント内で処理・表示することが出来ています。

本来の使い方とちょっとずれてる?

useState()を、イベントの受け渡しに使うのは、状態の管理という本来の使い方と少しずれているような気がするので、カスタムフック化して、これを隠してしまいましょう。

const [current, emit] = useState({value: 0})

の部分は、

interface MsgEmitter<A> {
    current: A
    emit: Dispatch<SetStateAction<A>>
}

function useMsgEmitter<A>(init: A): MsgEmitter<A> {
    const [current, emit] = useState(init)
    return { current, emit }
}

に、そして、

useEffect(() => { 
 // ...
}, [props.currentNum])

の部分は,

function useMsgReceiver<A, B>(effect: (b: B) => void, emitter: Pick<MsgEmitter<B>, "current">) {
    useEffect(() => effect(emitter.current), [emitter.current])
}

にします。(Pick<MsgEmitter<B>, "current">は、MsgEmitter<B>の内、currentだけ使えればいいという意味です。)
そもそも元の状態と渡されたイベントから一つのstateを変更するだけなら、更に抽象化が可能です。

function useCalculatedState<A, B>(effect: (setValue: (a: A) => void, prev: A, b: B) => void, emitter: Pick<MsgEmitter<B>, "current">, init?: A | (() => A)) {
    const [value, setValue] = useState<A>(init)
    useEffect(() => effect(setValue, value, emitter.current), [emitter.current])
    return value
}

完成

一応useMsgReceiver()useCalculatedState()の両方を使うように書いています。

index.tsx
// MsgEmitter関連の定義

interface MsgEmitter<A> {
    current: A
    emit: Dispatch<SetStateAction<A>>
}

function useMsgEmitter<A>(init: A): MsgEmitter<A> {
    const [current, emit] = useState(init)
    return { current, emit }
}

function useMsgReceiver<A, B>(effect: (b: B) => void, emitter: Pick<MsgEmitter<B>, "current">) {
    useEffect(() => effect(emitter.current), [emitter.current])
}

function useCalculatedState<A, B>(effect: (setValue: (a: A) => void, prev: A, b: B) => void, emitter: Pick<MsgEmitter<B>, "current">, init?: A | (() => A)) {
    const [value, setValue] = useState<A>(init)
    useEffect(() => effect(setValue, value, emitter.current), [emitter.current])
    return value
}

// 送信側の実装

interface NumberEvent {
    value: number
}

function newRandomNum(): NumberEvent {
    return { value: Math.round(Math.random() * 10) }
}

const Inputs: FC<{emitter: MsgEmitter<NumberEvent>}> = (props) => {
    return <button onClick={() => props.emitter.emit(newRandomNum())}>Fire Event!!</button>
}

// 受信側の実装

const DispSum: FC<{emitter: MsgEmitter<NumberEvent>}> = (props) => {
    const [sum, setSum] = useState(0)
    useMsgReceiver(n => {
        setSum(p => p + n.value) 
    }, props.emitter)
    return <div>Sum : {sum}</div>
}
const DispHistory: FC<{emitter: MsgEmitter<NumberEvent>}> = (props) => {
    const history = useCalculatedState((setValue, prev: number[], n) => {
        setValue([n.value, ...prev])
    }, props.emitter, [])
    return <div>History : [{history.join(", ")}]</div>
}

// 親コンポーネント

const App: FC = () => {
    const emitter = useMsgEmitter({value: 0})
    return (
        <div>
            <Inputs emitter={emitter}></Inputs>
            <DispSum emitter={emitter}></DispSum>
            <DispHistory emitter={emitter}></DispHistory>
        </div>
    )
}

reactive.gif

TODO

複数のEmitterに依存するように書き換えることもできるはずなので次回でやってみます。

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

[React & TS] styled-componentsで動的にスタイルを変更する

やりたいこと

よくみるこれをReact & TypeScript とstyled-componentsで実装したい

animation.gif

作ったもの

今回はstyled-componentsに真偽値を渡しておき、値を変更する形としました。

styled-components本家にやり方が記載されています。

hamburgerIcon.tsx
import React from 'react'
import styled, { css } from 'styled-components'

const Hamburger = styled.div<{ isOpen: boolean }>`
  position: absolute;
  top: 16px;
  right: 30px;
  width: 22px;
  height: 18px;
  transition: all 0.4s;

  span {
    :nth-of-type(2) {
      top: 50%;
      left: 50%;
      -webkit-transform: translate3d(-50%, -50%, 0);
      transform: translate3d(-50%, -50%, 0);
    }
  }

  ${(props) =>
    props.isOpen
      ? css`
          span {
            :nth-of-type(1) {
              -webkit-transform: translateY(20px) rotate(-45deg) scale(0.8);
              transform: translateY(8px) rotate(-45deg) scale(0.8);
            }

            :nth-of-type(2) {
              opacity: 0;
            }

            :nth-of-type(3) {
              -webkit-transform: translateY(-20px) rotate(45deg) scale(0.8);
              transform: translateY(-7px) rotate(45deg) scale(0.8);
            }
          }
        `
      : ''}
`

const HamburgerLine = styled.span`
  position: absolute;
  width: 100%;
  height: 3px;
  background-color: #707070;
  transition: all 0.4s;

  &:nth-of-type(1) {
    top: 0;
  }
  &:nth-of-type(2) {
    top: 8px;
  }
  &:nth-of-type(3) {
    bottom: 0;
  }
`

type PropsType = {
  isOpen: boolean
  toggleIsOpen
}

export const MenuHamburger: React.FC<PropsType> = ({
  isOpen,
  toggleIsOpen,
}) => {
  return (
    <Hamburger isOpen={isOpen} onClick={toggleIsOpen}>
      <HamburgerLine />
      <HamburgerLine />
      <HamburgerLine />
    </Hamburger>
  )
}

特別にやったこと

本家を見ると以下の様に書かれています。

import React from 'react'
import styled, { css } from 'styled-components'

const Button = styled.button`
  color: ${(props) => (props.primary ? 'white' : 'palevioletred')};
`

export const MenuHamburger: React.FC<any> = () => {
  return <Button primary>Primary</Button>
}

しかしTypeScriptだと、型解決が出来ずにIDEなどに怒られてしまいます。
image.png

以下の通り、styleの生成時に型を設定してあげましょう。

import React from 'react'
import styled, { css } from 'styled-components'

const Button = styled.button<{ primary: boolean }>`
  color: ${(props) => (props.primary ? 'white' : 'palevioletred')};
`

export const MenuHamburger: React.FC<any> = () => {
  return <Button primary>Primary</Button>
}

数が多い場合は、typeにまとめてもよいですね!

import React from 'react'
import styled, { css } from 'styled-components'

type ButtonProps = {
  primary: boolean
}

const Button = styled.button<ButtonProps>`
  color: ${(props) => (props.primary ? 'white' : 'palevioletred')};
`

export const MenuHamburger: React.FC<any> = () => {
  return <Button primary>Primary</Button>
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

React hooksを基礎から理解する (useCallback編) と React.memo

React hooksとは

React 16.8 で追加された新機能です。
クラスを書かなくても、 stateなどのReactの機能を、関数コンポーネントでシンプルに扱えるようになりました。

useCallbackとは

useCallbackはパフォーマンス向上のためのフックで、メモ化したコールバック関数を返します。

useEffectと同じように、依存配列(=[deps] コールバック関数が依存している要素が格納された配列)の要素のいずれかが変化した場合のみ、メモ化した値を再計算します。

メモ化とは

メモ化とは同じ結果を返す処理について、初回のみ処理を実行記録しておき、値が必要となった2回目以降は、前回の処理結果を計算することなく呼び出し値を得られるようにすることです。

イベントハンドラーのようなcallback関数をメモ化し、不要に生成される関数インスタンスの作成を抑制、再描画を減らすことにより、都度計算しなくて良くなることからパフォーマンスを向上が期待できます。

基本形

useCallback(callbackFunction, [deps]);

sampleFuncは、再レンダーされる度に新しく作られますが、a,bが変わらない限り、作り直す必要はありません。

const sampleFunc = () => {doSomething(a, b)}

usecallbackを使えば、依存配列の要素a,bのいずれかが変化した場合のみ、以前作ってメモ化したsampleFuncの値を再計算します。一方で全て前回と同じであれば、前回のsampleFuncを再利用します。

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

再レンダーによるコストについて検証してみる

Text,Count,Buttonコンポーネントを子に持つ親コンポーネントCounterコンポーネントを作成しました。

testVol1.jsx
import React, {useState} from 'react'

//Titleコンポーネント(子)
const Title = () => {
  console.log('Title component')
  return (
    <h2>useCallBackTest vol.1</h2>
  )
}

//Buttonコンポーネント(子)
const Button = ({handleClick,value}) => {
  console.log('Button child component', value)
  return <button type="button" onClick={handleClick}>{value}</button>
}

//Countコンポーネント(子)
const Count = ({text, countState}) => {
  console.log('Count child component', text)
  return <p>{text}:{countState}</p>
}

//Counterコンポーネント(親)
const Counter = () => {

  const [firstCountState, setFirstCountState] = useState(0)
  const [secondCountState, setSecondCountState] = useState(10)

//+ 1 ボタンのstateセット用関数
  const incrementFirstCounter = () => setFirstCountState(firstCountState + 1)

//+ 10 ボタンのstateセット用関数
  const incrementSecondCounter = () => setSecondCountState(secondCountState + 10)

//子コンポーネントを呼び出す
  return (
    <>
      <Title/>
      <Count text="+ 1 ボタン" countState={firstCountState}/>
      <Count text="+ 10 ボタン" countState={secondCountState}/>
      <Button handleClick={incrementFirstCounter} value={'+1 ボタン'}/>
      <Button handleClick={incrementSecondCounter} value={'+10 ボタン'}/>
    </>
  )
}

export default Counter

console.logを実行しているだけですが、すべてのコンポーネントが再レンダーされています。この部分で高コストな処理を行っていれば、その分だけパフォーマンスに悪影響を与えることになりますし、サイトが大きくなると、負荷も大きくなっていきます。

React.memoについて

React.memoでは、コンポーネントが返した React 要素を記録し、再レンダーされそうになった時に本当に再レンダーが必要かどうかをチェックして、必要な場合のみ再レンダーします。
デフォルトでは、等価性の判断にshallow compareを使っており、オブジェクトの1階層のみを比較することになります。

React.memoは、メモ化したいコンポーネントをラップして使います。

//Titleコンポーネント(子)
const Title = React.memo(() => {
  console.log('Title component')
  return (
    <h2>useCallBackTest vol.1</h2>
  )
})

//Buttonコンポーネント(子)
const Button = React.memo(({handleClick,value}) => {
  console.log('Button child component', value)
  return <button type="button" onClick={handleClick}>{value}</button>
})

//Countコンポーネント(子)
const Count = React.memo(({text, countState}) => {
  console.log('Count child component', text)
  return <p>{text}:{countState}</p>
})

コンポーネントをReact.memoでラップしてメモ化すると、初回ににTitleコンポーネント、Countコンポーネント2つ、Buttonコンポーネント2つがすべてレンダリングされました。

2回目以降、Titleコンポーネントについてはpropsがないので再レンダリングされていません。

Countコンポーネントについては、数字が更新されたコンポーネントについてのみ再レンダーされているので、最適化されています。

Buttonコンポーネントについては、ボタンのどちらかをクリックしたときにクリックされていないボタンも合わせ、2つのボタンが再レンダーされているので、最適化出来ていないようです。

Buttonコンポーネントは何故再レンダーされたか

Counter.jsx
//Counterコンポーネント(親)
const Counter = () => {

  const [firstCountState, setFirstCountState] = useState(0)
  const [secondCountState, setSecondCountState] = useState(10)

//+ 1 ボタンのstateセット用関数
  const incrementFirstCounter = () => setFirstCountState(firstCountState + 1)

//+ 10 ボタンのstateセット用関数
  const incrementSecondCounter = () => setSecondCountState(secondCountState + 10)

//子コンポーネントを呼び出す
  return (
    <>
      <Title/>
      <Count text="+ 1 ボタン" countState={firstCountState}/>
      <Count text="+ 10 ボタン" countState={secondCountState}/>
      <Button handleClick={incrementFirstCounter} value={'+1 ボタン'}/>
      <Button handleClick={incrementSecondCounter} value={'+10 ボタン'}/>
    </>
  )
}

<Button handleClick={incrementFirstCounter} value={'+1 ボタン'}/><Button handleClick={incrementSecondCounter} value={'+10 ボタン'}/>の2つのButtonコンポーネントについて、いずれかのボタンをクリックしたときに、stateが更新されるので再レンダーされますが、更新されていないほうのstateのボタンも再レンダーされています。一方のボタンがクリックされて親コンポーネントであるCounterコンポーネントが再レンダーされたタイミングで関数も再生成されており、再生成された関数をReact.memoが別の関数と認識したことによります。

React.memoについてもう少し詳しく

React.memoの第二引数には関数を渡すことができます。
第一引数として前回のprops(prevProps)を、第二引数として今回のprops(nextProps)を受け取ることが出来、真偽値を返すように書くことが出来ます。(areEqual)

const メモ化されたコンポーネント = React.memo(元のコンポーネント, (prevProps, nextProps) => {/* true or flase */})

このareEqual関数はpropsが等しいときにtrueを返し、propsが等しくないときにfalseを返します。
trueを返したときは再レンダーをスキップ、falseを返したときは再レンダーを行います。
(areEqualを省略した場合は、propsのshallow compareで等価性を判断することになります。)
また等価性のチェックにも、当然コストがかかることを考慮しなければなりません。

React公式サイト(React.memo)

useCallbackとReact.memoを組み合わせて最適化

親コンポーネントであるCounterコンポーネントが再レンダーされたタイミングで関数が生成性されないようにするため、useCallbackを使って最適化していきます。

//ReactからuseCallbackをimport
import React, {useState, useCallback} from 'react'

//Titleコンポーネント(子)
//React.memoでラップ
const Title = React.memo(() => {
  console.log('Title component')
  return (
    <h2>useCallBackTest vol.1</h2>
  )
})

//Buttonコンポーネント(子)
//React.memoでラップ
const Button = React.memo(({handleClick,value}) => {
  console.log('Button child component', value)
  return <button type="button" onClick={handleClick}>{value}</button>
})

//Countコンポーネント(子)
//React.memoでラップ
const Count = React.memo(({text, countState}) => {
  console.log('Count child component', text)
  return <p>{text}:{countState}</p>
})

//Counterコンポーネント(親)
const Counter = () => {

  const [firstCountState, setFirstCountState] = useState(0)
  const [secondCountState, setSecondCountState] = useState(10)

//+ 1 ボタンのstateセット用関数
//useCallbackで関数をラップし、依存配列には関数内で利用しているfirstCountStateを入れます。
  const incrementFirstCounter = useCallback(() => setFirstCountState(firstCountState + 1),[firstCountState])

//+ 10 ボタンのstateセット用関数
//useCallbackで関数をラップし、依存配列には関数内で利用しているsecondCountStateを入れます。
  const incrementSecondCounter = useCallback(() => setSecondCountState(secondCountState + 10),[secondCountState])

//子コンポーネントを呼び出す
  return (
    <>
      <Title/>
      <Count text="+ 1 ボタン" countState={firstCountState}/>
      <Count text="+ 10 ボタン" countState={secondCountState}/>
      <Button handleClick={incrementFirstCounter} value={'+1 ボタン'}/>
      <Button handleClick={incrementSecondCounter} value={'+10 ボタン'}/>
    </>
  )
}

export default Counter

うまく最適化出来ました!
関数をCounterコンポーネントの外に出せばいいんじゃないの!?って、そうなんですが、ここはuseCallbackの動作サンプルを作りたかったので。。。。:pray_tone2:
React.memoとuseCallback、さっそくリファクタリングに役立ちそうです:smiley:

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

React jsx 迷ったことまとめ

常時追加していくやつ。
React始めたばかりの初心者のため間違えていたらご指摘よろしくおねがいします。

Inputにて数字の入力のみを許すようにする

onInput={(e) => { e.target.value = e.target.value.replace(/[^0-9]/g, ''); }}

をInputタグに追加する。

<input
    type="text"
    className="form-control form-control-sm"
    name="なまえ"
    value={''}
    onChange={handleOnChange}
    onInput={(e) => { e.target.value = e.target.value.replace(/[^0-9]/g, ''); }}   // <-これ
/>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む