- 投稿日:2019-04-15T22:58:07+09:00
【webpack】をサクッとまとめる
概要
Webアプリケーションの開発で様々なnpmパッケージを使用する人にとっては必須のツールとなるwebpackについてサクッとまとめます!
前回の記事
Reactにおけるトランスパイラー【Babel】の役割続きはこちら
ReactにおけるComponentを解説!webpackとは
webpackはモジュールバンドラと呼ばれており、ソースコードを束ねて、ブラウザで実行できる静的なJavaScriptファイルを出力するものです。Webアプリの開発において様々なnpmパッケージを使う人にとっては必須のツールといっても過言でもないでしょう!
具体例
src/bar.jsexport default function bar() { //何かしらの処理 }src/index.js//bar.jsを読み込む import bar from './bar'; //bar.jsを実行 bar();
src/bar.js
とsrc/index.js
が上記のような関係にある時webpack.config.jsconst path = require('path'); module.exports = { //入力ファイルとしてsrc/index.jsを設定 entry: './src/index.js', //出力ファイルをdist/bundle.jsと設定 output: { path: path.resolve(__dirname, 'dist'), filename: 'bundle.js' } };のように
webpack.config.js
を書くとsrc/bar.js
とsrc/index.js
は1つのファイルにまとまります。page.html<!doctype html> <html> <head> ... </head> <body> ... <script src="dist/bundle.js"></script> </body> </html>そして上記のように、まとめた
dist/bundle.js
をpage.html
内で使用することによりアプリケーションが動作します。このように、いちいち
src/bar.js
やsrc/index.js
といったファイルをhtmlファイル上に指定するのではなくて、webpackが1つのbundle.jsというファイルにまとめてくれるので、html上では<script src="dist/bundle.js"></script>
のみ宣言すればアプリが動作してくれるようになるといった機能を提供してくれるのがwebpackになります。終わりに
webpack は WebApp に必要なリソースの依存関係を解決し、アセット(配布物)を生成するビルドツール(要するにコンパイラ)です。JavaScript だけでなく、CoffeeScript や TypeScript、CSS 系、画像ファイルなどを扱うことができます。
使ってみたい人はこちらのサイトを参考に使ってみましょう!
リファレンス
- 投稿日:2019-04-15T22:28:32+09:00
SPA で右クリックや shift + クリックなどによる別窓表示に対応させる
経緯
import React, { useCallback, useMemo } from 'react' import useReactRouter from 'use-react-router' import { myAction } from './myAction' export const Link = ({ href, children }) => { const { history } = useReactRouter() const onClick = useCallback(e => { myAction(href) history.push(href) e.preventDefault() return false }, [href]) const _href = useMemo(() => `#${href}`, [href]) return ( <a href={_href} onClick={onClick}> {children} </a> ) }上記のようなソースコードを書くと Mac の Meta + click や windows の ctrl + click を押した際に
別のタブで表示されるのではなく現在のタブでhistory.push(href)
が実行される。各種ライブラリがどのように対応しているか
ライブラリによってチェックしている項目が多少異なるようです。
良い感じに組み合わせてキミの最強のonClick
を作ろう!!!!!
gatsbyjs
の対応gatsby/packages/gatsby-link/src/index.js#L137
チェック項目 (一部
e.button !== 0
e.defaultPrevented
e.metaKey
e.altKey
e.ctrlKey
e.shiftKey
next.js
の対応next.js/packages/next/client/link.js#L60
チェック項目 (一部
nodeName === 'A'
e.nativeEvent.which === 2
!isLocal(href)
react router
の対応react-router/packages/react-router-dom/modules/Link.js#L15
チェック項目 (一部
(!this.props.target || this.props.target === "_self")
!isModifiedEvent(event)
- 投稿日:2019-04-15T21:46:35+09:00
Reactにおけるトランスパイラー【Babel】の役割
概要
前回の記事【ReactのJSXについて理解を深めよう!】においてちらっとトランスパイラーについて触れましたが、今回はさらにトランスパイラーについて深掘りしていきたいと思います!
続きはこちら
【webpack】をサクッとまとめるトランスパイラーとは
人間の職業で言うところの通訳者・翻訳者のような役割を果たしてくれます!
トランスコンパイラ(他にトランスパイラ、ソース・トゥ・ソースコンパイラ、などとも)は、あるプログラミング言語で書かれたプログラムのソースコードを入力として受け取り、別のプログラミング言語の同等のコードを目的コードとして生成する、ある種のコンパイラである。
JSXはそのままでは実行できず、トランスパイラーを使って実行可能なJavaScriptに変換する必要があります。その変換作業のことをトランスパイリング、変換することをトランスパイルと言います!
JSXで書いたコードは私たち人間には可読性の高いシンタックスとなっておりますが、残念ながらJavaScriptのシンタックスとは認識されません。
したがって、ブラウザやノードではそのコードは実行されません...
実は、前回の記事までに紹介したJSXのコードは自動的にJavaScriptに変換されていたのです!ではどのようにして自動変換されていたのでしょうか...
Babel
ReactではBabelというトランスパイラーがJSXを暗黙的にJavaScriptに変換してくれています。
では実際にJSXを書いてみて、BabelがどのようにJavaScriptに変換するのかを見てみましょう!まずはBabel REPLにアクセスしましょう
左側のスペースにJSXのコードを打つと、画像のように右側のスペースにリアルタイムでJavaScriptに変換されます。
このように、今まで書いてきたJSXはBabelを通してJavaScriptにトランスパイルされブラウザ上で実行されていたということです。
Reactを用いて実際にアプリケーションを作る上では、いちいちJSXのコードがどのようにJavaScriptに変換されているのか気にする必要はありませんが、裏ではBabel君が頑張ってくれていたんだな〜って思いながらコードを書くとBabel君も喜んでくれるのではないのでしょうか。
さらに詳しく知りたい方へ
Learn ES2015にアクセスしてみましょう!
リファレンス
- 投稿日:2019-04-15T21:00:22+09:00
CSS in JS ライブラリ「emotion」で、Atomic Design風なコンポーネントを作ってみる
はじめに
emotion は、パフォーマンス良くスタイルを構築する事を念頭に設計された CSS-in-JSライブラリです。
emotion の使い方を少し体験すると同時に、コンポーネントごとに適用するにはどう書いていくかを探るために、 Atomic Design のカードの例を参考に、サンプルアプリケーションを作成しました。
この記事は、そのサンプルアプリケーション作成にあたって、現時点で気になったポイントや迷いがあるポイントなどを記載しています。
書き方は色々あると思いますが、 styled の書き方がしっくりきたので、それを使って書いています。
サンプルアプリケーション
コードも含めて以下に置いています。
Next.jsのSandboxをベースに書いています。https://codesandbox.io/s/vnnl16yqny
Atoms
Atoms
は、「もっとも基本的な要素」で、それ以上分解することができないものなので、HTMLタグ1つで表現できるもの
として考えます。Atomsを利用するコンポーネントは
props
経由でデータをAtoms
のコンポーネントに渡します。下記の
card-image.js
というAtoms
のコンポーネントでは、画像のurlのみprops
で渡してもらいます。(厳密にはdefault値は必要かもしれません)atoms/card-image.jsimport styled from "@emotion/styled"; const Img = styled.img({ width: "64px", height: "64px", borderRadius: "32px", label: "a-card-image" }); const CardImage = props => <Img src={props.imgSrc} />; export default CardImage;
Atoms
には marginを設定しない 方がよいのではないかと考えています。理由として、 marginは周りの要素と組み合わさって初めて発生するものなので、Atoms
内で持つ事はないと思います。一方で、周りの要素を組み合わさって発生するmarginについては、付与すべき対象はこの
Atoms
のコンポーネントになると思います。そこで、
CSS props
の機能を使って、以下のように実現します。atoms/card-title.jsimport styled from "@emotion/styled"; const Div = styled.div( { /* atoms は margin を持たない */ fontSize: "14px", fontWeight: 600, color: "rgba(0, 0, 0, 0.7)", label: "a-card-title" }, props => ({ /* molecules として組み合わさる時に margin が付与される */ margin: props.margin, color: props.color }) ); const CardTitle = props => <Div {...props.overrideStyle}>{props.title}</Div>; export default CardTitle;上記では、
props.overrideStyle
からmarginのデータをもらい、上書きしています。
この機能を使えば、変更可能なプロパティをAtoms
側でコントロールできるので、上記の例だと、 「color
は変えて良い」と定義できるかなと思います。Molecules
Molecules
は、Atoms
を組み合わせたもので、独自の性質・機能を持つもの
として考えます。以下のように
Atoms
をimportしてきて、組み込みます。molecules/card.jsimport styled from "@emotion/styled"; import CardImage from "../atoms/card-image"; import CardTitle from "../atoms/card-title"; import CardDescription from "../atoms/card-description"; ... const Card = props => ( <Div {...props.overrideStyle.Card}> <CardImage imgSrc={props.imgSrc} /> <DivCardInfo> <CardTitle overrideStyle={combineStyle(overrideStyleOfCardTitle, { ...props.overrideStyle.CardTitle })} title={props.title} /> <CardDescription overrideStyle={combineStyle(overrideStyleOfCardDescription, { ...props.overrideStyle.CardDescription })} description={props.description} /> </DivCardInfo> </Div> ); export default Card;今回のサンプルでは、上位のまとまりである
Organisms
と同様に悩んでいる部分ではあるのですが、style
を親のコンポーネントから来るものと組み合わせて子のコンポーネントに渡す必要があります。そのマージ処理は今回以下のようにして作りました。
molecules/card.js... // 子コンポーネントの書き換え可能なstyleを上書きする const overrideStyleOfCardTitle = { margin: "4px 0 0 0" }; // 子コンポーネントの書き換え可能なstyleを上書きする const overrideStyleOfCardDescription = { margin: "8px 0 0 0" }; /* 親コンポーネントからのスタイル指定とmergeする方法 */ const combineStyle = (thisStyle, parentStyle) => { return Object.assign(thisStyle, parentStyle); }; const Card = props => ( ... <CardTitle overrideStyle={combineStyle(overrideStyleOfCardTitle, { ...props.overrideStyle.CardTitle })} title={props.title} /> ... );上記では、
combineStyle()
で、マージをしています。
マージの仕方は色々あると思っていて、どれを基本とするか、他にいい案がないかはまだ自分の中では固まっていないです。Organisms
Organisms
は、「原子・分子を組み合わせた比較的複雑な構造」で、Molcules
やAtoms
を組み合わせて作るまとまりとして考えます。このサンプルでは、シンプルなカードのリストをこのレイヤーに配置しています。
基本的にはOrganisms
と考える事は同じなのですが、このサンプルで1つハマった点がありました。
cssでよくある:first-of-type
といった擬似クラスのスタイルについてどうやって表現するかです。
この擬似クラスの受け口を子のコンポーネントに持たせると、他のプロパティと同じ方法で表現できますが、
それを子コンポーネントが持つのはどうなんだろうというのが、自分の中で疑問になりました。今回のサンプルでは、苦肉の策で以下のように実現させましたが、何か他にいい方法がないかは考えたいです。
organisms/card-list.js... // XXX: first-of-type の表現をpropsで渡すのがわからず、苦肉の策 // 回数に応じて適用するstyleを分岐させる。 const overrideStyleOfCard = (i, style) => { const defaultStyle = { margin: "8px 0 0 0" }; const firstChildStyle = { margin: "0" }; return i == 0 ? Object.assign(style.Card, firstChildStyle) : Object.assign(style.Card, defaultStyle); }; const CardList = ({ cardInfoList }) => ( <Div> {cardInfoList.map((data, i) => { overrideStyleOfCard(i, data.overrideStyle); return <Card {...data} key={i} />; })} </Div> ); ...Templates
Templates
は、「インタフェースの骨格」で、UIコンテンツ構造にフォーカスしたものです。これまでの
Atoms
からOrganisms
の作り方であれば不要なのではないかな?と思い、今回はこれに当たるコンポーネントは作りませんでした。Pages
Pages
は、実際のコンテンツを適用したもの。今回のコンポーネント設計では一番上位にあたるもの
と考えます。
そのため、ページの基本構造のDOMはここに記載していきます。pages/index.jsimport CardImage from "../atoms/card-image"; import CardTitle from "../atoms/card-title"; import CardDescription from "../atoms/card-description"; import Card from "../molecules/card"; import CardList from "../organisms/card-list"; import { H1, H2, H3 } from "../global-style/h"; // カードの情報 const redCardInfo = { ... } const greenCardInfo = { ... } const blueCardInfo = { ... } // カード情報リスト const sampleCardInfoList = [redCardInfo, greenCardInfo, blueCardInfo]; export default () => ( <section> <H1>Atomic Design Practice</H1> <p> このサンプルは、 <a href="https://uxdaystokyo.com/articles/glossary/atomic-design/"> https://uxdaystokyo.com/articles/glossary/atomic-design/ </a> のカードの例を参考に組み立てています。 </p> <div> <H2>Atoms</H2> <H3>CardImage</H3> <CardImage imgSrc={blueCardInfo.imgSrc} /> <H3>CardTitle</H3> <CardTitle title={blueCardInfo.title} /> <H3>CardDescription</H3> <CardDescription description={blueCardInfo.description} /> </div> <div> <H2>Molecules</H2> <H3>Card</H3> <Card {...blueCardInfo} /> </div> <div> <H2>Organisms</H2> <H3>Card List</H3> <CardList cardInfoList={sampleCardInfoList} /> </div> </section> );
Next.js
そのものをまだ勉強していないのですが、pages
ディレクトリ以下がルーティングの起点となっているようで、 偶然の一致かもしれませんが、相性はよさそうです。共通スタイル
例えば、hXタグは共通したスタイルを持ちたいなというときは、
common
的な場所に書いておき、import
して使うのが良さそうかなと思っています。global-style/h.jsimport styled from "@emotion/styled"; // hX の共通 style const HcommonStyle = { color: "#eee", padding: "8px", borderRadius: "4px" }; // h1 の style export const H1 = styled.h1( { ...HcommonStyle }, { fontSize: "24px", background: "#222", label: "g-h1" } ); // h2 の style export const H2 = styled.h2( { ...HcommonStyle }, { fontSize: "20px", background: "#555", label: "g-h2" } ); // h3 の style export const H3 = styled.h3( { ...HcommonStyle }, { fontSize: "14px", fontWeight: "500", background: "#999", label: "g-h3" } );pages/index.js... import { H1, H2, H3 } from "../global-style/h"; ... export default () => ( <section> <H1>Atomic Design Practice</H1> ...ディレクトリ構造的にどこに置くかは少し悩みどころですが・・・。
おわりに
今回は触れてませんが、 emotion は記法がいくつか選択できるので、初めてCSS-in-JSを使ってみた自分でも、基本的な使い方はすぐに慣れました。
課題として、 Atomic Design 風にコンポーネント分割していく際に、
style
の上書きをどうするかや、擬似クラスの表現の仕方があるのと、Next.js
で実際にアプリケーションを作っていくと、どういった壁が出てくるかが未知数です。一方で、これまではcss(scss)をFLOCSSで管理したりしていましたが、結局JS側のコンポーネントと乖離が出たりして、運用が長く続けば続くほど、辛い感じになってきている印象です。
これをCSS in JSで実装し、かつコンポーネントは Atomic Design の考え方をベースにしてルール化しておくと、チーム開発時に混乱が起きる確率が減るのではないかなと思っています。
まだまだ足りない観点やルールがありそうなので、これをベースにさらに作ってみて、新たな気づきがあれば、アップデートもしくは新規で書き起こそうと思います。
おまけ
実は上記サンプルは2つ目で、ボツにした1つ目は以下。
https://codesandbox.io/s/136z47n693overrideするstyleは全て上部でDOMを使って吸収するというやり方が大きく違います。どちらが良いかは好みかもしれませんが、対応しきれないケースがでるかなと思い、ボツにしました。
- 投稿日:2019-04-15T19:52:06+09:00
Vue.js初学者でも[確実に]ToDoアプリを動かしてVueコンポーネント間データ渡しを学ぶ
はじめに
前回のおさらいと今回すること
前回の記事ではVue.jsのHelloWorldを動かして基本事項を学びました。またコンポーネントという考えに触れ、実際に改良しました。
今回は、簡単なToDoアプリケーションを通して、複数のコンポーネントに分割してwebアプリの機能を作ることを初学者の状態から学びます。またその過程で求められるコンポーネント間のデータ渡しについて理解し、実装できるようになります。前回の状態の続きから実装を始めるので、前回の記事を読んでいたら[確実に]動かせるようになります。コードは更新のたびに、Githubにあげています。todoapp
Vuetify適応版なのでご注意を一連の連載で[確実に]理解すること一覧
Vue.jsを使ってHelloWorldを出力し、プロジェクト内のファイルの役割や仕組みを理解する 前回の記事
コンポーネントとは何かを理解し、HelloWorldを改良してページのQRコードを表示してみる。前回の記事
コンポーネント間の値の受け渡しの基礎を導入し、Vuexの意義を理解した上で導入する[本記事]
WebAPIを導入してフロントエンドとバックエンドでデータをやり取りし、実践的なサイトを作り上げる
ToDo管理アプリを実装してみよう
アプリの機能と構成について
ToDo管理アプリについて考えてみましょう。必要な機能は
ToDoタスクリストとして表示すること (=> タスクビュワーコンポーネント)
ToDoタスクの終了が管理できること (=> タスクリビュワーコンポーネント)
ToDoタスクの追加ができること (=> インプットタスクコンポーネント)
としましょう。今回はこの機能を実現するために上記のTaskViewコンポーネントとInputTaskコンポーネントの二つを実装します。前回同様にこれらのコンポーネントを貼り付けるためのキャンバスの役割を担うApp.vueも利用します。
コンポーネントが何の機能を持つのかは上述しましたが、これらのコンポーネント間でのデータのやり取りはどうなっているでしょうか?ここで一点だけ、全タスクデータは、App.vueが保持するという制約を加えます。それを踏まえて、ここではデータの移動を以下のように考えました。
タスクビュワーはタスクリストを受け取り、表示する
タスクビュワーは各タスクを終了状態にすることができるボタンを持ち、それを押すとタスクリスト中の該当タスクが終了状態になる
インプットタスクは入力フォームを持ち、入力されたタスクがタスクリストに追加される
コンポーネントの概念図は以下の関係になります。概念図のトップのApp.vueは二つのコンポーネントを内部に持ち、配置等を制御するためのコンポーネントであり、親コンポーネントと呼びます。
一方で、下の二つのコンポーネントは親から呼ばれたコンポーネントで子コンポーネントと呼びます。
TaskViewコンポーネントを実装してみよう。
子コンポーネントは親から値をもらって、表示しなければいけません。その橋渡しをする変数がPropsです。親は子供のProps変数に値を渡します。子はそれを受け取ります。
子コンポーネントの役割はもう一つあります。それはタスクの終了制御です。子供はタスクが終わった場合、親コンポーネントになんらかのアクションで通知します。親コンポーネントはボタンが押されたことを検知します。この橋渡しをする変数がEventです。PropsもEventも共通することとして親コンポーネントから呼び出すときに親と子で値の受け渡しがあることを宣言しておく必要があるということです。では実際に親から子コンポーネントを呼び出すコードを見てみましょう。
<taskView v-bind:tasks='tasks' v-on:child-event="TaskFinished"/>
この一文が親コンポーネント内のTemplate内に記述したコンポーネント呼び出しであることは前半の記事を読んだ方ならば分かるかと思います。前半のv-bind:tasks='tasks'
について
v-bind:'Props名'='データ名'
と書くことで「親は、'データ名'を'Props名'という名前で子に渡しますよ」という宣言です。後半の
v-on:child-event="TaskFinished"
については
v-on:'Event名'='親が呼び出すメソッド名'
ということで、「子から'Event名'のイベントが発火されたら、親は'親が呼び出すメソッドを' 実行しますよ」という宣言です。では実際のコードで見てみましょう。なお、HelloWorldコンポーネントの部分は今回は削除しましたが、あっても動きます。src/components/TaskView.vue<template> <div class='task'> <table> <!-- テーブル形式にして表示することにします。 <tr> <th>Task Name</th> <th>Status</th> </tr> <tr v-for='(task, index) in tasks' :key='index'> <!-- tasksの配列の要素をループして表示。 <td>{{ task.name }}</td> <!-- buttonアクション(v-on:click)にclickというメソッドを発火 <td v-if=task.flag ><button v-on:click='click(task.name)'> Done </button></td> <td v-else ><button v-on:click='click(task.name)'>Not Yet</button></td> <!--if文でflagを参照してボタンに表示名を変えている。 </tr> </table> </div> </template> <script> export default { name: 'TaskView', props: { tasks: Array // Array型のPropsを親から受け取ります 宣言 }, methods: { // template の部分のbuttonアクションで書いたものの実態はこれ。$emitで親に'child-event'というイベント名でイベントが起きたことを通知します。第二引数を指定すると、親に値を渡すことができます。 click: function (msg) { this.$emit('child-event', msg) } } } </script> <style scoped> </style>src/App.vue<template> <div id="app"> <img src="./assets/logo.png"> <taskView v-bind:tasks='tasks' v-on:child-event="TaskFinished"/> <!-- コンポーネントの作成文。 <!-- 前半は子のtasksというProps名に親のtasksというデータを渡す。 <!-- 後半は子がchild-eventという名前でイベントを投げたら、親はTaskFinishedというメソッドを発火する宣言 </div> </template> <script> import TaskView from './components/TaskView' export default { name: 'App', components: { TaskView }, data () { // ここでtaskの配列を与える。今回は{name(タスク名): String, flag(終了フラグ): Boolean} return { tasks: [ { name: 'No1', flag: false }, { name: 'No2', flag: true }, { name: 'No3', flag: false } ] } }, methods: { // 子からイベント名(event-child)を受け取って発火する関数。具体的に作り込んでいく部分。 TaskFinished: function (msg) { this.tasks.forEach(value => { if (value.name === msg) { if (value.flag === true) { value.flag = false } else if (value.flag === false) { value.flag = true } } }) } } } </script> <style> #app { font-family: 'Avenir', Helvetica, Arial, sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; text-align: center; color: #2c3e50; margin-top: 60px; } </style>表示は以下のようになっていて、ボタンを押して'Done'と'NotYet'が切り替われば成功です。
InputTaskを実装してみよう
タスクの登録では、親のもつタスクリストの配列を操作することになります。つまりTaskViewで出てきたようなEvent通知処理でタスク追加を通知し、受け取った親側で配列にタスクを追加します。といっても今回タスクは{タスク名,状態}しか保持しない上に初期状態では状態もfalseであることを考えると、タスク名のみを通知すればよいことがわかるでしょう。
src/components/InputTask.vue<template> <div class='InputTask'> <form v-on:submit.prevent='addtask(taskname)'> <!-- formタグ内に書いていきます。v-on:submitとv-on:clickという2パターンのイベントを書いていますが、発火する関数はどちらもaddtaskです。 <input type='text' v-model='taskname' placeholder='input the task'/> <!-- コンポーネントがもつtasknameというdataにinputの入力値を即時反映させるためにv-modelというオプションにtasknameを指定。 <button v-on:click.prevent='addtask(taskname)'>Submit</button> </form> </div> </template> <script> export default { name: 'InputTask', data () { return { taskname: '' } }, methods: { // v-onで定義したアクションから呼ばれる関数です。v-on:clickもv-on:submitもどちらもchild-eventという名前で親に通知します。親からしたらエンターキーで追加されたか、submitボタン押で追加されたかはどうでもよく、イベント発火のみを判断して処理したいからです。 addtask: function (msg) { this.$emit('child-event', msg) this.taskname = '' // イベントを通知したら一応、初期化します。 } } } </script> <style scoped> .input{ width: 130pt; height:30pt; } .button { display: block; position: relative; margin: 0 auto; width: 70pt; border: solid 1px silver; border-radius: 0.5rem 0.5rem; padding: 0.5rem 1.5rem; margin-top: 1rem; text-decoration: none; } </style>App.vue<template> <div id="app"> <img src="./assets/logo.png"> <InputTask v-on:child-event="TaskAdded"/> <!-- コンポーネントを作成。先ほど同様イベント通知を受ける宣言をここに。 受けたときに呼ぶ関数は'TaskAdded'と指定。 <taskView v-bind:tasks='tasks' v-on:child-event="TaskFinished"/> </div> </template> <script> import TaskView from './components/TaskView' import InputTask from './components/InputTask' export default { name: 'App', components: { TaskView, InputTask }, data () { return { tasks: [ { name: 'No1', flag: false }, { name: 'No2', flag: true }, { name: 'No3', flag: false } ] } }, methods: { TaskFinished: function (msg) { this.tasks.forEach(value => { if (value.name === msg) { if (value.flag === true) { value.flag = false } else if (value.flag === false) { value.flag = true } } }) }, TaskAdded: function (msg) { // InputTaskのイベントを受けて発火する関数。 // {受け取ったタスクの名前と初期状態false}でデータ配列の末尾に追加。デバッグ用にコンソール出力も console.log(msg) this.tasks.push({ name: msg, flag: false }) } } } </script> <style> #app { font-family: 'Avenir', Helvetica, Arial, sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; text-align: center; color: #2c3e50; margin-top: 60px; } </style>以下のような画面になり、エンターでもSubmitボタンを押すことでもタスクが追加できたら成功です。
終わりに
今回はコンポーネント間でのデータのやり取りの必要性を理解し、実際に親子間でのデータやりとりをToDoアプリを作成することで実践しました。では、例えばコンポーネントが20~30個のアプリになったり親子関係がさらに深くなったり、、、と複雑化していった場合に、全ての変数を今回のようなやり方でやり取りするのが適切でしょうか?
各コンポーネントから参照可能なデータストアがあれば楽なのにと感じることも出てくるかもしれません。それこそがQiitaでもたくさんの記事が書かれているVuex(ReactユーザならばRedux)の生まれた経緯です。次回はこのVuexを今回作成したToDoアプリに導入してみましょう。一連の連載
- 投稿日:2019-04-15T17:42:03+09:00
雰囲気で使わない React hooks の useCallback/useMemo
React hooks にはメモ化のための useCallback と useMemo という関数があります。
hooks を使い始めて、この二つの関数を知った私はこう思いました。「え?無条件でパフォーマンス上がるんなら全部これで書くべきやん!」
と。
というわけで、しばらくそのスタンスで書いてきたのですが、果たしてこの「無条件でパフォーマンスが上がる」という前提は本当に正しいのか、というかそもそも"パフォーマンス上がる"とは具体的に何をしてくれるのかを理解せずに使っていたので、ここで「全て useCallback/useMemo で書く」という方針が正しいのか、それとも他の方針が存在するかを考えてみました。
大きく次の3つの観点で考えます。
- パフォーマンス
- 可読性
- バグの発生しやすさ
1.パフォーマンス
「そもそも useCallback/useMemo はパフォーマンス向上の用途なのに、パフォーマンス観点で気にすることあるんか?」
と思った方もいらっしゃるかもしれません。まあ私もそう思っていたんですが、昔も「PureComponentにするとむしろパフォーマンスが落ちるケースがある」みたいなこともあったので、無思考にそう信じるのもよろしくないと思うのでこれをしっかり理解してみようと思います。
useCallback/useMemo 自体の処理コストを考える
先ほどの前提が正しいかを確認するためには「useCallback、useMemoを使った時のコスト」と「使わなかった時のコスト」を比べる必要があります。
まず useCallback/useMemo の処理のコストというのは「メモ化した値(厳密言うと deps ですが)との比較」です。
hooks は基本的に dependencyList(この記事ではこれ以降 deps と呼びます) と呼ばれるものを配列とした引数に取り、その値が変わった時に値が更新されるようになっています。例として一度 mount した後の useCallback の処理である updateCallback を見てみましょう
react/ReactFiberHooks.js at master · facebook/react · GitHub
function updateCallback<T>(callback: T, deps: Array<mixed> | void | null): T { const hook = updateWorkInProgressHook(); const nextDeps = deps === undefined ? null : deps; const prevState = hook.memoizedState; if (prevState !== null) { if (nextDeps !== null) { const prevDeps: Array<mixed> | null = prevState[1]; if (areHookInputsEqual(nextDeps, prevDeps)) { return prevState[0]; } } } hook.memoizedState = [callback, nextDeps]; return callback; }この中の areHookInputsEqual というものが比較のための関数みたいですね。これも見てみましょう。
function areHookInputsEqual( nextDeps: Array<mixed>, prevDeps: Array<mixed> | null, ) { // いろいろ warning 割愛 for (let i = 0; i < prevDeps.length && i < nextDeps.length; i++) { if (is(nextDeps[i], prevDeps[i])) { continue; } return false; } return true; }指定された deps 毎に
is
という関数で比較しているようです。さらにそれも見てみましょう。react/objectIs.js at master · facebook/react · GitHub
function is(x: any, y: any) { return ( (x === y && (x !== 0 || 1 / x === 1 / y)) || (x !== x && y !== y) // eslint-disable-line no-self-compare ); }(全然話逸れるが、なぜこれが Object.is と同値なのか全く分からない。教えて強い人。)
なのでこの is関数 x deps に指定された要素の数が useCallback/useMemo 自体のコストと考えて良さそうです。次に使わない場合のコスト、言い換えると useCallback/useMemo がどんな状態の時にどんな最適化をもたらしているかを見ていきましょう。
useMemo の恩恵
これまで useCallback/useMemo とひっくるめて語っていましたが、ここからは分けて考えます。
useMemo は計算した結果を保持するための関数です。
const memoedValue = React.useMemo(() => /* 何かしらの複雑な計算 */, [])なので useMemo を使わない場合の処理コストは、中身で行なっている計算によります。 その計算が is関数 x deps の数より重ければ useMemo を使った方がお得ということになります。ただいちいち「これは useMemo 使った方がパフォーマンスいいのか?」と考えるのもそれはそれで生産性低いので、ある程度自動的に使う使わないの判断ができるような軸が欲しい…と思いましたが、いい感じの軸が思いつきませんでした。
例えば次のようなものは useMemo 使うまでもないとパッと見で分かると思うんですが、
const checkedAll = checkedIds.length === items.lengthもう少し複雑なものがどっちがお得かは実際に検証してみないと分からないでしょう。一々そんなことをするのも馬鹿らしいので、疑わしきものはとりあえず useMemo みたいな方針が濃厚なのかなという気がします。
useCallback の恩恵
まず useCallback による最適化は useMemo の純粋にメモ化することによって計算量を抑えるのとは意味合いが違います。
実際に検証してみたわけではないですが(めんどくさい)、関数インスタンスを作成するコストは先ほどの is関数の比較するコストよりおそらく低いでしょう。このコストだけ考えると「あれ?useCallbackってコスト増えるだけで意味なくね?」と思うかもしれませんが、そうではありません。
useCallback は不要に新しく関数インスタンスを作成することを抑制することによって不要な再描画を減らしてくれます。詳しい話はこちらの記事のアンチパターンとして挙がっている「アロー関数をpropsに即時関数で渡す」を読むと分かりやすいと思います。こちらの記事では bind を使うことによって新しく関数が作成されることを防いでいますが、 useCallback を使うことにより Class でなくとも同じことができるようになります。
逆に言うと、子供のコンポーネントに対して関数を渡すようなことがなければ、特に useCallback を使う意味はないということです。(厳密に言うと PureComponent のように Props の変化に対して shallow equal で再描画判定を行うような場合限定なのですが、親が子のコンポーネントがどうやって再描画判定しているのかを気にするのも不自然な話なので、子に関数を渡している場合は useCallback で包んでおけばいいと思います。)
まとめ
まとめるとパフォーマンス観点では次のようになります。
- useMemo
- 疑わしきものは大体useMemo使っておけばおk
- useCallback
- 子に関数の参照を渡す場合は useCallback 使っておく。そうでない場合は使っても意味ない
余談: メモリに関して
ちなみに何ですがメモ化と聞くと、単純な実行速度や計算量の話だけでなく「メモリをめっちゃ食ってしまうことはないの?」と疑問を抱く方がいらっしゃるかもしれませんが、こと hooks のメモ化に関しては特に問題がありません。
というのも一般的なプログラミングの関数のメモ化とは違い、 hooks のメモ化は直前の値しか記憶していないからです。なので useCallback/useMemo を使っている場合と使っていない場合でメモリの使用量は変わりません。
試しに先ほども登場した useCallback のアップデート処理を見てみましょう。
function updateCallback<T>(callback: T, deps: Array<mixed> | void | null): T { const hook = updateWorkInProgressHook(); const nextDeps = deps === undefined ? null : deps; const prevState = hook.memoizedState; // deps が変わっていない場合は前回の State を返す if (prevState !== null) { if (nextDeps !== null) { const prevDeps: Array<mixed> | null = prevState[1]; if (areHookInputsEqual(nextDeps, prevDeps)) { return prevState[0]; } } } // 変わっている場合は新たな State に上書きして返す hook.memoizedState = [callback, nextDeps]; return callback; }ご覧の通り
hook.memoizedState
は一つだけです。これは deps が変わった時に「上書き」されます。このため、メモ化によるメモリの使い過ぎについては特に心配する必要はありません。2.可読性
次に可読性です。当然ですが useCallback/useMemo を使うと1個包む関数が増えるので普通に記述するよりいささか冗長になります。
const hoge = () => console.log('fuga')const hoge = React.useCallback(() => console.log('fuga'), [])うん、微妙に読みづらいですね。
ただ、書いといてなんですが、個人的にはこの観点どうでもよいと思っています。他の観点の方がより本質的な課題なのでこれが決め手になるというのはあり得ないなと。ただ気まぐれに useCallback/useMemo 使っているところとそうでないところが散乱しても気持ち悪いので、方針はちゃんと揃えておきたいなという気持ちです。
3.バグの発生しやすさ
これは実体験からの感想ですが、useCallback/useMemo(特に useCallback の方)を使う方がバグが増えます。
理由としては useCallback を使った際に dependencyList をちゃんと書いていないと、古い値を保持してしまうためです。
ちなみにかの Dan 先生はこのような値の保持を stale closures と呼んでいます。直訳で古いクロージャという意味ですね。So far it seems like the biggest confusion point with Hooks is stale closures. It’s also the one missing in the docs. In hindsight we should have documented that in detail. But with a new paradigm it’s a bit difficult to know what ends up an issue in practice.
— Dan Abramov (@dan_abramov) 2019年2月22日もし全く useCallback/useMemo を使わない場合は、パフォーマンス改善はないもののとりあえず常に最新の値を参照してくれるため、また、多くの人はこの挙動を期待したメンタルモデルでプログラミングしていると思うので、意図しない挙動は減る印象です。
防ぐ方法
上記のようなバグを防ぐための基本的なプラクティスとしては「useCallback/useMemo の中で参照する要素は全て deps に書く」です。とりあえずそうしておけば必ず最新の値には保たれます。
また、このリスクを軽減する方法として eslint の
exhaustive-deps
があります。[ESLint] Feedback for 'exhaustive-deps' lint rule · Issue #14920 · facebook/react · GitHub
この lint rule を入れれば、 deps が必要な useXXX の関数を使った時に、 deps に列挙されていない要素があれば warning を出してくれるようになります。
小ネタ: deps に書きたくない時もある
基本的には全部列挙した方がいいというのはそうなんですが、あえてそうしたくない場合もあります。
分かりやすいのが古くは componentDidMount でやっていたような「Mount時に一回だけ」やりたいような処理は useEffect を使いつつも deps には何も入れたくありません。それ以外にも小技として useCallback 内で useState の set系の関数を実行する時に、引数に関数を入れ、deps には何も入れないことによって関数の再生成を抑えることができます。詳しくはこちらの記事にまとまっていますが、この記事でも軽く解説します。
React Hooks、useStateの更新関数引数には関数を - Qiitaまずは普通に deps に定義した例
function Hoge() { const [count, setCount] = React.useState(0) const increment = React.useCallback(() => { setCount(count + 1) }, [count]) return ( <div> <Button onClick={increment}>ふやす</Button> {count} </div> ) }これでも動きはしますが、
count
がアップデートされる度に関数が生成されてしまいます。
useCallback 内では以前の状態と相対的に状態を更新しているだけです。そして useState の更新関数では関数を引数に与えると、その関数の引数に現在の状態が入ってきます。これを利用することによって次のような書き方でも意図通り動くようになります。function Hoge() { const [count, setCount] = React.useState(0) const increment = React.useCallback(() => { setCount(_count => _count + 1) }, []) return ( <div> <Button onClick={increment}>ふやす</Button> {count} </div> ) }こうすることによって、useCallback での関数生成はマウント時に1回きりだけでよくなります。
なにが言いたいかというと、「deps に全て列挙する」が常に正しいわけではなく、意図的に列挙しない指定しないケースもいくつかあるのでルールは柔軟にしていきたいね、ということです。この辺も型化して自動で判定していけるようにできればいいですね。
結論
長々と語ってきましたが、結論としては次のような感じで書いていくのがいいかなと思います。
- useCallback/useMemo が効果を発生する状況をちゃんとチームで周知する
- 自動化で判定するのは現状難しそうなので、知識共有して個々で適材適所使いどころを判断する
- eslint の ehaustive-deps を可能であれば利用する
- なんらかの形で「参照している要素を deps に指定する」を実現できればよい
- ただしあえて指定したくないケースもあるので柔軟に対応する
"適材適所"というフワフワした結論にはなってしまいましたが、ご意見あればぜひコメントください!
お読みいただきありがとうございました?♂️
- 投稿日:2019-04-15T14:22:37+09:00
connected-react-routerとredux-persistを共存させる
connected-react-routerとredux-persistの両方を一つのプロジェクト内で共存させようとするとよく分からなくなるのでまとめ
説明のためのベースプロジェクトは
githubのconnected-react-routerのexamples/basic/
を利用準備
$ git clone https://github.com/supasate/connected-react-router.git $ cd connected-react-router/examples/basic $ npm install $ npm starthttp://[実行しているサーバーのIP]:8080/
でサンプル画面を表示
redux-persistがない場合の動作
Counterを表示
カウントを操作
react-router経由でページ移動した後戻った場合は状態が保存されている
↓
↓
ページの更新やアドレスバーを書き換えた場合状態はクリアされる
↓
redux-persistを導入する
npm install redux-persist
src/configureStore.jsの編集
src/configureStore.jsimport { createBrowserHistory } from 'history' import { applyMiddleware, compose, createStore } from 'redux' import { routerMiddleware } from 'connected-react-router' import createRootReducer from './reducers' // モジュールの追加 +import { persistReducer } from 'redux-persist' +import storage from 'redux-persist/lib/storage' // redux-persistの設定 +const persistConfig = { + key: 'root', + storage, +} export const history = createBrowserHistory() // 元のrootReducerからpersistedReducerを生成 +const persistedReducer = persistReducer(persistConfig, createRootReducer(history)) // store生成時に参照するrootReducerをpersistedReducerに差し替え export default function configureStore(preloadedState) { const composeEnhancer = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose const store = createStore( - createRootReducer(history), + persistedReducer, preloadedState, composeEnhancer( applyMiddleware( routerMiddleware(history), ), ), ) // Hot reloading if (module.hot) { // Enable Webpack hot module replacement for reducers module.hot.accept('./reducers', () => { store.replaceReducer(createRootReducer(history)); }); } return store }src/index.jsの編集
src/index.jsimport { AppContainer } from 'react-hot-loader' import { Provider } from 'react-redux' import React from 'react' import ReactDOM from 'react-dom' import App from './App' import configureStore, { history } from './configureStore' // モジュールの追加 +import { persistStore } from 'redux-persist' +import { PersistGate } from 'redux-persist/integration/react' const store = configureStore() // 元のstoreからpersistStoreを生成 +const pstore = persistStore(store) // AppコンポーネントをPersistGateの配下にする const render = () => { ReactDOM.render( <AppContainer> <Provider store={store}> - <App history={history} /> + <PersistGate loading={<p>loading...</p>} persistor={pstore}> + <App history={history} /> + </PersistGate> </Provider> </AppContainer>, document.getElementById('react-root') ) } render() // Hot reloading if (module.hot) { // Reload components module.hot.accept('./App', () => { render() }) }これで再度実行するとページの更新を掛けても状態が保存されている
↓
react-routerをredux-persistから除外する
ただしこのままではreact-routerのLinkを経由しないアドレスバーからのパス入力にて
/や/helloや/counterなどを入力してページ遷移をしようとしても最後にreact-routerのLink経由で移動したページにリダイレクトされてしまうこれを回避するためには
src/configureStore.jsのpersistConfigに保存したくない要素をブラックリストとして登録する登録する対象はブラウザのデベロッパーツールにてlocalStorageを確認すると以下の様に保存されているので
routerをブラックリストに登録すればよさそうであるsrc/configureStore.jsconst persistConfig = { key: 'root', storage, + blacklist: ['router'], }
これでページリロードに対して状態を保存を保持しつつアドレスバーを有効に使えるようになる
- 投稿日:2019-04-15T12:42:38+09:00
Reactで複数のタグを使いたい時
未だにFlagmentの省略した書き方してない人もいると思うので書き残しておきます。
Flagmentってなに?
JSXのreturnでは、一つの要素しか返しちゃいけないルールがあるので何かのタグでくくらないといけないんだけど
<div>
を使うと無駄な<div>
があちこちで発生してしまうため、Flagmentってもので記述をするやり方。
確かclassとか属性値も普通の要素のように割り当てられるんじゃなかったかな?(適当な記憶)全体を
<div>
で囲むのは遅れてる。昔のコードをみると
<div>
で囲っているコードよくありますよね。それしかできなかったんだから仕方ないです。でも今は、もっといい方法があるので覚えておいてください。const Sample = () => { return( <div> <h1>Hello World</h1> <p>サンプルコード</p> </div> ); }; export default Sample;なぜダメなのか
無駄な
<div>
を生成しちゃうから
<React.Flagment>
のコード
<div>
の次に考えられたのが<React.Flagment>
でコンポーネントを囲む方法。
これで記述すれば無駄な<div>
コードを生み出さなくていい少しだけ全体的にネストが浅くなるはずです。const Sample = () => { return( <React.Fragment> <h1>Hello World2</h1> <p>サンプルコード2</p> </React.Fragment> ); }; export default Sample;なぜダメなのか
記述が長い。いちいち
<React.Flagment>
なんて書いてられないよ...。本当は、
<>
これでいいんだよ結論は、これ。
これは、<React.Flagment>
を省略しただけの書き方
最近React Hooksの公式ドキュメント見たときも使われていたし
ガンガンこれ使っちゃっていいと思う。const Sample = () => { return( <> <h1>Hello World3</h1> <p>サンプルコード3</p> </> ); }; export default Sample;これで終わりです。
Happy hacking!?