- 投稿日:2020-06-29T23:58:50+09:00
コンポーネント設計で悩んでいる人へ。簡単にコンポーネント設計できるESLint Plugin作りましたよ??
この記事について
この記事は、筆者が作ったESLint プラグインの紹介記事なります。
「 なぜ作ったのか? 」「 コンポーネント設計とは何か? 」という議題に触れ、コンポーネント設計の難しさやこのプラグインの有意性について語っています。
筆者は専門家では無いので間違っている所や認識のズレなどがあるかもしれませんが、もしあった場合は編集リクエストやコメントにて指摘してくれると嬉しいです。後、現在この記事で紹介するプラグインは「 コンポーネントは JSX で記述されている 」ことを想定しています。
作ったモノ
Specific Component Designを導入するためのELint プラグインです。
プラグインの詳細は、github のドキュメントに日本語で書いていますので、是非見てみて下さい。
以下の画像は、github へのリンクカードです。インストール方法
npmに公開しているので、以下のコマンドでインストールする事が出来ます。
npmでインストール$> npm install eslint-plugin-scdyarn でも大丈夫です。
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, }, ], }, };現在のところ
jsx
とtsx
の方では動作している事を確認しています。
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 はコンポーネント単位が小さい順に、
- Nue
- Otem
- Pafe
- Page
- Module
の 5 つの分類があり、それぞれの分類には明確な分類基準があります。
それら明確な基準は以下のようになります。Nue
Nue は最小単位のコンポーネントを分類します。
規約
- コンポーネントファイルを
nues
またはnue
フォルダー下に入れる- 要素数( html タグやコンポーネントの数 )は、0~5 個以内する
- State は持たせない
- Hooks の使用禁止
Otem
Nueよりも大きなコンポーネントを分類します。
規約
- コンポーネントファイルを
otems
またはotem
フォルダー下に入れる- 要素数( html タグやコンポーネントの数 )は、5~10 個以内する
- State は持たせない
- Hooks の使用禁止
Pafe
NueやOtemよりも大きなコンポーネント、または State を保持ているコンポーネントを分類します。
規約
- コンポーネントファイルを
pafes
またはpafe
フォルダー下に入れる- 要素数( html タグやコンポーネントの数 )が10 個以上、または State を保持している
- Hooks を使用してもよい
Page
Page
は、ページを構成するコンポーネントを分類します。規約
- コンポーネントファイルを
pages
またはpage
フォルダー下に入れる<main></main>
を最低一つ持たせる- Hooks を使用してもよい
Module
Module
は、上記のどれにも分類できない例外的なコンポーネントを分類します。
Module
には明確な規約はありません。自由に設計できます。※ Module に分類するのは、どうしても他のモノに分類出来ない時です。なるべくNue・Otem・Pafe・Pageに分類できるようにして下さい。
使ってみた感じ
以下の gif 画像は、プラグインを導入してNueコンポーネントを定義した時の挙動です。( VSCode の拡張を使ってます )
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 />
の二つのコンポーネントが使っています。
注目すべき点は、ExplorePage
とMyEntriesPage
の双方ともに<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 を渡すのに、true
・false
それぞれ二通りずつあります。これは明らかに、再利用性
が損なわれていることが分かります。つまり、
拡張性
を上げた結果、再利用性
が失われています!これが
再利用性
・拡張性
が相反すると言った所以です。
また、これらはboolean型だけの話ではなく、Propsの数が多くなったり、別のデータ型になったとしても同じです。再利用性と拡張性にどう向き合うか?
再利用性
と拡張性
はコンポーネント作るための意義であり、双方とても大事な要素です。
しかし双方が相反しているため両者を高く保つことは困難で、状況に応じてどちらかを高く保つ必要があるかという事にコンポーネント設計者は頭を悩ませています。私はこの問題について再利用性と拡張性を維持することを諦め明確な基準を設けることで、
再利用性
と拡張性
を最低限保持することで対応しています。そして、この「 明確な基準を設ける 」というのがこの記事で紹介している ESLint プラグインやSCDを作った理由です。
長々と話してきましたが、結局のところ
再利用性
と拡張性
を高く保つ事は難しく、考えた末の結果は最低限の再利用性
と拡張性
は保持するという所に落ち着きました。これが良いか悪いかで言えば、それはたぶん人それぞれでしょう。あなたは eslint-plugin-scd を使うべきか?
もしあなたがコンポーネント設計を複数人で行う場合は、この記事で紹介している ESLint プラグインのようなコンポーネント設計を強制するESLint プラグインを使うべきであると思います。
なぜなら、人によってコンポーネント設計の基準は違っており、それが色々な人為的ミスにつながるからです。
もしそうでないなら、この記事で紹介している ESLint プラグインを使っても使わなくても大丈夫でしょう。つまり、このプラグインを使うべき人は以下のような人たちだと思います。
- コンポーネント設計に自信が無い人
- コンポーネント設計に労力をかけたくない人
- 複数人でコンポーネント設計を行う必要がある人
あとがき
ここまでコンポーネント設計について語ってきましたが、正直説明するのが難しく、私の考えている事の一割も説明できてないように思えて、自分の説明力の無さに落胆していますが、あくまでESLint プラグインの紹介記事なので、プラグインの事だけでも知ってもらえたら幸いです。
コンポーネント設計の闇は、State とかキャッシュが入ってきた途端に一気に深くなりますよね ?
私はフロントエンドで一番難しいのがコンポーネント設計だと思ってますが、皆さんはどうでしょうか?そこまで難しくないでしょうか?
ここら辺の認識は、使うフレームワークなどによって結構変わってくるかもしれませんね。Anguler だと悩まないかも ?またフロントエンドはデザインのところばかり注目されて、こういった設計があるって事を外部の人から理解してもらえない事が多々あるので、この記事で少しでもコンポーネント設計の難しさについて理解してもらえたらいいなと思っています。
最後に、今回作ったESLint プラグインですが、もしかしたら既存のプラグインで同じような事は出来るかもしれません。
ただ今回は調べるのが面倒くさかったのと、前々から ESLint プラグイン作ってみたかったので作ってみました。
色々と学べたのでいい機会だったと思います ?もしSCDのルールが気に入らない人は
.eslintrc.js
などの設定ファイルで、ルールをカスタマイズして使うこともできますので、是非使ってみて下さい。それでは ?
拡張性という言葉はあまり良くないと思ってますが、他にいい言葉が思い浮かばなかったので拡張性としています。 ↩
- 投稿日:2020-06-29T18:04:24+09:00
3Dシーンを作ってすぐ公開 Three.js+Gatsby+TypeScriptによるモダンWebXRテンプレート
この記事はWebXR Tech Tokyo #1の発表で使用いたしました。
イベントを企画いただいた運営の皆様、会場を盛り上げていただいた参加者の皆様、ありがとうございました!この記事でできるようになること
Three.jsを使った3Dウェブページをモダンな技術スタックを使って簡単に開発・公開できるようになります。オリジナルテンプレートを使うことでスムーズに始められ、またGatsbyを用いることで自分好みのカスタマイズを簡単に加えることが可能です。
以下のサンプルシーンは予めテンプレートに組み込まれております。本記事での解説に加え、必要に応じてテンプレート内の実装を参考にしていただき、ご自身で3Dシーンを作成する際のお役立ていただければ幸いです。
使用する技術スタック
Three.js
TypeScript
Gatsby
Netlify
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のサンプルページにあるものを少しカスタマイズしたものになります。
ベースシーンの解説
src/scenes/BaseScene.tsx
にカスタム用のシーンを追加しました。最低限の必要なコンポーネントのみを記載しています。BaseScene.tsximport 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 BaseScene3DシーンをWebで実装する場合に最も重要なコンポーネントとして、
Camera
,Scene
,Renderer
の3つがあります。Mesh
は表示されるオブジェクトを表しており、基本的にGeometry
とMaterial
の2つから構成されてます。今回はシンプルな立方体オブジェクトのMeshが表示対象となっています。実行してコンテンツを確認してみましょう。localhost:8000
から確認できます。カスタムシーンを追加する
上記のベースシーンをカスタマイズしてみます。カメラとオブジェクトを以下のように修正します。
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つ現れます。
正面からだとよく分からないので、立方体だとわかるように回転を加えてみます。
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) } ...ここまでで動きのある3Dオブジェクトを作成できましたが、少し味気ないため次節で表面に動きを付けていきます。
GLSLの追加
WebGLで何かを描画するためには2つのシェーダが必要になります。シェーダを描くための言語GLSL(グラフィクス・ライブラリ・シェーダー言語)を利用したサンプルもThree.jsのコードにはありますが、あくまで素のHTMLを読み込むことを想定した書き方なのでそのまま利用することができません。Reactを利用する場合は以下のように記述していきます。
頂点シェーダ
CustomScene.tsxconst vert = ` varying vec2 vUv; void main() { vUv = uv; vec4 mvPosition = modelViewMatrix * vec4( position, 1.0 ); gl_Position = projectionMatrix * mvPosition; } `フラグメントシェーダ
CustomScene.tsxconst 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 ); } `最初に用意した
MeshNormalMaterial
をShaderMaterial
に置き換えます。CustomScene.tsxconst 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) }表面の鮮やかな模様が時間と共に変化している動的な描画になりました。
シェーダとGLSLに関しては以下のサイトがわかりやすかったためリンクを添付します。
WebGLのシェーダーとGLSLここまでのソースコードは以下のようになります。
CustomScene.tsximport 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 BaseSceneGlitchの追加
Post Processing(後処理)を追加することでシーンにプラスアルファの味を加えてみます。今回は画面が割れるような振動を表現できるGlitchというフィルターを追加してみます。
Post Processingを追加するためには
EffectComposer
をインポートする必要があります。Three.jsではこのようにサンプルとして便利なライブラリを提供してくれていますので、Reactを使用する場合は次のようにインポートします。CustomScene.tsximport 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' // 追加フィルターをシーンに導入するには、
EffectComposer
にRenderPass
とGlitchPass
を以下のように追加したあと、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} /> </> ) ...右上に小さく文字が表示されているため、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を覆い隠してしまうとそれらが向こうになる場合がありますので、実現したい表現によっては注意が必要です。
OBJファイルのロード
新しいシーンで3Dオブジェクトファイルの取り扱いを説明していきます。3Dモデルフォーマットにはいくつか種類がありますが、今回は
.obj
形式のファイルをOBJLoader
を使用して取り込んで見たいと思います。CustomScene2.tsximport 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ファイルをローカルにコピーしておきましょう。相対パスでファイルを指定します。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
を開くと以下のシーンが表示されます。ここまでで3Dシーンに関する説明は以上です。同様に
page
ディレクトリ以下にファイルを作成することで新たなカスタムページを作成することができます。Three.jsはWeb上にたくさんのサンプルが落ちていますので、上記の基本をベースにサンプルを動かしつつ自身のポートフォリオを作成していってください。Webページの公開
GitHubへ変更をプッシュ
最後に、ここまでの変更をWeb上で公開するため、まずは自身のGitHubリポジトリに対してソースコードをプッシュしておきましょう。
Netlifyの設定
Netlifyを使うことで簡単にポートフォリオサイトを公開できます。リンクからNetlifyのページに飛び、自身のページにログインをしてください。初めての方はサインアップが必要になります。右上にある「New site from Git」から新しいサイトを作成するページに飛びます。ソースコードがGitHubにある場合はGitHubボタンを選択して次に進みます。
今回保存されたソースコードを含むリポジトリを選択します。ビルド設定はデフォルトで問題ありません。「Deploy site」ボタンを押するとビルドが始まり、しばらくするとサイトが公開されます。
また、Netlify公開用にランダムで割り当てられた文字列のURLではなく、独自の名前を設定することも可能です。
Setting→Domain Management→Domains→Custom domains→Options→Edit site nameから選択できます。設定したURLからページが公開されていることを確認してみましょう。
あとがき
Three.jsの公式サンプルページには、様々な表現方法を用いたカタログが一覧になっています。サンプルコードとセットになっており、新しく3Dシーンを作成する際にはとても参考になるでしょう。
一方、長い歴史を持つThree.jsは、今ではあまり使われない記法で記述されたコードが多いのも現状です。
今回のテンプレートではTypeScriptやES6以降の記法を取り入れつつ、現場で使われることの多いReactをベースに作成しました。react-three-fiberというライブラリも存在しますが、Three.jsのバージョンアップに追随できる、かつ特定のフレームに依存しないなるべく素のThree.jsを扱えるようにカスタマイズ性の高いテンプレートとして公開しています。オープンソースですのでお気軽にPR、スター等いただけると今後の活動の励みになります。また、3Dのポートフォリオジェネレータとは別に、ソーシャルメディアのポートフォリオジェネレータを別途開発中です。今後はSNSだけではなく3Dアバターなどもクリップできるようにしていく予定です。現在α版としてテストユーザを募集しておりますので、ご興味がありましたらこちらまでご連絡をお願いいたします。
お気に入りのポストを好きなSNSから、好きな数だけNoCodeでクリップできるポートフォリオサイトです。「複数のSNSを1つのURLで管理したい」「埋もれてしまった過去のポストをクリップしておきたい」場合に便利です。SNS版前略プロフィール?のようなイメージでお楽しみください。
https://storygate.info最後までご一読いただきありがとうございました。
- 投稿日:2020-06-29T15:08:01+09:00
【React Hooks】兄弟間でイベントを飛ばす
TL;DR
- React で、兄弟コンポーネント間でイベントを送りたい。
useEffect()
ってイベントっぽくない?useState()
の本来と違う使い方なのでカスタムフックに抽出しよう。背景
- Functional Component で書きます。
- 親コンポーネントPとその子コンポーネントAとBがあります。
- Aは複数選択可なアイテムで、新たに選択されたアイテムに対して、
- Web APIを叩いて得られた結果を全て保持し、Bでグラフとして表示します。
このようなアプリを作ろうとすると、非同期処理が入るので、選択された値の配列をPに保管して、Bに Attribute として渡してその中で処理しようと考えると、結果をキャッシュしないといけません。
キャッシュを実装するのが面倒だったので、そこで、イベント駆動プログラミングの出番です。Aで新たに選択された値をBに渡して、B内で処理させましょう。子コンポーネントでのデータの使い方について親が知る必要がないほうがたぶん楽なので。
以下は単純化されたサンプルアプリケーションで話を進めます。
useEffectって、イベント駆動なのではないか
Reactには、
useEffect(effect, deps)
というHookがあります。この引数deps
は、引数effect
として渡される関数は、前回のrender時と今回のrender時でdeps
(配列)の値のどれかが変わるときに実行されます。(初回render時にも実行されます。)
これって、EventやReactive ExtentionのStreamと似ていませんか?似てますよね。index.tsxinterface 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> ) }こうすると、
DispSum
・DispHistory
各コンポーネントは独立にイベントを受け取ってコンポーネント内で処理・表示することが出来ています。本来の使い方とちょっとずれてる?
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> ) }TODO
複数のEmitterに依存するように書き換えることもできるはずなので次回でやってみます。
- 投稿日:2020-06-29T10:58:43+09:00
[React & TS] styled-componentsで動的にスタイルを変更する
やりたいこと
よくみるこれをReact & TypeScript とstyled-componentsで実装したい
作ったもの
今回はstyled-componentsに真偽値を渡しておき、値を変更する形としました。
styled-components本家にやり方が記載されています。
hamburgerIcon.tsximport 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などに怒られてしまいます。
以下の通り、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> }
- 投稿日:2020-06-29T10:15:18+09:00
React hooksを基礎から理解する (useCallback編) と React.memo
React hooksとは
React 16.8 で追加された新機能です。
クラスを書かなくても、state
などのReactの機能を、関数コンポーネントでシンプルに扱えるようになりました。
- React hooksを基礎から理解する (useState編)
- React hooksを基礎から理解する (useEffect編)
- React hooksを基礎から理解する (useContext編)
- React hooksを基礎から理解する (useReducer編)
- React hooksを基礎から理解する (useCallback編) 今ここ
- React hooksを基礎から理解する (useMemo編)
- React hooksを基礎から理解する (useRef編)
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.jsximport 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 Counterconsole.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で等価性を判断することになります。)
また等価性のチェックにも、当然コストがかかることを考慮しなければなりません。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の動作サンプルを作りたかったので。。。。
React.memoとuseCallback、さっそくリファクタリングに役立ちそうです
- 投稿日:2020-06-29T00:10:43+09:00
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, ''); }} // <-これ />