20191023のReactに関する記事は7件です。

Gatsby.js + NetlifyCMSのデータの流れまとめ(+投稿画面、URLのカスタマイズ)

Gatsby(とNetlifyCMSを使ったとき)のデータの流れを書きたいと思います。
どのディレクトリ/ファイルが何をしているか、俯瞰できる記事がなかったので。
また、ある箇所をカスタマイズするためにはどのファイルをいじればいいかも逆引き的に書きました。
この記事では、CMSでの投稿画面やURLのカスタマイズをしています。

Gatsbyの説明や、Netlifyとの連携については他の記事に任せます。
ちなみに使ったスターターはgatsby-starter-netlify-cmsです。(https://github.com/netlify-templates/gatsby-starter-netlify-cms)
このスターター特有の話があったらごめんなさい!

データの流れ

Gatsby概略図.png

CMSで記事を書く

マークダウンファイルができる

マークダウンファイルからデータを抜き出してGraphQLに入れる

GraphQLからデータを取り出して、テンプレートに埋め込む

って感じです。
流れがわかったので、具体的に、ある箇所をいじるためにはどのファイルを触ればいいか見ていきます。

逆引き ~これをするにはどこを触ればいいのか~

CMSの投稿編集ページの管理 → static/admin/config.yml

このファイルでCMSの投稿画面をカスタマイズします。
デフォルトだと、PostとPagesしか編集できません(左側)。
この編集可能なページの種類のことをcollectionと言うらしいです。
netlifycms.png
static/admin/config.ymlのcollectionsに設定を追加することで、他のフォルダのページを作成/編集することができるようになります。
例えば、Post、Pagesの他にSpecialPostという種類のページを編集できるようにしたいときは以下を追加します。
簡単のため、fieldは、埋め込むテンプレートとタイトルだけにしています。
fieldのwidgetの種類などは以下を参照のこと。
https://www.netlifycms.org/docs/widgets/

static/admin/config.yml
collections:
  - name: "specialPost" #Gatsby(GraphQL)内でのフィールド名
    label: "SpecialPosts" #CMS上での項目名
    folder: "src/pages/SpecialPosts" #ページを置きたいディレクトリ
    create: true #CMS上で新規作成可能かどうか
    fields: #ページの持つフィールド(項目)名(widgetがhiddenだと編集できない)
      - {label: "Template Key", name: "templateKey", widget: "hidden", default: "index-page"}
      - {label: "Title", name: "title", widget: "string"}

スクリーンショット 2019-10-22 22.33.15.png
無事、新たにSpecialPostがcollectionに追加されました!

CMSでのプレビュー表示を変える → src/cms/cms.js と src/cms/preview-templates/

上記のカスタマイズでPostとPages以外も編集できるようになっても、プレビューがうまくいかないと思います。
まだプレビューテンプレートの登録がないからです。
ってことでsrc/cms/cms.jsでプレビューテンプレートを登録してあげます。

src/cms/cms.jsにsrc/cms/preview-templates/から使いたいプレビューテンプレートをimportし、
CMS.registerPreviewTemplate(追加したcollectionのname, インポートしたプレビューテンプレート)
を追加してあげればOK。

テンプレートのレイアウトを変えたい → src/components/

この中にSassとかReactコンポーネントが入っているので、これをいじればよいです。

ページに対して使うテンプレートを変えたい → static/admin/config.yml + src/templates/

各ページのmdファイルにはtemplateKeyが書いてあります。
このtemplateKeyと同名の、src/templates/以下のjsファイルをテンプレートに使っています(その設定はgatsby-node.jsに書いてあります)。
なので、mdファイルのtemplateKeyを、使いたいテンプレートの名前にしてあげましょう。

また、そもそもCMSでテンプレートの指定を変えてあげることもできます。

CMSでデフォルトテンプレートを変える

mdファイルのtemplateKeyはもともとstatic/admin/config.ymlからきています。
例えばblogですと、collectionsのblogのfieldsに
- {label: "Template Key", name: "templateKey", widget: "hidden", default: "blog-post"}
があります。
このデフォルト値が設定されているのです。

widgetが"hidden"なので、CMSにはtemplateKeyの編集欄は表示されず、デフォルト値の"blog-post"が設定されたままです。
そしてmdファイルにtemplateKey: "blog-post"が記されるわけです。
なので、このwidgetを"hidden"から"string"に変えてあげます。
するとCMSにTemplate Keyというラベルで文字列が編集できる欄が現れます(一番上)。
スクリーンショット 2019-10-22 22.13.44.png

この欄に、使いたいテンプレートの名前を書いてあげれば良いのです。
これで無事テンプレートを使い分けられるようになりました。

collectionごとに使うテンプレートを変えないのであれば、widgetを"hidden"のままにして、defaultだけ使いたいテンプレート名にしてあげてもいいと思います。

サイトメタデータ、プラグイン管理 → gatsby-config.js

gatsby-config.jsではサイトのタイトルやディスクリプションの設定やプラグインの追加や削除等の管理ができます。

URLのカスタマイズ → static/admin/config.yml + gatsby-node.js

URLをカスタマイズします。
まずデフォルトでは、ページのslugがURLになっているのが、gatsby-node.jsを見るとわかります。

gatsby-node.js
posts.forEach(edge => {
      const id = edge.node.id
      createPage({
        path: edge.node.fields.slug, // <= ここ!
        tags: edge.node.frontmatter.tags,
        component: path.resolve(
          `src/templates/${String(edge.node.frontmatter.templateKey)}.js`
        ),
        // additional data can be passed via context
        context: {
          id,
        },
      })
    }

で、このslugがどこから来ているのかと言うと、その下の箇所

gatsby-node.js
exports.onCreateNode = ({ node, actions, getNode }) => {
  const { createNodeField } = actions
  fmImagesToRelative(node) // convert image paths for gatsby images

  if (node.internal.type === `MarkdownRemark`) {
    const value = createFilePath({ node, getNode })
    createNodeField({
      name: `slug`,
      node,
      value,
    })
  }
}

createFilePathでファイルのパスを取得してるっぽいですね。
てことで、urlというフィールドをページごとに持たせて、それを前者のcreatePageのpathのvalueに持たせればいいわけです。

今回は、urlフィールドが設定されているページはそれをurlにして、設定されていないページはデフォルトの(slugを使った)URLにしようと思います。

URLフィールドの作成

1.CMSへのURLフィールドの追加
static/admin/config.ymlの、URLを追加したいcollectionのfieldに
- {label: "URL", name: "url", widget: "string"}
を追加します。
これでCMS上でURLフィールドを編集できるようになりました。

2.GraphQLにurlフィールド入れる
CMSでURLフィールドを設定したら、それを GraphQLに入れます。
node.frontmatter.urlでurlフィールドの値を取り出し、createNodeFieldでurlフィールドをGraphQLに入れます。
もしURL設定がなく、node.frontmatter.urlがないなら、代わりにデフォルトのslugを入れる設定もしています(valueにはslugが入っている)。

gatsby-node.js
exports.onCreateNode = ({ node, actions, getNode }) => {
  const { createNodeField } = actions
  fmImagesToRelative(node) // convert image paths for gatsby images

  if (node.internal.type === `MarkdownRemark`) {
    const value = createFilePath({ node, getNode })
    const url = node.frontmatter.url
    createNodeField({
      name: `slug`,
      node,
      value:url ? url : value //urlが設定されていなかったらデフォルトのslug
    })

  }
}

これでめでたく、URLをCMS上で自分で設定できるようになりました!

まとめ

GraphQLもNetlifyCMSも初見で、何がどうなってるのか、どこに何があって何してるのかもさっぱりでしたが、だいぶ見通しが良くなって良かったです!
Gatsbyのこと書いてるブログはだいたいGatsbyなので、調べものも速くて気持ちいいですね!
流行ってほしいなGatsby。

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

Express+TypeScript だけで React を SSR する

Express+TypeScript だけで React を Server Side Rendering (SSR) してみます。Babel は使用しません。

TypeScript は JSX に対応しているので、JSX を変換するだけなら Babel は不要です。

ディレクトリ構成

.
├── app.ts
├── createEngine.ts
├── package.json
├── pages/
│   └── Hello.tsx
└── tsconfig.json

設定ファイル

package.json
{
  "name": "react-ssr-ts",
  "version": "1.0.0",
  "license": "CC0-1.0",
  "private": true,
  "scripts": {
    "dev": "ts-node-dev --no-notify app.ts",
    "build": "tsc",
    "start": "NODE_ENV=production node dist/app.js"
  },
  "dependencies": {
    "express": "^4.17.1",
    "react": "^16.11.0",
    "react-dom": "^16.11.0"
  },
  "devDependencies": {
    "@types/express": "^4.17.1",
    "@types/react": "^16.9.9",
    "@types/react-dom": "^16.9.2",
    "ts-node-dev": "^1.0.0-pre.43",
    "typescript": "^3.6.4"
  }
}

ts-node-dev はファイルに変更があると自動で再起動してくれるツールです。ts-node と node-dev を掛け合わせたようなやつです。

start で NODE_ENV=production としていますが、その事情は後ほど。

tsconfig.json
{
  "compilerOptions": {
    "target": "es2019",
    "module": "commonjs",
    "jsx": "react",
    "outDir": "./dist",
    "strict": true,
    "esModuleInterop": true
  }
}

"jsx": "react" がミソです。tsc が JSX を React.createElement に置き換えてくれるので、普通に JS として実行可能になります。

ページ

pages/Hello.tsx
import React from 'react';

type HelloProps = {
  name: string;
};

const Hello: React.FC<HelloProps> = ({ name }) => (
  <h1>Hello, {name}</h1>
);

export default Hello;

ページは pages/ に置いていますが、app.ts の設定でディレクトリ名を自由に指定できます。

ページのコンポーネントは export default してください。

テンプレートエンジン

createEngine.ts
import React from 'react';
import ReactDOMServer from 'react-dom/server';

export interface EngineOptions {}

export const createEngine = (engineOptions?: EngineOptions) => {
  const renderFile = (path: string, options: object, callback: (e: any, rendered: string) => void): void => {
    const component = require(path).default as React.ComponentType<any>;
    const markup = ReactDOMServer.renderToStaticMarkup(
      React.createElement(component, options)
    );
    return callback(null, markup);
  };
  return renderFile;
};

tsx ファイルをテンプレートとして使うためのテンプレートエンジンです。渡された js または tsx ファイルを ReactDOMServer.renderToStaticMarkup で HTML の文字列に変換しています。

EngineOptions は特に使用していないので、createEngine でラップする必要はないのですが、将来の拡張用にとりあえずこうしています。

エントリポイント

app.ts
import express from 'express';
import { createEngine } from './createEngine';

const app = express();

const ext = app.get('env') === 'production' ? 'js' : 'tsx';
app.set('views', __dirname + '/pages');
app.set('view engine', ext);
app.engine(ext, createEngine());

app.get('/:name', (req, res) => {
  res.render('Hello', { name: req.params.name });
});

app.listen(8080, () => {
  console.log('> Ready on http://localhost:8080/');
});

先ほどのテンプレートエンジンを app.engine に登録します。ページのディレクトリ名を変えたい場合は、app.set('views', __dirname + '/pages') のところを変えてください。

view engine の値を js と tsx で分けている理由ですが、ts-node-dev で実行した場合 tsx が渡されてくる一方、tsc して node dist/app.js した場合 js が渡されてくるので、それぞれの場合でテンプレートの拡張子を変える必要があります。このあたりもう少しスマートにできないものか・・・

おわり

以上です。意外と簡単に React の SSR ができました。

普通は Next.js を使うのがいいと思います(Dynamic Routing で難がありますが)。

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

Express+TypeScript で React をテンプレートエンジンとして使用する

Express+TypeScript で Server Side Rendering (SSR) して、React をテンプレートエンジンとして使用してみます。Babel は使用しません。

TypeScript は JSX に対応しているので、JSX を変換するだけなら Babel は不要です。

ディレクトリ構成

.
├── app.ts
├── createEngine.ts
├── package.json
├── pages/
│   └── Hello.tsx
└── tsconfig.json

設定ファイル

package.json
{
  "name": "react-ssr-ts",
  "version": "1.0.0",
  "license": "CC0-1.0",
  "private": true,
  "scripts": {
    "dev": "ts-node-dev --no-notify app.ts",
    "build": "tsc",
    "start": "node dist/app.js"
  },
  "dependencies": {
    "express": "^4.17.1",
    "react": "^16.11.0",
    "react-dom": "^16.11.0"
  },
  "devDependencies": {
    "@types/express": "^4.17.1",
    "@types/react": "^16.9.9",
    "@types/react-dom": "^16.9.2",
    "ts-node-dev": "^1.0.0-pre.43",
    "typescript": "^3.6.4"
  }
}

ts-node-dev はファイルに変更があると自動で再起動してくれるツールです。ts-node と node-dev を掛け合わせたようなやつです。

tsconfig.json
{
  "compilerOptions": {
    "target": "es2019",
    "module": "commonjs",
    "jsx": "react",
    "outDir": "./dist",
    "strict": true,
    "esModuleInterop": true
  }
}

"jsx": "react" がミソです。tsc が JSX を React.createElement に置き換えてくれるので、普通に JS として実行可能になります。

ページ

pages/Hello.tsx
import React from 'react';

type HelloProps = {
  name: string;
};

const Hello: React.FC<HelloProps> = ({ name }) => (
  <h1>Hello, {name}</h1>
);

export default Hello;

ページは pages/ に置いていますが、app.ts の設定でディレクトリ名を自由に指定できます。

ページのコンポーネントは export default してください。

テンプレートエンジン

createEngine.ts
import React from 'react';
import ReactDOMServer from 'react-dom/server';

export interface EngineOptions {}

export const createEngine = (engineOptions?: EngineOptions) => {
  const renderFile = (path: string, options: object, callback: (e: any, rendered: string) => void): void => {
    const component = require(path).default as React.ComponentType<any>;
    const markup = ReactDOMServer.renderToStaticMarkup(
      React.createElement(component, options)
    );
    return callback(null, markup);
  };
  return renderFile;
};

tsx ファイルをテンプレートとして使うためのテンプレートエンジンです。渡された js または tsx ファイルを ReactDOMServer.renderToStaticMarkup で HTML の文字列に変換しています。

EngineOptions は特に使用していないので、createEngine でラップする必要はないのですが、将来の拡張用にとりあえずこうしています。

エントリポイント

app.ts
import express from 'express';
import { createEngine } from './createEngine';

const app = express();

const isTsNodeDev = Object.keys(require.cache).some(path => path.includes('/ts-node-dev/'));
const ext = isTsNodeDev ? 'tsx' : 'js';
app.set('views', __dirname + '/pages');
app.set('view engine', ext);
app.engine(ext, createEngine());

app.get('/:name', (req, res) => {
  res.render('Hello', { name: req.params.name });
});

app.listen(8080, () => {
  console.log('> Ready on http://localhost:8080/');
});

先ほどのテンプレートエンジンを app.engine に登録します。ページのディレクトリ名を変えたい場合は、app.set('views', __dirname + '/pages') のところを変えてください。

view engine の値を js と tsx で分けている理由ですが、ts-node-dev で実行した場合 tsx が渡されてくる一方、tsc して node dist/app.js した場合 js が渡されてくるので、それぞれの場合でテンプレートの拡張子を変える必要があります。このあたりもう少しスマートにできないものか・・・

追記: (2019-10-24)
@suin さんの『ts-node-devで起動されたかを調べる方法』を参考に、NODE_ENV で判定していたところを、ts-node-dev で起動されたかどうかで判定するよう書き換えました。ありがとうございます!

おわり

以上です。意外と簡単に React をテンプレートエンジンとして使うことができました。

普通は Next.js を使うのがいいと思います(Dynamic Routing で難がありますが)。

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

【React】Reactドキュメントで勉強してみた

概要

Reactの公式ドキュメントを読みながら勉強した際の備忘メモです。
Reactを動かしながら学習したかったので、環境はDockerでサクッと作った感じです。
(環境構築は記述していないです。)

前提

Javascriptの知識
自信のない方は、Javascriptチュートリアルで知識を確認してから進める。

React Documents

公式DocsのMAIN CONCEPTを進めていきました。
項目1.Hello Worldは飛ばして、次項の2.JSXの導入からになります。
またドキュメント内のJavascriptのおさらい的な部分は省いています。
最後の12.Reactの流儀は検索可能な商品データ表をReactで作っていく様子が記述されています。
流儀と言われ畏れ多いので、公式ドキュメントに任せます。(面倒なだけ)

1.Hello World

前提の話などなので、飛ばします。

2.JSXの導入

javascriptの拡張構文(プログラミング言語)

導入は必須ではないが、有用な見た目、有用なエラー警告を多く表示してくれる。

式の埋め込み

{}(中括弧)でjavscriptの式を利用できる

embed.js
const name = 'taro';
const element = <h1>Hello {name}</h1>;

ReactDOM.render(element, document.getElementbyId('root'));

JSXも式である

コンパイル後、JSXは普通のJavascriptの関数呼び出しに変換されて、Javascriptオブジェクトとして評価される。

formula.js
function getGreeding(user){
  if(user){
    return <h1>Hello {user}.</h1>;
  }
  return <h1>Hello foo.</h1>;
}

属性指定

文字列リテラルを属性として指定するために""(引用符)を使用できる。

また、Javascript埋め込む際は、{}(中括弧)を使用する。

Javascriptを使用時には、中括弧を囲む引用符を使用しない。

str.js
const element = <div className="string"></div>;  // 文字列

const foo = "string";
const fooElement = <div className={foo}></div>;  // Javascript

const fighters = "dave";
const fightersElememt = <div className="{fighters}"></div>; //  NG

キャメルケースのプロパティ命名規則

JSXはHTMLよりもJavascriptに近いので、HTML属性ではなく、キャメルケースの命名規則を使用する。

class.js
<div class="class_name"></div> // NG
<div className="class_name"></div> // OK

子要素指定

子要素を持つことができる。
また、中身がない場合はタグを閉じる(/>する)。

child.js
const child = (
  <div>
    <h1>Hello React</h1>
    <h2>Hello JSX</h2>
  </div>
);

const close = <img src={foo} />;

JSXはインジェクション攻撃を防ぐ

デフォルトでReactDOMは、JSXに埋め込まれた値をレンダリングされる前にエスケープするため、レンダーの前に全てが文字列に変換される。これはXSS攻撃の防止になる。

xss.js
const title = response.malInput;
const element = <h1>{title}</h1>;

JSXはオブジェクトの表現

BabelはJSXをReact.createElement()の呼び出しへコンパイルする。

下記の2つは同じになる。

element.js
const element = (
  <h1 className="greeting">
    Hello React.
  </h1>
);


const element = React.createElement(
  'h1',
  {className: 'greeting'},
  'Hello React.'
);

3.要素のレンダー

要素をDOMとして描画する

Reactだけで構成されたアプリケーションは、通常一つのDOMノードだけを持つ。

React要素をDOMノードにレンダリングするには、

ReactDOM.render()に、React要素(第1引数)とDOMノード(第2引数)を渡す。

root.html
<div id="root"></div>

rootのidを持つDOM要素に、elementをレンダーする

root.js
const element = <h1>Hello React</h1>;
ReactDOM.render(element, document.getElementbyId('root'));

レンダリングされた要素の更新

React要素はImmutableのため一度作成すると変更ができない。

更新する方法は、新しい要素を作成して、ReactDOM.render()に渡す。

以下は、秒ごとに動く時計の例です。

clock.js
function Clock(){
  const element = (
    <div>
      <h1>It is {new Date().toLocaleTimeString()}.</h1>
    </div>
  );
  ReactDOM.render(element, document.getElementbyId('root')
  }

setInterval(clock, 1000);

必要な箇所のみ更新する

ReactDOMは要素とその子要素を、以前のものと比較し必要なだけの要素を更新する。

4.componentとprops

コンポーネントを定義する方法としては、javascriptの関数、またはes6のクラスを記述する。
propsという任意の入力を受け取りReact要素を返す。

詳細はコンポーネントAPIリファレンス

関数 component

データの入ったpropsというオブジェクトを引数として受け取り、React要素を返す。

func.js
function Hello(props){
  return <p>Hello {props.name}.</p>;
}

クラス component

class.js
class Hello extends React.Component{
  render(){
    return <p>Hello {this.props.name}.</p>;
  }
}

componentのrender

DOMのタグを表すReact要素だけではなく、ユーザ定義のcomponentを表すことができる。

Reactがユーザ定義のコンポーネントを見つけた場合、JSXの属性を単一のオブジェクトとして、コンポーネントに渡す。

このオブジェクトのことはpropsと呼ぶ。

comp.js
function Hello(props){
  return <p>Hello {props.name}</p>;
}

const element = <Hello name="aiueo" />;

ReactDOM.render(
  element,
  document.getElementById('root')
);

上記の処理の流れ
1: <Hello name="aiueo" />という要素を引数として、ReactDOM.render()を呼び出す。
2: Helloコンポーネントを呼び出し、propsとして、{name: 'aiueo'}を渡す。
3: Helloコンポーネントは<p>Hello aiueo</p>を返す。
4: ReactDOMは<p>Hello aiueo</p>に一致するように効率的に更新する。

コンポーネントを組み合わせる

コンポーネントは自信の出力の中で、他のコンポーネントを参照できる。

↓Helloを何回もレンダリングするAppを作成する。

conbine.js
function Hello(props){
  return <p>Hello {props.name}</p>;
}

function App(){
  return(
    <div>
      <Hello name="Taro" />
      <Hello name="Jiro" />
      <Hello name="Saburo" />
    </div>
  );
}

ReactDOM.render(
  <App />,
  document.getElementById('root')
);

componentを抽出する

コンポーネントを分割する(コンポーネントをより小さいコンポーネントに分割することを恐れないでください、とのこと)。

以下のCommentコンポーネントについて考えてみる。

base.js
function Comment(props){
  return(
    <div className="comment">
      <div className="userinfo">
        <img 
          src={props.author.imageUrl}
          alt={props.author.name}
          className="avatar"
        />
        <div className="username">
          {props.author.name}
        </div>
      </div>
      <div className="text">
        {props.text}
      </div>
      <div className="date">
        {formatDate(props.date)}
      </div>
    </div>
  );
}

多くのネストがあるため、内部の個々の部品の再利用は困難になる。

↓ ここでコンポーネントを抽出する

userinfo部分を抽出する
自身がComment内にレンダリングされていることは知っておく必要はない。
そのため、コンポーネントのコンテキストからではなく、自身のコンポーネントの観点からpropsの名前を定義する。

avatar.js
function Avatar(props){
  return(
    <img 
      src={props.user.imageUrl}
      alt={props.user.name}
      className="avatar"
    />
  );
}

userinfo.js
function UserInfo(props){
  render(
    <div className="userinfo">
      <Avatar user={props.user} />
      <div className="username">
        {props.user.name}
      </div>
    </div>
  );
}

↓ よりシンプルにする。

alternateBase.js
function Avatar(props){
  return(
    <img 
      src={props.user.imageUrl}
      alt={props.user.name}
      className="avatar"
    />
  );
}

function UserInfo(props){
  render(
    <div className="userinfo">
      <Avatar user={props.user} />
      <div className="username">
        {props.user.name}
      </div>
    </div>
  );
}

function Comment(props){
  return(
    <div className="comment">
      <UserInfo user={props.author} />
      <div className="text">
        {props.text}
      </div>
      <div className="date">
        {formatDate(props.date)}
      </div>
    </div>
  );
}

propsは読み取り専用

Reactは柔軟だが、1つ厳格なルールがある。

コンポーネントは自分自身のpropsは決して変更してはいけない。

以下、公式ドキュメントから引用

全ての React コンポーネントは、自己の props に対して純関数のように振る舞わねばなりません。

5.stateとライフサイクル

componentのカプセル化

Clockコンポーネントを再利用可能かつカプセル化する。

clock.js
function Clock(props){
  return(
    <div>
      <p>Hello React</p>
      <p>{props.date.toLocaleTimeString()}</p>
    </div>
  );
}

function tick(){
  ReactDOM.render(
    <Clock date={new Date()} />,
    document.getElementById('root')
  );
}

setInterVal(tick, 1000);

↑ UIを毎秒ごとに更新する処理は、Clockの内部実装であるべき処理なので、ReactDOM.render()部分を一度のみ記述して、Clock自身を更新させたい。
ここでstateを利用する。
statepropsに似ているが、コンポーネントに管理されるプライベートなもの。

関数をクラスに変換する

以下の流れで関数コンポーネントをクラスに変換する。

1.React.Componentを継承する同名のes6のクラスを作成。

2.render()と呼ばれる空のメソッドを1つ追加。

3.関数の中身をrender()メソッドに移動。

4.render()内のpropsthis.propsに書き換える。

5.空になった関数の宣言部分を削除。

以下コード

clock.js
class Clock extends React.Component{
  render(){
    return (
      <div>
         <p>Hello React</p>
         <p>{this.props.date.toLocaleTimeString()}</p>
      </div>
    );
  }
}

function tick(){
  ReactDOM.render(
    <Clock date={new Date()} />,
    document.getElementById('root')
  );
}

setInterVal(tick, 1000);

これにより、render()は更新した際に毎回呼ばれるが、同一のDOMノード内で<Clock />をレンダーしている限り、Clockクラスのインスタンスは1つだけ使われる。
このことにより、ローカルstateやライフサイクルメソッドといった追加機能を利用できる。

ローカルstateを設定

以下のステップでdatepropsからstateに移動する。

1.render()メソッド内のthis.props.datethis.state.dateに書き換える。

2.this.stateを初期化するクラスコンストラクタを作成する。

3.<Clock />要素から、dateプロパティを削除する。

以下コード

clock.js
class Clock extends React.Component{
  constructor(props){
    // クラスのコンポーネントは常に props を引数として親クラスのコンストラクタを呼び出す必要がある。
    super(props);  
    this.state = {
      date: new Date()
    };
  }

  render(){
    return (
      <div>
         <p>Hello React</p>
         <p>{this.state.date.toLocaleTimeString()}</p>
      </div>
    );
  }
}

function tick(){
  ReactDOM.render(
    <Clock />,
    document.getElementById('root')
  );
}

setInterVal(tick, 1000);

ライフサイクルメソッド

多くのコンポーネントを持つアプリケーションでは、コンポーネントが破棄された場合にそのコンポーネントが持っていたリソースを開放することがとても大切。

Clockを例にすると、

・タイマーを設定したい時 => DOMとして描画される時(マウント(mounting)と呼ぶ)
・タイマーをクリアしたい時 => 生成したDOMが削除される時(アンマウント(unmounting)と呼ぶ)

コンポーネントクラスは特別なメソッドでマウント、アンマウントする時のコードを実行できる。
これらのメソッドはライフサイクルメソッドと呼ばれる。詳しくはReact.Component – Reactで確認。

clock.js
class Clock extends React.Component{
  constructor(props){
    super(props);
    this.state = {
      date: new Date()
    };
  }

  // 出力がDOMにレンダーされた後に実行される
  componentDidMount(){
    this.timerID = setInterval(
      () => this.tick(),
      1000
    );
  }

  // コンポーネントがDOMから削除されるときに呼び出される
  componentWillUnmount(){
    clearInterval(this.timerID);
  }

  // コンポーネントのローカルstateの更新をスケジュールするためにthis.setState()を使用する
  tick(){
    this.setState({
      date: new Date()
    });
  }

  render(){
    return (
      <div>
        <p>Hello React</p>
        <p>{this.state.date.toLocaleTimeString()}</p>
      </div>
    );
  }
}

ReactDOM.render(
  <Clock />,
  document.getElementById('root')
);

上記の処理の流れ
1.<Clock />ReactDOM.render()に渡され、Clockコンポーネントのコンストラクタを呼び出す。
Clockは現在時刻を表示するので、現在時刻を含んだオブジェクトでthis.stateを初期化する。そしてこのstateを更新していく。

2.Clockコンポーネントのrender()メソッドを呼び出し、Reactは画面に何を表示すべきか知る。そして、DOMをClockのレンダー出力と一致するように更新する。

3.Clockの出力がDOMに挿入されると、componentDidMount()メソッドを呼び出し、Clockコンポーネントは毎秒ごとに tick()メソッドを呼び出すためにタイマーを設定するようブラウザに要求する。

4.ブラウザは、毎秒ごとにtick()メソッドを呼び出す。その中で Clockコンポーネントは、現在時刻を含んだオブジェクトを引数として setState()を呼び出し、UI の更新をスケジュールします。setState()が呼び出され、Reactはstateが変わったということが分かり、render()メソッドを再度呼び、画面上に何を表示すべきかを知る。
(今回は、render()メソッド内のthis.state.dateが異なるため、レンダリングされる出力は新しく更新された時間が含まれる。それに従って ReactはDOMを更新する。)

5.この後に ClockコンポーネントがDOMから削除されると、componentWillUnmount()メソッドを呼び、タイマーが停止する。

stateを正しく使用する

setState()について知っておくべきことが3つある。

1.stateについて、直接変更しない。
代わりにsetState()を使用する。constructorの中は唯一this.stateに直接代入していい。

one.js
const this.state.foo = 'abc'; // NG

2.stateの更新は非同期で行われる時がある。
this.propsthis.stateは非同期で更新されるため、それらの値に依存すべきではない。

two.js
// NG 更新に失敗することがある
this.setState({
  counter: this.state.counter + this.props.increment
});

// OK アロー関数ではなく、通常の関数でもOK
this.setState((state, props) => ({
  counter: state.counter + props.increment
}));

3.stateの更新はマージされる
setState()が呼ばれた際に、現在のstateの値にマージする。
stateが独立した変数を含んでいる場合、別々にsetState()を呼び出し、更新することができる。

three.js
constructor(props){
  super(props);
  this.state = (
    posts: [],
    comments: []
  );
}

componentDidMount(){
  fetchPosts().then(response => {
    this.setState(
      posts: response.posts
    );
  });

  fetchComments().then(response => {
    this.setState(
      comments: response.comments
    );
  });
}

データは下方向に伝わる

stateを所有してセットするコンポーネント自身以外からはそのstateにアクセスすることはできない。
コンポーネントはその子コンポーネントにpropsとして自身のstateを渡しても構わない。
どんなstateも必ず特定のコンポーネントが所有し、stateから生ずる全てのデータは、ツリーでそれらの下にいるコンポーネントにのみ影響する。

6.イベント処理

Reactのイベント処理はDOM要素のイベント処理に似ているが文法的な違いがある。
例えば、Reactのイベントは小文字ではなく、キャメルケースで表す。
JSXではイベントハンドラとして文字列ではなく、関数を渡す。

index.html
<button onclick="activeLacers()"></button>
index.js
<button onClick={acticeLacers}></button>

また、Reactではfalseを返してもデフォルトで抑止しないため、明示的にpreventDefaultを呼び出す

index.html
<a href="#" onclick="alert('this link was clicked'); return false">click me</a>
index.js
function ActionLink(){
  function handleClick(e){
    e.preventDefault();
    alert('this link was clicked');
  }

  return(
    <a href="#" onClick={handleClick}>click me</a>
  );
}

ここでのeは合成イベント

Reactの場合、DOM生成後にaddEventListenerを呼び出して、リスナーを追加するべきではない。
代わりに要素が最初にレンダリングされる際に、リスナーを指定する。

コンポーネントをクラスを使用して定義した場合、一般的なパターンでは、クラスのメソッドになる。

以下コード
※JSXのコールバックにおけるthisには注意する。
Javascriptのクラスのメソッドはデフォルトでバインドされないため、バインドする。

toggle.js
class Toggle extends React.Component{
  constructor(props){
    super(props);
      this.state = {
        isToggleOn: true
    };
      this.hundleClick = this.hundleClick.bind(this);
  }

  hundleClick(){
    this.setState(state => ({
        isToggleOn: !state.isToggleOn
    }));
  }

  render(){
    return (
      <button onClick={this.hundleClick}>
        {this.state.isToggleOn ? 'ON' :'OFF'}
      </button>
    );
  }
}

イベントハンドラに引数を渡す

ループ処理内で、イベントハンドラの追加のパラメータを渡したい場合

両方同じ処理

loop.js
<button onClick={(e) => this.deleteRow(id, e)}>Delete</button>
<button onClick={this.deleteRow.bind(this, id)}>Delete</button>

どちらもReactのイベントを表すeという引数はidの次の2番目の引数として渡されることになる
アロー関数では明示的にeを渡す必要があるが、bindの場合はid以降の追加の引数は自動的に転送される

7.条件付きレンダー

Reactの条件条件付きレンダーはJavascriptにおける条件分岐と同じように動作する。

要素変数

要素を保持するために変数を利用できる。
コンポーネントの一部を条件付きでレンダーしたい場合に役立つ。

以下ログイン/ログアウトボタンを表すコード

logInOut.js
function UserGreeting(props){
  return (
    <p>Hello React!</p>
  )
}

function GuestGreeting(props){
  return (
    <p>Please Login!</p>
  );
}

function Greeting(props){
  const isLogin = props.isLogin;

  if(isLogin){
    return <UserGreeting />;
  }
  return <GuestGreeting />;
}

function LoginButton(props){
  return (
    <button onClick={props.onClick}>Login</button>
  );
}

function LogoutButton(props){
  return (
    <button onClick={props.onClick}>Logout</button>
  );
}

class LoginControl extends React.Component{
  constructor(props){
    super(props);
    this.hundleLoginClick = this.hundleLoginClick.bind(this);
    this.hundleLogoutClick = this.hundleLogoutClick.bind(this);
    this.state = {
      isLogin: false
    };
  }

  hundleLoginClick(){
    this.setState({
      isLogin: true
    });
  }


  hundleLogoutClick(){
    this.setState({
      isLogin: false
    });
  }

  render(){
    const isLogin = this.state.isLogin;
    let btn;

    if(isLogin){
      btn = <LogoutButton onClick={this.state.hundleLogoutClick} />
    }else{
      btn = <LoginButton onClick={this.state.hundleLoginClick} />
    }

    return (
      <Greeting isLogin={isLogin} />
    );
  }
}

ReactDOM.render(
  <LoginControl />,
  document.getElementById('root')
);

インラインif

以下、unreadMsgtrueなら&&以降が返され、falseならfalseが返される

inlineIf.js
function Mailbox(props){
  const unreadMsg = props.unreadMsg;
  return (
    <div>
      <p>Hello React!</p>
      {unreadMsg > 0 &&
        <p>You have {unreadMsg.length} unread message.</p>
      }
    </div>
  );
}

const msgs = ["foo", "red", "nir"];

ReactDOM.render(
  <Mailbox unreadMsg={msgs}/>,
  document.getElementById('root')
);

インラインif-else

可読性が悪い場合は使用しない

inlineElse.js
render(){
  const isLogin = this.state.isLogin;
  return (
    <div>
      The user is <b>{isLogin ? 'currently' : 'not'}</b> log in
    </div>
  );
}

コンポーネントのレンダーを防ぐ

render()の代わりにnullを返す。

コンポーネントのrenderメソッドからnullを返してもコンポーネントのライフサイクルメソッドのトリガーには影響しない。

preventRender.js
function WarningBanner(props){
  if(!props.warn){
    return null;
  }

  return(
    <div className="alert-danger">
      Warning!
    </div>
  );
}

class Page extends React.Component{
  constructor(props){
    super(props);
    this.hundleToggleClick = this.hundleToggleClick.bind(this);
    this.state = {
      showWarning: true
    };
  }

  hundleToggleClick(){
    this.setState(state => ({
      showWarning: !state.showWarning
    }));
  }

  render(){
    return(
      <div>
        <WarningBanner warn={this.state.showWarning} />
        <button onClick={this.hundleToggleClick}>
          {this.state.showWarning ? 'hide' : 'show'}
        </button>
      </div>
    );
  }
}

ReactDOM.render(
  <Page />,
  document.getElementById('train')
);

8.リストとkey

複数のコンポーネントをレンダリングする

要素の集合を作成し{}で囲むことでJSXに含めることができる。

multi.js
const memberNames = ["dave", "nick", "chester", "tom"];
const listItems = memberNames.map((member, index) => {
  return <li key={index}>{member}</li>
});

ReactDOM.render(
  <ul>{listItems}</ul>,
  document.getElementById('train')
);

基本的なリストコンポーネント

通常リストは、何かしらのコンポーネントの内部でレンダリングしたい。
上記のコードを例に、要素を出力するコンポーネントを作ることができる。

listComp.js
function MemberList(props){
  const memberNames = props.members;
  const listItems = memberNames.map((member, index) => {
    return <li key={index}>{member}</li> // keyに関しては次の節で
  });
  return(
    <ul>{listItems}</ul>
  );
}

const memberNames = ["dave", "nick", "chester", "tom"];
ReactDOM.render(
  <MemberList members={memberNames} />,
  document.getElementById('train')
);

key

どの要素が追加、変更、削除されたのかReactが識別するのに役立つ。
配列内の項目に識別の安定性を持たせるために、各項目にkeyを与えるべきである。

key1.js
const numbers = [1,2,3,4,5];
const listNumber = numbers.map(number => {
  return <li key={number.toString()}>{number}</li>
});

兄弟間で項目を一意に特定できるkeyを設定するのが最良、
多くの場合はデータ内のidkeyとして使用することになる。

key2.js
const todos = [
  {id: 1, text: "a"},
  {id: 2, text: "b"},
  {id: 3, text: "c"},
  {id: 4, text: "d"},
  {id: 5, text: "e"}
];
const todoItem = todos.map(todo => {
  return <li key={todo.id}>{todo.text}</li>
});

安定したidがない場合、項目のindexを使用することができる。

key3.js
const todos = [
  {text: "a"},
  {text: "b"},
  {text: "c"},
  {text: "d"},
  {text: "e"}
];
const todoItem = todos.map((todo, index) => {
  return <li key={index}>{todo.text}</li>
});

要素の並び順を変更される可能性がある場合は、indexkeyとして使用することは、パフォーマンスに悪影響を与え、コンポーネントの状態に問題を起こさせる可能性がある。詳しくは、Robin Pokornyの解説を参照。

keyのあるコンポーネントの抽出

keyが意味を持つのは、それを取り囲んでいる配列側の文脈になる。

例:ダメパターン

ngKey.js
function ListItem(props){
  const value = props.value;
  return(
    <li key={value.toString()}>{value}</li>
  );
}

function NumberList(props){
  const numbers = props.numbers;
  const listItems = numbers.map(number => {
    return <ListItem value={number} />
  });
  return(
    <ul>{listItems}</ul>
  );
}

const numbers = [1,2,3,4,5];
ReactDOM.render(
  <NumberList numbers={numbers} />,
  document.getElementById('train')
);

<ListItem />を抽出する際には、key<li>要素ではなく、配列内の<ListItem />要素に残しておくべき。

例:okパターン
基本ルールとしては、map()の呼び出し内の要素にkeyが必要。

okKey.js
function ListItem(props){
  return(
    <li>{props.value}</li>
  );
}

function NumberList(props){
  const numbers = props.numbers;
  const listItems = numbers.map(number => {
    return (
    <ListItem
      key={number.toString()}
      value={number}
    />
    );
  });
  return(
    <ul>{listItems}</ul>
  );
}

const numbers = [1,2,3,4,5];
ReactDOM.render(
  <NumberList numbers={numbers} />,
  document.getElementById('train')
);

keyは兄弟間の要素で一意であれば良い

二つの異なる配列を作成する場合では、同一のkeyを使っても構わない。

uniqueKey.js
function Blog(props){
  const sidebar = (
    <ul>
      {props.posts.map(post =>
        <li key={post.id}>
          {post.title}
        </li>
      )}
    </ul>
  );

  const content = props.posts.map(post =>
    <div key={post.id}>
      <h3>{post.title}</h3>
      <p>{post.content}</p>
    </div>
  );

  return (
    <div>
      {sidebar}
      <hr />
      {content}
    </div>
  );
}

const posts = [
  {id: 1, title: "hello", content: "react"},
  {id: 2, title: "bye", content: "javascript"}
];
ReactDOM.render(
  <Blog posts={posts} />,
  document.getElementById('train')
);

keyはReactへのヒントとしては使われるが、記述したコンポーネントには渡されないため、別名のpropsとして明示的に渡す必要がある。

propsKey.js
const content = posts.map(post => 
  <Post
    key={post.id} // cannot read
    id={post.id} // can read
    title={post.title}
  />
);

Postコンポーネントはidは読み取れるが、keyは読み取れない。

map()をJSXに埋め込む

上記を例にlistItemsを別途宣言して、JSXに含める。

emb.js
function NumberList(props){
  const numbers = props.numbers:
  const listItems = numbers.map(number => 
    <ListItem 
      key={number.toString()}
      value={number}
    />
  );
  return (
    <ul>
      {listItems}
    </ul>
  );
}

map()の結果をインライン化することもできる。

embInline.js
function NumberList(props){
  const numbers = props.numbers:
  return (
    <ul>
      {numbers.map(number => 
        <ListItem 
          key={number.toString()}
          value={number}
        />
      )}
    </ul>
  );
}

可読性のためにコンポーネントを抽出する必要があるのかどうか考える必要はある。

9.フォーム

HTMLのフォームの送信に応答してユーザがフォームに入力したデータにアクセするようなjavascript関数があると便利

controlled component

Reactでは、変更される可能性があるものはstateプロパティに保持され、setState関数でのみ更新される。
そのためこの2つの状態を結合させることで、フォームをレンダーしているReactコンポーネントが後続のユーザ入力でフォームに起きることを制御できる。

例:フォーム送信時に名前をログとして残す。

logName.js
class NameForm extends React.Component {
  constructor(props) {
    super(props);
    this.handleChange = this.handleChange.bind(this);
    this.handleSubmit = this.handleSubmit.bind(this);
    this.state = (
      {value: ''}
    );

  }

  handleChange(event) {
    this.setState({value: event.target.value});
  }

  handleSubmit(event) {
    alert(`A name was submitted:${this.state.value}`);
    event.preventDefault();
  }

  render() {
    return (
      <form onSubmit={this.handleSubmit}>
        <label>
          Name:
          <input
            type="text"
            value={this.state.value}
            onChange={this.handleChange}
          />
        </label>
        <input type="submit" value="Submit" />
      </form>
    );
  }
}

ReactDOM.render(
  <NameForm />,
  document.getElementById('train')
);

textareaはvalue属性を使用する。

textarea.js
class AreaForm extends React.Component{
  constructor(props) {
    super(props);
    this.handleChange = this.handleChange.bind(this);
    this.handleSubmit = this.handleSubmit.bind(this);
    this.state = {
      value: 'Please write everything about your favorite thing.'
    };
  }

  handleChange(event) {
    this.setState(
      {value: event.target.value}
    );
  }

  handleSubmit(event) {
    alert('An essay was submitted: ' + this.state.value);
    event.preventDefault();
  }

  render() {
    return (
      <form onSubmit={this.handleSubmit}>
        <label>
          Essay:
          <textarea
            value={this.state.value}
            onChange={this.handleChange}
          />
        </label>
        <input type="submit" value="Submit" />
      </form>
    );
  }
}

ReactDOM.render(
  <AreaForm />,
  document.getElementById('train')
);

selectはselect属性があるので、親のselectタグのvalueを扱う。

select.js
class SelectForm extends React.Component{
  constructor(props){
    super(props);
    this.hundleChange = this.hundleChange.bind(this);
    this.hundleSubmit = this.hundleSubmit.bind(this);
    this.state = (
      {value: ''}
    );
  }

  hundleChange(event){
    this.setState({value: event.target.value});
  }

  hundleSubmit(event){
    alert(`Your favorite type is ${this.state.value}`);
    event.preventDefault();
  }

  render(){
    return(
      <form onSubmit={this.hundleSubmit}>
        <label>
          Pick your favorite type.
          <select value={this.state.value} onChange={this.hundleChange}>
            <option value="red">red</option>
            <option value="blue">blue</option>
            <option value="green">green</option>
            <option value="yellow">yellow</option>
            <option value="white">white</option>
          </select>
        </label>
        <input type="submit" value="Submit" />
      </form>
    );
  }
}


ReactDOM.render(
  <SelectForm />,
  document.getElementById('train')
);

value属性に配列を渡すことで複数オプションを選択することができる。

selectMulti.js
<select multiple={true} value={['B', 'C']}>

複数入力の処理

複数のinput要素を制御するには、name属性を追加すれば、event.target.nameに基づいて、処理を選択することができる。

multiInput.js
class Reservation extends React.Component{
  constructor(props){
    super(props);
    this.hundleInputChange = this.hundleInputChange.bind(this);
    this.state = {
      isGoing: true,
      numberOfGuest: 2
    };
  }

  hundleInputChange(event){
    const target = event.target;
    const value = target.type === 'checkbox' ? target.checked : target.value;
    const name = target.name;

    this.setState(
      {[name]: value}
    );
  }

  render(){
    return(
      <form>
        <label>
          Is going:
          <input
            name="isGoing"
            type="checkbox"
            checked={this.state.isGoing}
            onChange={this.hundleInputChange}
          />
        </label>
        <br />
        <label>
          Number of guest:
          <input
            name="numberOfGuest"
            type="number"
            value={this.state.numberOfGuest}
            onChange={this.hundleInputChange}
          />
        </label>
      </form>
    );
  }
}

ReactDOM.render(
  <Reservation />,
  document.getElementById('train')
);

制御された入力におけるnull値

制御されたコンポーネントでvalueプロパティに値を指定することでユーザーが値を変更できないようになる。

null.js
ReactDOM.render(<input value="hi" />, 
  document.getElementById('train'));

setTimeout(function() {
  ReactDOM.render(<input value={null} />, mountNode);
}, 1000);

制御されたコンポーネントの代替手段

制御されたコンポーネントは、あらゆる種類のデータの変更に対してイベントハンドラをかく場合などは、入力フォームを実装する代替手段である非制御コンポーネント – Reactを検討する。

本格的なソリューション

完全なソリューションを探している場合はformikが人気がある選択肢の一つ。

10.stateのリフトアップ

いくつかのコンポーネントが同一の変化するデータを反映する必要がある。そのような時は最も近い共通の祖先コンポーネントにstateをリフトアップすることが良い。

コード例:水が沸騰するかどうか計算する

base.js
function BoilingVerdict(props){
  if(props.celsius >= 100){
    return <p>The water would voil.</p>;
  }
  return <p>The water would not voil.</p>;
}

class Calculator extends React.Component{
  constructor(props){
    super(props);
    this.handleChange = this.handleChange.bind(props);
    this.state = (
      {temperature: ''}
    );
  }

  handleChange(e){
    this.setState(
      {temperature: e.target.value}
    );
  }

  render(){
    const temperature = this.state.temperature;
    return(
      <fieldset>
        <legend>Enter temperature in Celsius.</legend>
        <input
          value={this.state.temperature}
          onChange={this.handleChange}
        />
        <BoilingVerdict celsius={parseFloat(temperature)} />
      </fieldset>
    );
  }
}

ReactDOM.render(
  <Calculator />,
  document.getElementById('train')
);

上記に二つ目の要素追加(華氏温度)。

addprops.js
function BoilingVerdict(props){
  if(props.celsius >= 100){
    return <p>The water would voil.</p>;
  }
  return <p>The water would not voil.</p>;
}

const scaleNames = {
  c: 'Celsius',
  f: 'Fahrenheit'
};

class TemperatureInput extends React.Component{
  constructor(props){
    super(props);
    this.handleChange = this.handleChange.bind(props);
    this.state = (
      {temperature: ''}
    );
  }

  handleChange(e){
    this.setState(
      {temperature: e.target.value}
    );
  }

  render(){
    const temperature = this.state.temperature;
    const scale = this.props.scale;
    return(
      <fieldset>
        <legend>Enter temperature in {scaleNames[scale]}.</legend>
        <input
          value={this.state.temperature}
          onChange={this.handleChange}
        />
        <BoilingVerdict celsius={parseFloat(temperature)} />
      </fieldset>
    );
  }
}

class Calculator extends React.Component{
  render(){
    return(
      <div>
        <TemperatureInput scale='c' />
        <TemperatureInput scale='f' />
      </div>
    );
  }
}

ReactDOM.render(
  <Calculator />,
  document.getElementById('train')
);

片方のフィールドを変化させても、もう一方のフィールドの更新はされないため、二つのフィールドを同期させるようにする。

変換関数作成

trans.js
// 摂氏温度に変換
function toCelsius(fahrenheit){
  return (fahrenheit - 32) * 5 / 9;
}

// 華氏温度に変換
function toFahrenehit(celsius){
  return (celsius * 9 / 5) + 32;
}


// 常に小数第3位までで四捨五入されるようにする
function tryConvert(temperature, convert){
  const input = parseFloat(temperature);
  if(Number.isNaN(input)){
    return null;
  }
  const output = convert(input);
  const rounded = Math.round(output * 1000) / 1000;
  return rounded.toString();
}

stateのリフトアップ

まだ両方のTemperatureInputコンポーネントは独立してローカル stateを保持しているが、同期していてほしい。
よって、stateをリフトアップして実現する。(TemperatureInputからCalculatorに移動する。)
以下の手順で行う。

1.TemperatureInputコンポーネントのthis.state.temperaturethis.props.temperatureに変える。
this.props.temperatureは既にあるものとする。(後でこれはCalculatorから渡すようにする)

first.js
class TemperatureInput extends React.Component{
// 省略
  render(){
    // const temperature = this.state.temperature;
    const temperature = this.props.temperature;
// 省略
  }
}

2.TemperatureInputが自身の温度を更新したい場合は、this.props.onTemperatureChangeを呼び出す。(名前は任意)

second.js
class TemperatureInput extends React.Component{
// 省略
  handleChange(e){
    // this.setState({temperature: e.target.value});
    this.props.onTemperatureChange(e.target.value);
  }
// 省略
}

onTemperatureChangeプロパティは親コンポーネントCalculatorからtemperatureプロパティと共に渡される。親コンポーネントは入力の変化に応じて自身のローカルstateを更新し、両方の入力フォームは新しい値で再レンダーされる。

3.現時点のtemperaturescaleの入力を、このコンポーネントのローカルstateに保存する。(両方の入力の保存は不必要)

liftup.js
const scaleNames = {
  c: 'Celsius',
  f: 'Fahrenheit'
};

function toCelsius(fahrenheit){
  return (fahrenheit - 32) * 5 / 9;
}

function toFahrenehit(celsius){
  return (celsius * 9 / 5) + 32;
}

function tryConvert(temperature, convert){
  const input = parseFloat(temperature);
  if(Number.isNaN(input)){
    return '';
  }
  const output = convert(input);
  const rounded = Math.round(output * 1000) / 1000;
  return rounded.toString();
}

function BoilingVerdict(props){
  if(props.celsius >= 100){
    return <p>The water would boil.</p>;
  }
  return <p>The water would not boil.</p>;
}

class TemperatureInput extends React.Component{
  constructor(props){
    super(props);
    this.handleChange = this.handleChange.bind(this);
  }

  handleChange(e){
    this.props.onTemperatureChange(e.target.value);
  }

  render(){
    const temperature = this.props.temperature;
    const scale = this.props.scale;
    return(
      <fieldset>
        <legend>Enter temperature in {scaleNames[scale]}.</legend>
        <input
          value={temperature}
          onChange={this.handleChange}
        />
      </fieldset>
    );
  }
}

class Calculator extends React.Component{
  constructor(props){
    super(props);
    this.handleCelsiusChange = this.handleCelsiusChange.bind(this);
    this.handleFahrenheitChange = this.handleFahrenheitChange.bind(this);
    this.state = {
      temperature: '',
      scale: 'c'
    };
  }

  handleCelsiusChange(temperature){
    this.setState({scale: 'c', temperature});
  }

  handleFahrenheitChange(temperature){
    this.setState({scale: 'f', temperature});
  }

  render(){
    const scale = this.state.scale;
    const temperature = this.state.temperature;
    const celsius = scale === 'f' ? tryConvert(temperature, toCelsius) : temperature;
    const fahrenheit = scale === 'c' ? tryConvert(temperature, toFahrenehit) : temperature;
    return(
      <div>
        <TemperatureInput
          scale='c'
          temperature={celsius}
          onTemperatureChange={this.handleCelsiusChange}
        />
        <TemperatureInput
          scale='f'
          temperature={fahrenheit}
          onTemperatureChange={this.handleFahrenheitChange}
        />
        <BoilingVerdict celsius={parseFloat(celsius)} />
      </div>
    );
  }
}

ReactDOM.render(
  <Calculator />,
  document.getElementById('train')
);

これで、どちらの入力コンポーネントを編集したかに関係なく、Calculatorthis.state.temperaturethis.state.scaleが更新される。

上記の変更処理の流れ

DOMのinputでのonChangeで呼ばれた関数を実行する。
(TemperatureInputコンポーネントのhandleChange)
 ↓
TemperatureInputコンポーネントのhandleChangeはthis.props.temperature()に値を与えて呼び出す。
(onTemperatureChangeを含むpropsは、親クラスのCalculatorから与えられる)
 ↓
編集したフィールドによって、呼ばれるメソッドが決まる。
(CalculatorコンポーネントのhandleCelsiusChangeかhandleFahrenheitChange)
 ↓
このメソッドはCalculatorコンポーネントが新しい入力値と更新した値をthis.setStateに与えて呼び出し、Calculatorコンポーネントを再レンダリングする。
(Calculatorコンポーネントのrender)
 ↓
Calculatorコンポーネントのrenderを呼び出し、両方の入力コンポーネント値を再計算。
(toCelsius、toFahrenehit)
 ↓
Calculatorコンポーネントが与えた新しいpropsで各TemperatureInputコンポーネントのrenderメソッドが呼ばれる。
+
BoilingVerdictのコンポーネントのrenderメソッドが呼ばれる。
 ↓
以上の判定結果と入力コンポーネント値によってDOMを更新する。
+
変更された入力コンポーネントは現在の値によって、もう一方の入力コンポーネントは変換され更新される。

11.コンポジションと継承

子要素の出力

sidebarやDialogのような子要素を知らない汎用的な入れ物を表すコンポーネントで使用される。

child.js
function Fancyborder(props){
  return (
    <div className={`FancyBorder FancyBorder-${props.color}`}>
      {props.children}
    </div>
  );
}

function WelcomeDialog(){
  return(
    <Fancyborder color="blue">
      <h1 className="Dialog-title">
        Welcome
      </h1>
      <p className="Dialog-message">
        Hello React
      </p>
    </Fancyborder>
  );
}

ReactDOM.render(
  <WelcomeDialog />,
  document.getElementById('train')
);

上記のFancyBorderタグの要素はchildrenpropsで渡される。

一般的ではないが、childrenの代わりに独自のpropsを作成して、渡すこともできる。

orgChild.js
function Contacts(){
  return(
    <div className="Contacts">
      <p>about contact</p>
    </div>
  );
}

function Chat(){
  return(
    <div className="Chat">
      <p>chat</p>
    </div>
  );
}

function SplitPane(props){
  return(
    <div className="SplitPane">
      <div className="SplitPane-left">
        {props.left}
      </div>
      <div className="SplitPane-right">
        {props.right}
      </div>
    </div>
  );
}

function App(){
  return(
    <SplitPane
      left={<Contacts />}
      right={<Chat />}
    />
  );
}

ReactDOM.render(
  <App />,
  document.getElementById('train')
);

Reactの要素はオブジェクトなのでpropsとして渡すことができる。

特化したコンポーネント

コンポーネントは他のコンポーネントを特別な要素として扱うことができる。(以下はDialogに対してWelcomeDialogは特別なケース)

specializedComponent.js
function Dialog(props){
  return(
    <div className="blue">
      <h1 className="Dialog-title">
        {props.title}
      </h1>
      <p className="Dialog-message">
        {props.message}
      </p>
    </div>
  );
}

function WelcomeDialog(){
  return(
    <Dialog
      title="welcome"
      message="hello react"
    />
  );
}

ReactDOM.render(
  <WelcomeDialog />,
  document.getElementById('train')
);

コンポジションはクラスとして定義されたコンポーネントでも同じように動作する。

specializedClassComponent.js
function Fancyborder(props){
  return (
    <div className={`FancyBorder FancyBorder-${props.color}`}>
      {props.children}
    </div>
  );
}

function Dialog(props){
  return(
    <Fancyborder color="blue">
      <h1 className="Dialog-title">
        {props.title}
      </h1>
      <p className="Dialog-message">
        {props.message}
      </p>
      {props.children}
    </Fancyborder>
  );
}

class SignUpDialog extends React.Component{
  constructor(props){
    super(props);
    this.handleChange = this.handleChange.bind(this);
    this.handleSignUp = this.handleSignUp.bind(this);
    this.state = (
      {login: ''}
    );
  }

  handleChange(e){
    this.setState(
      {login: e.target.value}
    );
  }

  handleSignUp(){
    alert(`welcome a board ${this.state.login}`);
  }

  render(){
    return(
      <Dialog
        title="XYZ Program"
        message="How should we refer to you?"
      >
        <input
          value={this.state.login}
          onChange={this.handleChange}
        />
        <button onClick={this.handleSignUp}>
          Sign me up.
        </button>
      </Dialog>
    );
  }
}

ReactDOM.render(
  <SignUpDialog />,
  document.getElementById('train')
);

継承はどうするの

propsとコンポジションにより、コンポーネントの見た目、振る舞いを明示的にそして安全にカスタマイズするのに十分な柔軟性を得ることができる。コンポーネントはどのようなpropsでも受け付け、それはプリミティブ値、React要素、関数でもいい。

コンポーネント間で非UI機能を再利用したい場合、それを別の JavaScriptモジュールに抽出することがいい。
コンポーネントはその関数やオブジェクト、クラスなどを継承することなくインポートすることで使用することができる。

12.Reactの流儀

こちらからどうぞ

最後に

だらだらと長くなってしまいましたが、なんとか推敲することができたかなと...汗
間違っている!気になる!等ありましたら、
暖かい目で編集リクエストをいただければと思います。

参考

React - MAIN CONCEPT

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

Reactの基本メモ

概要

Reactの公式ドキュメントを読みながら勉強した際の備忘メモです。
Reactを動かしながら学習したかったので、環境はDockerでサクッと作った感じです。
(環境構築は記述していないです。)

前提

Javascriptの知識
自信のない方は、Javascriptチュートリアルで知識を確認してから進める。

React Documents

公式DocsのMAIN CONCEPTを進めていきました。
項目1.Hello Worldは飛ばして、次項の2.JSXの導入からになります。
またドキュメント内のJavascriptのおさらい的な部分は省いています。
最後の12.Reactの流儀は検索可能な商品データ表をReactで作っていく様子が記述されています。
流儀と言われ畏れ多いので、公式ドキュメントに任せます。(面倒なだけ)

1.Hello World

前提の話などなので、飛ばします。

2.JSXの導入

javascriptの拡張構文(プログラミング言語)

導入は必須ではないが、有用な見た目、有用なエラー警告を多く表示してくれる。

式の埋め込み

{}(中括弧)でjavscriptの式を利用できる

embed.js
const name = 'taro';
const element = <h1>Hello {name}</h1>;

ReactDOM.render(element, document.getElementbyId('root'));

JSXも式である

コンパイル後、JSXは普通のJavascriptの関数呼び出しに変換されて、Javascriptオブジェクトとして評価される。

formula.js
function getGreeding(user){
  if(user){
    return <h1>Hello {user}.</h1>;
  }
  return <h1>Hello foo.</h1>;
}

属性指定

文字列リテラルを属性として指定するために""(引用符)を使用できる。

また、Javascript埋め込む際は、{}(中括弧)を使用する。

Javascriptを使用時には、中括弧を囲む引用符を使用しない。

str.js
const element = <div className="string"></div>;  // 文字列

const foo = "string";
const fooElement = <div className={foo}></div>;  // Javascript

const fighters = "dave";
const fightersElememt = <div className="{fighters}"></div>; //  NG

キャメルケースのプロパティ命名規則

JSXはHTMLよりもJavascriptに近いので、HTML属性ではなく、キャメルケースの命名規則を使用する。

class.js
<div class="class_name"></div> // NG
<div className="class_name"></div> // OK

子要素指定

子要素を持つことができる。
また、中身がない場合はタグを閉じる(/>する)。

child.js
const child = (
  <div>
    <h1>Hello React</h1>
    <h2>Hello JSX</h2>
  </div>
);

const close = <img src={foo} />;

JSXはインジェクション攻撃を防ぐ

デフォルトでReactDOMは、JSXに埋め込まれた値をレンダリングされる前にエスケープするため、レンダーの前に全てが文字列に変換される。これはXSS攻撃の防止になる。

xss.js
const title = response.malInput;
const element = <h1>{title}</h1>;

JSXはオブジェクトの表現

BabelはJSXをReact.createElement()の呼び出しへコンパイルする。

下記の2つは同じになる。

element.js
const element = (
  <h1 className="greeting">
    Hello React.
  </h1>
);


const element = React.createElement(
  'h1',
  {className: 'greeting'},
  'Hello React.'
);

3.要素のレンダー

要素をDOMとして描画する

Reactだけで構成されたアプリケーションは、通常一つのDOMノードだけを持つ。

React要素をDOMノードにレンダリングするには、

ReactDOM.render()に、React要素(第1引数)とDOMノード(第2引数)を渡す。

root.html
<div id="root"></div>

rootのidを持つDOM要素に、elementをレンダーする

root.js
const element = <h1>Hello React</h1>;
ReactDOM.render(element, document.getElementbyId('root'));

レンダリングされた要素の更新

React要素はImmutableのため一度作成すると変更ができない。

更新する方法は、新しい要素を作成して、ReactDOM.render()に渡す。

以下は、秒ごとに動く時計の例です。

clock.js
function Clock(){
  const element = (
    <div>
      <h1>It is {new Date().toLocaleTimeString()}.</h1>
    </div>
  );
  ReactDOM.render(element, document.getElementbyId('root')
  }

setInterval(clock, 1000);

必要な箇所のみ更新する

ReactDOMは要素とその子要素を、以前のものと比較し必要なだけの要素を更新する。

4.componentとprops

コンポーネントを定義する方法としては、javascriptの関数、またはes6のクラスを記述する。
propsという任意の入力を受け取りReact要素を返す。

詳細はコンポーネントAPIリファレンス

関数 component

データの入ったpropsというオブジェクトを引数として受け取り、React要素を返す。

func.js
function Hello(props){
  return <p>Hello {props.name}.</p>;
}

クラス component

class.js
class Hello extends React.Component{
  render(){
    return <p>Hello {this.props.name}.</p>;
  }
}

componentのrender

DOMのタグを表すReact要素だけではなく、ユーザ定義のcomponentを表すことができる。

Reactがユーザ定義のコンポーネントを見つけた場合、JSXの属性を単一のオブジェクトとして、コンポーネントに渡す。

このオブジェクトのことはpropsと呼ぶ。

comp.js
function Hello(props){
  return <p>Hello {props.name}</p>;
}

const element = <Hello name="aiueo" />;

ReactDOM.render(
  element,
  document.getElementById('root')
);

上記の処理の流れ
1: <Hello name="aiueo" />という要素を引数として、ReactDOM.render()を呼び出す。
2: Helloコンポーネントを呼び出し、propsとして、{name: 'aiueo'}を渡す。
3: Helloコンポーネントは<p>Hello aiueo</p>を返す。
4: ReactDOMは<p>Hello aiueo</p>に一致するように効率的に更新する。

コンポーネントを組み合わせる

コンポーネントは自信の出力の中で、他のコンポーネントを参照できる。

↓Helloを何回もレンダリングするAppを作成する。

conbine.js
function Hello(props){
  return <p>Hello {props.name}</p>;
}

function App(){
  return(
    <div>
      <Hello name="Taro" />
      <Hello name="Jiro" />
      <Hello name="Saburo" />
    </div>
  );
}

ReactDOM.render(
  <App />,
  document.getElementById('root')
);

componentを抽出する

コンポーネントを分割する(コンポーネントをより小さいコンポーネントに分割することを恐れないでください、とのこと)。

以下のCommentコンポーネントについて考えてみる。

base.js
function Comment(props){
  return(
    <div className="comment">
      <div className="userinfo">
        <img 
          src={props.author.imageUrl}
          alt={props.author.name}
          className="avatar"
        />
        <div className="username">
          {props.author.name}
        </div>
      </div>
      <div className="text">
        {props.text}
      </div>
      <div className="date">
        {formatDate(props.date)}
      </div>
    </div>
  );
}

多くのネストがあるため、内部の個々の部品の再利用は困難になる。

↓ ここでコンポーネントを抽出する

userinfo部分を抽出する
自身がComment内にレンダリングされていることは知っておく必要はない。
そのため、コンポーネントのコンテキストからではなく、自身のコンポーネントの観点からpropsの名前を定義する。

avatar.js
function Avatar(props){
  return(
    <img 
      src={props.user.imageUrl}
      alt={props.user.name}
      className="avatar"
    />
  );
}

userinfo.js
function UserInfo(props){
  render(
    <div className="userinfo">
      <Avatar user={props.user} />
      <div className="username">
        {props.user.name}
      </div>
    </div>
  );
}

↓ よりシンプルにする。

alternateBase.js
function Avatar(props){
  return(
    <img 
      src={props.user.imageUrl}
      alt={props.user.name}
      className="avatar"
    />
  );
}

function UserInfo(props){
  render(
    <div className="userinfo">
      <Avatar user={props.user} />
      <div className="username">
        {props.user.name}
      </div>
    </div>
  );
}

function Comment(props){
  return(
    <div className="comment">
      <UserInfo user={props.author} />
      <div className="text">
        {props.text}
      </div>
      <div className="date">
        {formatDate(props.date)}
      </div>
    </div>
  );
}

propsは読み取り専用

Reactは柔軟だが、1つ厳格なルールがある。

コンポーネントは自分自身のpropsは決して変更してはいけない。

以下、公式ドキュメントから引用

全ての React コンポーネントは、自己の props に対して純関数のように振る舞わねばなりません。

5.stateとライフサイクル

componentのカプセル化

Clockコンポーネントを再利用可能かつカプセル化する。

clock.js
function Clock(props){
  return(
    <div>
      <p>Hello React</p>
      <p>{props.date.toLocaleTimeString()}</p>
    </div>
  );
}

function tick(){
  ReactDOM.render(
    <Clock date={new Date()} />,
    document.getElementById('root')
  );
}

setInterVal(tick, 1000);

↑ UIを毎秒ごとに更新する処理は、Clockの内部実装であるべき処理なので、ReactDOM.render()部分を一度のみ記述して、Clock自身を更新させたい。
ここでstateを利用する。
statepropsに似ているが、コンポーネントに管理されるプライベートなもの。

関数をクラスに変換する

以下の流れで関数コンポーネントをクラスに変換する。

1.React.Componentを継承する同名のes6のクラスを作成。

2.render()と呼ばれる空のメソッドを1つ追加。

3.関数の中身をrender()メソッドに移動。

4.render()内のpropsthis.propsに書き換える。

5.空になった関数の宣言部分を削除。

以下コード

clock.js
class Clock extends React.Component{
  render(){
    return (
      <div>
         <p>Hello React</p>
         <p>{this.props.date.toLocaleTimeString()}</p>
      </div>
    );
  }
}

function tick(){
  ReactDOM.render(
    <Clock date={new Date()} />,
    document.getElementById('root')
  );
}

setInterVal(tick, 1000);

これにより、render()は更新した際に毎回呼ばれるが、同一のDOMノード内で<Clock />をレンダーしている限り、Clockクラスのインスタンスは1つだけ使われる。
このことにより、ローカルstateやライフサイクルメソッドといった追加機能を利用できる。

ローカルstateを設定

以下のステップでdatepropsからstateに移動する。

1.render()メソッド内のthis.props.datethis.state.dateに書き換える。

2.this.stateを初期化するクラスコンストラクタを作成する。

3.<Clock />要素から、dateプロパティを削除する。

以下コード

clock.js
class Clock extends React.Component{
  constructor(props){
    // クラスのコンポーネントは常に props を引数として親クラスのコンストラクタを呼び出す必要がある。
    super(props);  
    this.state = {
      date: new Date()
    };
  }

  render(){
    return (
      <div>
         <p>Hello React</p>
         <p>{this.state.date.toLocaleTimeString()}</p>
      </div>
    );
  }
}

function tick(){
  ReactDOM.render(
    <Clock />,
    document.getElementById('root')
  );
}

setInterVal(tick, 1000);

ライフサイクルメソッド

多くのコンポーネントを持つアプリケーションでは、コンポーネントが破棄された場合にそのコンポーネントが持っていたリソースを開放することがとても大切。

Clockを例にすると、

・タイマーを設定したい時 => DOMとして描画される時(マウント(mounting)と呼ぶ)
・タイマーをクリアしたい時 => 生成したDOMが削除される時(アンマウント(unmounting)と呼ぶ)

コンポーネントクラスは特別なメソッドでマウント、アンマウントする時のコードを実行できる。
これらのメソッドはライフサイクルメソッドと呼ばれる。詳しくはReact.Component – Reactで確認。

clock.js
class Clock extends React.Component{
  constructor(props){
    super(props);
    this.state = {
      date: new Date()
    };
  }

  // 出力がDOMにレンダーされた後に実行される
  componentDidMount(){
    this.timerID = setInterval(
      () => this.tick(),
      1000
    );
  }

  // コンポーネントがDOMから削除されるときに呼び出される
  componentWillUnmount(){
    clearInterval(this.timerID);
  }

  // コンポーネントのローカルstateの更新をスケジュールするためにthis.setState()を使用する
  tick(){
    this.setState({
      date: new Date()
    });
  }

  render(){
    return (
      <div>
        <p>Hello React</p>
        <p>{this.state.date.toLocaleTimeString()}</p>
      </div>
    );
  }
}

ReactDOM.render(
  <Clock />,
  document.getElementById('root')
);

上記の処理の流れ
1.<Clock />ReactDOM.render()に渡され、Clockコンポーネントのコンストラクタを呼び出す。
Clockは現在時刻を表示するので、現在時刻を含んだオブジェクトでthis.stateを初期化する。そしてこのstateを更新していく。

2.Clockコンポーネントのrender()メソッドを呼び出し、Reactは画面に何を表示すべきか知る。そして、DOMをClockのレンダー出力と一致するように更新する。

3.Clockの出力がDOMに挿入されると、componentDidMount()メソッドを呼び出し、Clockコンポーネントは毎秒ごとに tick()メソッドを呼び出すためにタイマーを設定するようブラウザに要求する。

4.ブラウザは、毎秒ごとにtick()メソッドを呼び出す。その中で Clockコンポーネントは、現在時刻を含んだオブジェクトを引数として setState()を呼び出し、UI の更新をスケジュールします。setState()が呼び出され、Reactはstateが変わったということが分かり、render()メソッドを再度呼び、画面上に何を表示すべきかを知る。
(今回は、render()メソッド内のthis.state.dateが異なるため、レンダリングされる出力は新しく更新された時間が含まれる。それに従って ReactはDOMを更新する。)

5.この後に ClockコンポーネントがDOMから削除されると、componentWillUnmount()メソッドを呼び、タイマーが停止する。

stateを正しく使用する

setState()について知っておくべきことが3つある。

1.stateについて、直接変更しない。
代わりにsetState()を使用する。constructorの中は唯一this.stateに直接代入していい。

one.js
const this.state.foo = 'abc'; // NG

2.stateの更新は非同期で行われる時がある。
this.propsthis.stateは非同期で更新されるため、それらの値に依存すべきではない。

two.js
// NG 更新に失敗することがある
this.setState({
  counter: this.state.counter + this.props.increment
});

// OK アロー関数ではなく、通常の関数でもOK
this.setState((state, props) => ({
  counter: state.counter + props.increment
}));

3.stateの更新はマージされる
setState()が呼ばれた際に、現在のstateの値にマージする。
stateが独立した変数を含んでいる場合、別々にsetState()を呼び出し、更新することができる。

three.js
constructor(props){
  super(props);
  this.state = (
    posts: [],
    comments: []
  );
}

componentDidMount(){
  fetchPosts().then(response => {
    this.setState(
      posts: response.posts
    );
  });

  fetchComments().then(response => {
    this.setState(
      comments: response.comments
    );
  });
}

データは下方向に伝わる

stateを所有してセットするコンポーネント自身以外からはそのstateにアクセスすることはできない。
コンポーネントはその子コンポーネントにpropsとして自身のstateを渡しても構わない。
どんなstateも必ず特定のコンポーネントが所有し、stateから生ずる全てのデータは、ツリーでそれらの下にいるコンポーネントにのみ影響する。

6.イベント処理

Reactのイベント処理はDOM要素のイベント処理に似ているが文法的な違いがある。
例えば、Reactのイベントは小文字ではなく、キャメルケースで表す。
JSXではイベントハンドラとして文字列ではなく、関数を渡す。

index.html
<button onclick="activeLacers()"></button>
index.js
<button onClick={acticeLacers}></button>

また、Reactではfalseを返してもデフォルトで抑止しないため、明示的にpreventDefaultを呼び出す

index.html
<a href="#" onclick="alert('this link was clicked'); return false">click me</a>
index.js
function ActionLink(){
  function handleClick(e){
    e.preventDefault();
    alert('this link was clicked');
  }

  return(
    <a href="#" onClick={handleClick}>click me</a>
  );
}

ここでのeは合成イベント

Reactの場合、DOM生成後にaddEventListenerを呼び出して、リスナーを追加するべきではない。
代わりに要素が最初にレンダリングされる際に、リスナーを指定する。

コンポーネントをクラスを使用して定義した場合、一般的なパターンでは、クラスのメソッドになる。

以下コード
※JSXのコールバックにおけるthisには注意する。
Javascriptのクラスのメソッドはデフォルトでバインドされないため、バインドする。

toggle.js
class Toggle extends React.Component{
  constructor(props){
    super(props);
      this.state = {
        isToggleOn: true
    };
      this.hundleClick = this.hundleClick.bind(this);
  }

  hundleClick(){
    this.setState(state => ({
        isToggleOn: !state.isToggleOn
    }));
  }

  render(){
    return (
      <button onClick={this.hundleClick}>
        {this.state.isToggleOn ? 'ON' :'OFF'}
      </button>
    );
  }
}

イベントハンドラに引数を渡す

ループ処理内で、イベントハンドラの追加のパラメータを渡したい場合

両方同じ処理

loop.js
<button onClick={(e) => this.deleteRow(id, e)}>Delete</button>
<button onClick={this.deleteRow.bind(this, id)}>Delete</button>

どちらもReactのイベントを表すeという引数はidの次の2番目の引数として渡されることになる
アロー関数では明示的にeを渡す必要があるが、bindの場合はid以降の追加の引数は自動的に転送される

7.条件付きレンダー

Reactの条件条件付きレンダーはJavascriptにおける条件分岐と同じように動作する。

要素変数

要素を保持するために変数を利用できる。
コンポーネントの一部を条件付きでレンダーしたい場合に役立つ。

以下ログイン/ログアウトボタンを表すコード

logInOut.js
function UserGreeting(props){
  return (
    <p>Hello React!</p>
  )
}

function GuestGreeting(props){
  return (
    <p>Please Login!</p>
  );
}

function Greeting(props){
  const isLogin = props.isLogin;

  if(isLogin){
    return <UserGreeting />;
  }
  return <GuestGreeting />;
}

function LoginButton(props){
  return (
    <button onClick={props.onClick}>Login</button>
  );
}

function LogoutButton(props){
  return (
    <button onClick={props.onClick}>Logout</button>
  );
}

class LoginControl extends React.Component{
  constructor(props){
    super(props);
    this.hundleLoginClick = this.hundleLoginClick.bind(this);
    this.hundleLogoutClick = this.hundleLogoutClick.bind(this);
    this.state = {
      isLogin: false
    };
  }

  hundleLoginClick(){
    this.setState({
      isLogin: true
    });
  }


  hundleLogoutClick(){
    this.setState({
      isLogin: false
    });
  }

  render(){
    const isLogin = this.state.isLogin;
    let btn;

    if(isLogin){
      btn = <LogoutButton onClick={this.state.hundleLogoutClick} />
    }else{
      btn = <LoginButton onClick={this.state.hundleLoginClick} />
    }

    return (
      <Greeting isLogin={isLogin} />
    );
  }
}

ReactDOM.render(
  <LoginControl />,
  document.getElementById('root')
);

インラインif

以下、unreadMsgtrueなら&&以降が返され、falseならfalseが返される

inlineIf.js
function Mailbox(props){
  const unreadMsg = props.unreadMsg;
  return (
    <div>
      <p>Hello React!</p>
      {unreadMsg > 0 &&
        <p>You have {unreadMsg.length} unread message.</p>
      }
    </div>
  );
}

const msgs = ["foo", "red", "nir"];

ReactDOM.render(
  <Mailbox unreadMsg={msgs}/>,
  document.getElementById('root')
);

インラインif-else

可読性が悪い場合は使用しない

inlineElse.js
render(){
  const isLogin = this.state.isLogin;
  return (
    <div>
      The user is <b>{isLogin ? 'currently' : 'not'}</b> log in
    </div>
  );
}

コンポーネントのレンダーを防ぐ

render()の代わりにnullを返す。

コンポーネントのrenderメソッドからnullを返してもコンポーネントのライフサイクルメソッドのトリガーには影響しない。

preventRender.js
function WarningBanner(props){
  if(!props.warn){
    return null;
  }

  return(
    <div className="alert-danger">
      Warning!
    </div>
  );
}

class Page extends React.Component{
  constructor(props){
    super(props);
    this.hundleToggleClick = this.hundleToggleClick.bind(this);
    this.state = {
      showWarning: true
    };
  }

  hundleToggleClick(){
    this.setState(state => ({
      showWarning: !state.showWarning
    }));
  }

  render(){
    return(
      <div>
        <WarningBanner warn={this.state.showWarning} />
        <button onClick={this.hundleToggleClick}>
          {this.state.showWarning ? 'hide' : 'show'}
        </button>
      </div>
    );
  }
}

ReactDOM.render(
  <Page />,
  document.getElementById('train')
);

8.リストとkey

複数のコンポーネントをレンダリングする

要素の集合を作成し{}で囲むことでJSXに含めることができる。

multi.js
const memberNames = ["dave", "nick", "chester", "tom"];
const listItems = memberNames.map((member, index) => {
  return <li key={index}>{member}</li>
});

ReactDOM.render(
  <ul>{listItems}</ul>,
  document.getElementById('train')
);

基本的なリストコンポーネント

通常リストは、何かしらのコンポーネントの内部でレンダリングしたい。
上記のコードを例に、要素を出力するコンポーネントを作ることができる。

listComp.js
function MemberList(props){
  const memberNames = props.members;
  const listItems = memberNames.map((member, index) => {
    return <li key={index}>{member}</li> // keyに関しては次の節で
  });
  return(
    <ul>{listItems}</ul>
  );
}

const memberNames = ["dave", "nick", "chester", "tom"];
ReactDOM.render(
  <MemberList members={memberNames} />,
  document.getElementById('train')
);

key

どの要素が追加、変更、削除されたのかReactが識別するのに役立つ。
配列内の項目に識別の安定性を持たせるために、各項目にkeyを与えるべきである。

key1.js
const numbers = [1,2,3,4,5];
const listNumber = numbers.map(number => {
  return <li key={number.toString()}>{number}</li>
});

兄弟間で項目を一意に特定できるkeyを設定するのが最良、
多くの場合はデータ内のidkeyとして使用することになる。

key2.js
const todos = [
  {id: 1, text: "a"},
  {id: 2, text: "b"},
  {id: 3, text: "c"},
  {id: 4, text: "d"},
  {id: 5, text: "e"}
];
const todoItem = todos.map(todo => {
  return <li key={todo.id}>{todo.text}</li>
});

安定したidがない場合、項目のindexを使用することができる。

key3.js
const todos = [
  {text: "a"},
  {text: "b"},
  {text: "c"},
  {text: "d"},
  {text: "e"}
];
const todoItem = todos.map((todo, index) => {
  return <li key={index}>{todo.text}</li>
});

要素の並び順を変更される可能性がある場合は、indexkeyとして使用することは、パフォーマンスに悪影響を与え、コンポーネントの状態に問題を起こさせる可能性がある。詳しくは、Robin Pokornyの解説を参照。

keyのあるコンポーネントの抽出

keyが意味を持つのは、それを取り囲んでいる配列側の文脈になる。

例:ダメパターン

ngKey.js
function ListItem(props){
  const value = props.value;
  return(
    <li key={value.toString()}>{value}</li>
  );
}

function NumberList(props){
  const numbers = props.numbers;
  const listItems = numbers.map(number => {
    return <ListItem value={number} />
  });
  return(
    <ul>{listItems}</ul>
  );
}

const numbers = [1,2,3,4,5];
ReactDOM.render(
  <NumberList numbers={numbers} />,
  document.getElementById('train')
);

<ListItem />を抽出する際には、key<li>要素ではなく、配列内の<ListItem />要素に残しておくべき。

例:okパターン
基本ルールとしては、map()の呼び出し内の要素にkeyが必要。

okKey.js
function ListItem(props){
  return(
    <li>{props.value}</li>
  );
}

function NumberList(props){
  const numbers = props.numbers;
  const listItems = numbers.map(number => {
    return (
    <ListItem
      key={number.toString()}
      value={number}
    />
    );
  });
  return(
    <ul>{listItems}</ul>
  );
}

const numbers = [1,2,3,4,5];
ReactDOM.render(
  <NumberList numbers={numbers} />,
  document.getElementById('train')
);

keyは兄弟間の要素で一意であれば良い

二つの異なる配列を作成する場合では、同一のkeyを使っても構わない。

uniqueKey.js
function Blog(props){
  const sidebar = (
    <ul>
      {props.posts.map(post =>
        <li key={post.id}>
          {post.title}
        </li>
      )}
    </ul>
  );

  const content = props.posts.map(post =>
    <div key={post.id}>
      <h3>{post.title}</h3>
      <p>{post.content}</p>
    </div>
  );

  return (
    <div>
      {sidebar}
      <hr />
      {content}
    </div>
  );
}

const posts = [
  {id: 1, title: "hello", content: "react"},
  {id: 2, title: "bye", content: "javascript"}
];
ReactDOM.render(
  <Blog posts={posts} />,
  document.getElementById('train')
);

keyはReactへのヒントとしては使われるが、記述したコンポーネントには渡されないため、別名のpropsとして明示的に渡す必要がある。

propsKey.js
const content = posts.map(post => 
  <Post
    key={post.id} // cannot read
    id={post.id} // can read
    title={post.title}
  />
);

Postコンポーネントはidは読み取れるが、keyは読み取れない。

map()をJSXに埋め込む

上記を例にlistItemsを別途宣言して、JSXに含める。

emb.js
function NumberList(props){
  const numbers = props.numbers:
  const listItems = numbers.map(number => 
    <ListItem 
      key={number.toString()}
      value={number}
    />
  );
  return (
    <ul>
      {listItems}
    </ul>
  );
}

map()の結果をインライン化することもできる。

embInline.js
function NumberList(props){
  const numbers = props.numbers:
  return (
    <ul>
      {numbers.map(number => 
        <ListItem 
          key={number.toString()}
          value={number}
        />
      )}
    </ul>
  );
}

可読性のためにコンポーネントを抽出する必要があるのかどうか考える必要はある。

9.フォーム

HTMLのフォームの送信に応答してユーザがフォームに入力したデータにアクセするようなjavascript関数があると便利

controlled component

Reactでは、変更される可能性があるものはstateプロパティに保持され、setState関数でのみ更新される。
そのためこの2つの状態を結合させることで、フォームをレンダーしているReactコンポーネントが後続のユーザ入力でフォームに起きることを制御できる。

例:フォーム送信時に名前をログとして残す。

logName.js
class NameForm extends React.Component {
  constructor(props) {
    super(props);
    this.handleChange = this.handleChange.bind(this);
    this.handleSubmit = this.handleSubmit.bind(this);
    this.state = (
      {value: ''}
    );

  }

  handleChange(event) {
    this.setState({value: event.target.value});
  }

  handleSubmit(event) {
    alert(`A name was submitted:${this.state.value}`);
    event.preventDefault();
  }

  render() {
    return (
      <form onSubmit={this.handleSubmit}>
        <label>
          Name:
          <input
            type="text"
            value={this.state.value}
            onChange={this.handleChange}
          />
        </label>
        <input type="submit" value="Submit" />
      </form>
    );
  }
}

ReactDOM.render(
  <NameForm />,
  document.getElementById('train')
);

textareaはvalue属性を使用する。

textarea.js
class AreaForm extends React.Component{
  constructor(props) {
    super(props);
    this.handleChange = this.handleChange.bind(this);
    this.handleSubmit = this.handleSubmit.bind(this);
    this.state = {
      value: 'Please write everything about your favorite thing.'
    };
  }

  handleChange(event) {
    this.setState(
      {value: event.target.value}
    );
  }

  handleSubmit(event) {
    alert('An essay was submitted: ' + this.state.value);
    event.preventDefault();
  }

  render() {
    return (
      <form onSubmit={this.handleSubmit}>
        <label>
          Essay:
          <textarea
            value={this.state.value}
            onChange={this.handleChange}
          />
        </label>
        <input type="submit" value="Submit" />
      </form>
    );
  }
}

ReactDOM.render(
  <AreaForm />,
  document.getElementById('train')
);

selectはselect属性があるので、親のselectタグのvalueを扱う。

select.js
class SelectForm extends React.Component{
  constructor(props){
    super(props);
    this.hundleChange = this.hundleChange.bind(this);
    this.hundleSubmit = this.hundleSubmit.bind(this);
    this.state = (
      {value: ''}
    );
  }

  hundleChange(event){
    this.setState({value: event.target.value});
  }

  hundleSubmit(event){
    alert(`Your favorite type is ${this.state.value}`);
    event.preventDefault();
  }

  render(){
    return(
      <form onSubmit={this.hundleSubmit}>
        <label>
          Pick your favorite type.
          <select value={this.state.value} onChange={this.hundleChange}>
            <option value="red">red</option>
            <option value="blue">blue</option>
            <option value="green">green</option>
            <option value="yellow">yellow</option>
            <option value="white">white</option>
          </select>
        </label>
        <input type="submit" value="Submit" />
      </form>
    );
  }
}


ReactDOM.render(
  <SelectForm />,
  document.getElementById('train')
);

value属性に配列を渡すことで複数オプションを選択することができる。

selectMulti.js
<select multiple={true} value={['B', 'C']}>

複数入力の処理

複数のinput要素を制御するには、name属性を追加すれば、event.target.nameに基づいて、処理を選択することができる。

multiInput.js
class Reservation extends React.Component{
  constructor(props){
    super(props);
    this.hundleInputChange = this.hundleInputChange.bind(this);
    this.state = {
      isGoing: true,
      numberOfGuest: 2
    };
  }

  hundleInputChange(event){
    const target = event.target;
    const value = target.type === 'checkbox' ? target.checked : target.value;
    const name = target.name;

    this.setState(
      {[name]: value}
    );
  }

  render(){
    return(
      <form>
        <label>
          Is going:
          <input
            name="isGoing"
            type="checkbox"
            checked={this.state.isGoing}
            onChange={this.hundleInputChange}
          />
        </label>
        <br />
        <label>
          Number of guest:
          <input
            name="numberOfGuest"
            type="number"
            value={this.state.numberOfGuest}
            onChange={this.hundleInputChange}
          />
        </label>
      </form>
    );
  }
}

ReactDOM.render(
  <Reservation />,
  document.getElementById('train')
);

制御された入力におけるnull値

制御されたコンポーネントでvalueプロパティに値を指定することでユーザーが値を変更できないようになる。

null.js
ReactDOM.render(<input value="hi" />, 
  document.getElementById('train'));

setTimeout(function() {
  ReactDOM.render(<input value={null} />, mountNode);
}, 1000);

制御されたコンポーネントの代替手段

制御されたコンポーネントは、あらゆる種類のデータの変更に対してイベントハンドラをかく場合などは、入力フォームを実装する代替手段である非制御コンポーネント – Reactを検討する。

本格的なソリューション

完全なソリューションを探している場合はformikが人気がある選択肢の一つ。

10.stateのリフトアップ

いくつかのコンポーネントが同一の変化するデータを反映する必要がある。そのような時は最も近い共通の祖先コンポーネントにstateをリフトアップすることが良い。

コード例:水が沸騰するかどうか計算する

base.js
function BoilingVerdict(props){
  if(props.celsius >= 100){
    return <p>The water would voil.</p>;
  }
  return <p>The water would not voil.</p>;
}

class Calculator extends React.Component{
  constructor(props){
    super(props);
    this.handleChange = this.handleChange.bind(props);
    this.state = (
      {temperature: ''}
    );
  }

  handleChange(e){
    this.setState(
      {temperature: e.target.value}
    );
  }

  render(){
    const temperature = this.state.temperature;
    return(
      <fieldset>
        <legend>Enter temperature in Celsius.</legend>
        <input
          value={this.state.temperature}
          onChange={this.handleChange}
        />
        <BoilingVerdict celsius={parseFloat(temperature)} />
      </fieldset>
    );
  }
}

ReactDOM.render(
  <Calculator />,
  document.getElementById('train')
);

上記に二つ目の要素追加(華氏温度)。

addprops.js
function BoilingVerdict(props){
  if(props.celsius >= 100){
    return <p>The water would voil.</p>;
  }
  return <p>The water would not voil.</p>;
}

const scaleNames = {
  c: 'Celsius',
  f: 'Fahrenheit'
};

class TemperatureInput extends React.Component{
  constructor(props){
    super(props);
    this.handleChange = this.handleChange.bind(props);
    this.state = (
      {temperature: ''}
    );
  }

  handleChange(e){
    this.setState(
      {temperature: e.target.value}
    );
  }

  render(){
    const temperature = this.state.temperature;
    const scale = this.props.scale;
    return(
      <fieldset>
        <legend>Enter temperature in {scaleNames[scale]}.</legend>
        <input
          value={this.state.temperature}
          onChange={this.handleChange}
        />
        <BoilingVerdict celsius={parseFloat(temperature)} />
      </fieldset>
    );
  }
}

class Calculator extends React.Component{
  render(){
    return(
      <div>
        <TemperatureInput scale='c' />
        <TemperatureInput scale='f' />
      </div>
    );
  }
}

ReactDOM.render(
  <Calculator />,
  document.getElementById('train')
);

片方のフィールドを変化させても、もう一方のフィールドの更新はされないため、二つのフィールドを同期させるようにする。

変換関数作成

trans.js
// 摂氏温度に変換
function toCelsius(fahrenheit){
  return (fahrenheit - 32) * 5 / 9;
}

// 華氏温度に変換
function toFahrenehit(celsius){
  return (celsius * 9 / 5) + 32;
}


// 常に小数第3位までで四捨五入されるようにする
function tryConvert(temperature, convert){
  const input = parseFloat(temperature);
  if(Number.isNaN(input)){
    return null;
  }
  const output = convert(input);
  const rounded = Math.round(output * 1000) / 1000;
  return rounded.toString();
}

stateのリフトアップ

まだ両方のTemperatureInputコンポーネントは独立してローカル stateを保持しているが、同期していてほしい。
よって、stateをリフトアップして実現する。(TemperatureInputからCalculatorに移動する。)
以下の手順で行う。

1.TemperatureInputコンポーネントのthis.state.temperaturethis.props.temperatureに変える。
this.props.temperatureは既にあるものとする。(後でこれはCalculatorから渡すようにする)

first.js
class TemperatureInput extends React.Component{
// 省略
  render(){
    // const temperature = this.state.temperature;
    const temperature = this.props.temperature;
// 省略
  }
}

2.TemperatureInputが自身の温度を更新したい場合は、this.props.onTemperatureChangeを呼び出す。(名前は任意)

second.js
class TemperatureInput extends React.Component{
// 省略
  handleChange(e){
    // this.setState({temperature: e.target.value});
    this.props.onTemperatureChange(e.target.value);
  }
// 省略
}

onTemperatureChangeプロパティは親コンポーネントCalculatorからtemperatureプロパティと共に渡される。親コンポーネントは入力の変化に応じて自身のローカルstateを更新し、両方の入力フォームは新しい値で再レンダーされる。

3.現時点のtemperaturescaleの入力を、このコンポーネントのローカルstateに保存する。(両方の入力の保存は不必要)

liftup.js
const scaleNames = {
  c: 'Celsius',
  f: 'Fahrenheit'
};

function toCelsius(fahrenheit){
  return (fahrenheit - 32) * 5 / 9;
}

function toFahrenehit(celsius){
  return (celsius * 9 / 5) + 32;
}

function tryConvert(temperature, convert){
  const input = parseFloat(temperature);
  if(Number.isNaN(input)){
    return '';
  }
  const output = convert(input);
  const rounded = Math.round(output * 1000) / 1000;
  return rounded.toString();
}

function BoilingVerdict(props){
  if(props.celsius >= 100){
    return <p>The water would boil.</p>;
  }
  return <p>The water would not boil.</p>;
}

class TemperatureInput extends React.Component{
  constructor(props){
    super(props);
    this.handleChange = this.handleChange.bind(this);
  }

  handleChange(e){
    this.props.onTemperatureChange(e.target.value);
  }

  render(){
    const temperature = this.props.temperature;
    const scale = this.props.scale;
    return(
      <fieldset>
        <legend>Enter temperature in {scaleNames[scale]}.</legend>
        <input
          value={temperature}
          onChange={this.handleChange}
        />
      </fieldset>
    );
  }
}

class Calculator extends React.Component{
  constructor(props){
    super(props);
    this.handleCelsiusChange = this.handleCelsiusChange.bind(this);
    this.handleFahrenheitChange = this.handleFahrenheitChange.bind(this);
    this.state = {
      temperature: '',
      scale: 'c'
    };
  }

  handleCelsiusChange(temperature){
    this.setState({scale: 'c', temperature});
  }

  handleFahrenheitChange(temperature){
    this.setState({scale: 'f', temperature});
  }

  render(){
    const scale = this.state.scale;
    const temperature = this.state.temperature;
    const celsius = scale === 'f' ? tryConvert(temperature, toCelsius) : temperature;
    const fahrenheit = scale === 'c' ? tryConvert(temperature, toFahrenehit) : temperature;
    return(
      <div>
        <TemperatureInput
          scale='c'
          temperature={celsius}
          onTemperatureChange={this.handleCelsiusChange}
        />
        <TemperatureInput
          scale='f'
          temperature={fahrenheit}
          onTemperatureChange={this.handleFahrenheitChange}
        />
        <BoilingVerdict celsius={parseFloat(celsius)} />
      </div>
    );
  }
}

ReactDOM.render(
  <Calculator />,
  document.getElementById('train')
);

これで、どちらの入力コンポーネントを編集したかに関係なく、Calculatorthis.state.temperaturethis.state.scaleが更新される。

上記の変更処理の流れ

DOMのinputでのonChangeで呼ばれた関数を実行する。
(TemperatureInputコンポーネントのhandleChange)
 ↓
TemperatureInputコンポーネントのhandleChangeはthis.props.temperature()に値を与えて呼び出す。
(onTemperatureChangeを含むpropsは、親クラスのCalculatorから与えられる)
 ↓
編集したフィールドによって、呼ばれるメソッドが決まる。
(CalculatorコンポーネントのhandleCelsiusChangeかhandleFahrenheitChange)
 ↓
このメソッドはCalculatorコンポーネントが新しい入力値と更新した値をthis.setStateに与えて呼び出し、Calculatorコンポーネントを再レンダリングする。
(Calculatorコンポーネントのrender)
 ↓
Calculatorコンポーネントのrenderを呼び出し、両方の入力コンポーネント値を再計算。
(toCelsius、toFahrenehit)
 ↓
Calculatorコンポーネントが与えた新しいpropsで各TemperatureInputコンポーネントのrenderメソッドが呼ばれる。
+
BoilingVerdictのコンポーネントのrenderメソッドが呼ばれる。
 ↓
以上の判定結果と入力コンポーネント値によってDOMを更新する。
+
変更された入力コンポーネントは現在の値によって、もう一方の入力コンポーネントは変換され更新される。

11.コンポジションと継承

子要素の出力

sidebarやDialogのような子要素を知らない汎用的な入れ物を表すコンポーネントで使用される。

child.js
function Fancyborder(props){
  return (
    <div className={`FancyBorder FancyBorder-${props.color}`}>
      {props.children}
    </div>
  );
}

function WelcomeDialog(){
  return(
    <Fancyborder color="blue">
      <h1 className="Dialog-title">
        Welcome
      </h1>
      <p className="Dialog-message">
        Hello React
      </p>
    </Fancyborder>
  );
}

ReactDOM.render(
  <WelcomeDialog />,
  document.getElementById('train')
);

上記のFancyBorderタグの要素はchildrenpropsで渡される。

一般的ではないが、childrenの代わりに独自のpropsを作成して、渡すこともできる。

orgChild.js
function Contacts(){
  return(
    <div className="Contacts">
      <p>about contact</p>
    </div>
  );
}

function Chat(){
  return(
    <div className="Chat">
      <p>chat</p>
    </div>
  );
}

function SplitPane(props){
  return(
    <div className="SplitPane">
      <div className="SplitPane-left">
        {props.left}
      </div>
      <div className="SplitPane-right">
        {props.right}
      </div>
    </div>
  );
}

function App(){
  return(
    <SplitPane
      left={<Contacts />}
      right={<Chat />}
    />
  );
}

ReactDOM.render(
  <App />,
  document.getElementById('train')
);

Reactの要素はオブジェクトなのでpropsとして渡すことができる。

特化したコンポーネント

コンポーネントは他のコンポーネントを特別な要素として扱うことができる。(以下はDialogに対してWelcomeDialogは特別なケース)

specializedComponent.js
function Dialog(props){
  return(
    <div className="blue">
      <h1 className="Dialog-title">
        {props.title}
      </h1>
      <p className="Dialog-message">
        {props.message}
      </p>
    </div>
  );
}

function WelcomeDialog(){
  return(
    <Dialog
      title="welcome"
      message="hello react"
    />
  );
}

ReactDOM.render(
  <WelcomeDialog />,
  document.getElementById('train')
);

コンポジションはクラスとして定義されたコンポーネントでも同じように動作する。

specializedClassComponent.js
function Fancyborder(props){
  return (
    <div className={`FancyBorder FancyBorder-${props.color}`}>
      {props.children}
    </div>
  );
}

function Dialog(props){
  return(
    <Fancyborder color="blue">
      <h1 className="Dialog-title">
        {props.title}
      </h1>
      <p className="Dialog-message">
        {props.message}
      </p>
      {props.children}
    </Fancyborder>
  );
}

class SignUpDialog extends React.Component{
  constructor(props){
    super(props);
    this.handleChange = this.handleChange.bind(this);
    this.handleSignUp = this.handleSignUp.bind(this);
    this.state = (
      {login: ''}
    );
  }

  handleChange(e){
    this.setState(
      {login: e.target.value}
    );
  }

  handleSignUp(){
    alert(`welcome a board ${this.state.login}`);
  }

  render(){
    return(
      <Dialog
        title="XYZ Program"
        message="How should we refer to you?"
      >
        <input
          value={this.state.login}
          onChange={this.handleChange}
        />
        <button onClick={this.handleSignUp}>
          Sign me up.
        </button>
      </Dialog>
    );
  }
}

ReactDOM.render(
  <SignUpDialog />,
  document.getElementById('train')
);

継承はどうするの

propsとコンポジションにより、コンポーネントの見た目、振る舞いを明示的にそして安全にカスタマイズするのに十分な柔軟性を得ることができる。コンポーネントはどのようなpropsでも受け付け、それはプリミティブ値、React要素、関数でもいい。

コンポーネント間で非UI機能を再利用したい場合、それを別の JavaScriptモジュールに抽出することがいい。
コンポーネントはその関数やオブジェクト、クラスなどを継承することなくインポートすることで使用することができる。

12.Reactの流儀

こちらからどうぞ

最後に

だらだらと長くなってしまいましたが、なんとか推敲することができたかなと...汗
間違っている!気になる!等ありましたら、
暖かい目で編集リクエストをいただければと思います。

参考

React - MAIN CONCEPT

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

DockerでReact + Swagger 環境を作ろう

現在業務でReactを使用しているのですが、事前に勉強する際にDocker環境を作成していたので、それを共有します。
ちなみにSwagger環境に関しては、自分で作成してうまくいかなかったので、上司が作成した環境を参考にさせてもらいました。感謝です:pray:

前提

  • Docker導入済み
  • docker-composeコマンドが使用できる

事前準備

今回は4つのコンテナを使用。

  • node...Reactのコードを実行するコンテナ
  • swagger-editor...Swagger Editorのコンテナ
  • swagger-api...Swagger定義ファイルから生成されたコードを実行するコンテナ
  • swagger-nginx...swagger-apiの内容を配信するアプリケーションサーバのコンテナ(nginxをさらに高性能に使えるようにしたopenrestyを使用)

これらのDockerfileや必要な設定ファイルを用意していきます。
ちなみに作成後のディレクトリは以下のようになります。

例
React(プロジェクトフォルダ)
├─ docker
|  ├─ nginx
|  |  ├─ swagger.conf
|  ├─ node
|  |  ├─ Dockerfile
|  ├─ swagger-api
|     ├─ Dockerfile
|     ├─ swagger.json
├─ docker-compose.yml

nginx/swagger.conf

nginx(openresty)の設定を記述します。
ここでCORSに関する設定をしておかないと、React側からaxiosでリクエストする際にエラーになってしまいます。
(自分が最初環境を作ろうとしたときに、ここでつまづいていました。今も設定に関しては疎いです...。)

server {
  listen 80;
  server_name localhost;

  location /docs/ {
    proxy_pass http://swagger-api:8000/docs/;
  }

  location / {
    if ($request_method = 'OPTIONS') {
      add_header Access-Control-Allow-Origin $http_origin;
      add_header Access-Control-Allow-Headers 'X-Requested-With, Authorization, Origin, Accept, Content-Type';
      add_header Access-Control-Allow-Methods 'GET, POST, PUT, DELETE';
      add_header Access-Control-Max-Age 86400;
      add_header Access-Control-Allow-Credentials true;
      add_header Access-Control-Expose-Headers Set-Cookie;
      add_header Content-Type 'text/plain charset=UTF-8';
      add_header Content-Length 0;
      return 204;
    }
    proxy_pass http://swagger-api:8000;
    add_header Access-Control-Allow-Origin $http_origin always;
    add_header Access-Control-Allow-Credentials true always;
    add_header Access-Control-Expose-Headers Content-Disposition;
  }
}

node/Dockerfile

Facebook公式のReactプロジェクトを作成するCLIツールであるcreate-react-appを使用するので、インストールしておきます。

FROM "node:12-alpine"

WORKDIR /usr/src/app/

RUN yarn global add create-react-app

swagger-api/Dockerfile

Swagger定義ファイルをもとにコードを生成するSwagger Codegenをインストールして、実際にnode.jsのコードを生成しています。

FROM openjdk:8-jdk-alpine

ARG CLI_VERSION=2.4.5

RUN wget http://central.maven.org/maven2/io/swagger/swagger-codegen-cli/${CLI_VERSION}/swagger-codegen-cli-${CLI_VERSION}.jar -O /swagger-codegen-cli.jar

RUN apk --update add bash nodejs npm && rm -rf /var/cache/apk/* 

ADD swagger.json swagger.json

RUN java -jar /swagger-codegen-cli.jar generate -l nodejs-server -i swagger.json -o src && \
    cd src && npm install

CMD ["java", "-jar", "/swagger-codegen-cli.jar"]

swagger-api/swagger.json

Swagger定義ファイルです。
ここでは簡単に1つだけAPIを定義しています。

{
  "swagger": "2.0",
  "info": {
    "description": "テスト用API",
    "version": "1.0.0",
    "title": "Swagger test API設計書",
    "contact": {},
    "license": {
      "name": ""
    }
  },
  "host": "localhost:8000",
  "basePath": "/",
  "tags": [
    {
      "name": "User",
      "description": "User Controller"
    }
  ],
  "paths": {
    "/user": {
      "get": {
        "tags": [
          "User"
        ],
        "summary": "ユーザ情報取得",
        "description": "ユーザ情報を取得する。",
        "operationId": "doGetUsingGET",
        "consumes": [
          "application/json"
        ],
        "produces": [
          "application/json"
        ],
        "responses": {
          "200": {
            "description": "OK",
            "schema": {
              "$ref": "#/definitions/UserGetOut"
            }
          },
          "400": {
            "description": "バリデーションエラー",
            "schema": {
              "$ref": "#/definitions/ValidationErrorsOut"
            }
          },
          "500": {
            "description": "想定外のエラー",
            "schema": {
              "$ref": "#/definitions/ErrorInfo"
            }
          }
        }
      }
    }
  },
  "definitions": {
    "UserGetOut": {
      "type": "object",
      "required": [
        "userId",
        "name",
        "kana"
      ],
      "properties": {
        "userId": {
          "type": "string",
          "example": "abc001",
          "description": "ユーザID"
        },
        "name": {
          "type": "string",
          "example": "山田 太郎",
          "description": "名前"
        },
        "kana": {
          "type": "string",
          "example": "ヤマダ タロウ",
          "description": "カナ"
        }
      }
    },
    "ValidationError": {
      "type": "object",
      "required": [
        "itemName"
        "code",
        "content"
      ],
      "properties": {
        "itemName": {
          "type": "string",
          "example": "userId",
          "description": "項目名"
        },
        "code": {
          "type": "string",
          "example": "NotBlank",
          "description": "code"
        },
        "content": {
          "type": "string",
          "example": "必須項目が設定されていません。",
          "description": "content"
        }
      }
    },
    "ValidationErrorsOut": {
      "type": "object",
      "required": [
        "validationInfo"
      ],
      "properties": {
        "validationInfo": {
          "type": "array",
          "description": "バリデーション情報",
          "items": {
            "$ref": "#/definitions/ValidationError"
          }
        }
      }
    },
    "ErrorInfo": {
      "type": "object",
      "required": [
        "code",
        "content"
      ],
      "properties": {
        "code": {
          "type": "string",
          "example": "exception.errors.unexpeced.exception",
          "description": "エラーコード"
        },
        "content": {
          "type": "string",
          "example": "システムエラーが発生しました。誠に恐れ入りますが、初めからやり直して下さい。",
          "description": "メッセージ"
        }
      }
    }
  }
}

docker-compose.yml

今回使用する4つのコンテナ定義を書いていきます。
なお、nodeのenvironmentのCHOKIDAR_USEPOLLING=trueはホットリロードを有効化にするものです。

version: "3"
services:
  node:
    build: ./docker/node
    environment:
      - NODE_ENV=development
      - CHOKIDAR_USEPOLLING=true
    volumes:
      - ./app/:/usr/src/app
    tty: true
    stdin_open: true
    ports:
      - "3000:3000"

  swagger-editor:
    image: swaggerapi/swagger-editor
    ports:
      - "8081:8080"

  swagger-api:
    build: ./docker/swagger-api
    working_dir: /src
    command: node index.js

  swagger-nginx:
    image: openresty/openresty:alpine
    ports:
      - 8000:80
    depends_on:
      - swagger-api
    volumes:
      - ./docker/nginx/swagger.conf:/etc/nginx/conf.d/default.conf:ro

コンテナの作成と起動

コンテナのビルド

$ docker-compose build

コンテナをバックグラウンドで起動

$ docker-compose up -d

Reactプロジェクト作成と起動

1.コンテナの中に入る

$ docker-compose exec node sh

2.プロジェクトのひな型作成

$ create-react-app .

これでプロジェクトのひな型が作成されます。
この時点でのディレクトリ構成は以下のようになります。

例
React(プロジェクトフォルダ)
├─ app
|  ├─ node_modules
|  |  ├─ (各種パッケージ)
|  ├─ public
|  |  ├─ favicon.ico
|  |  ├─ index.html
|  |  ├─ logo192.png
|  |  ├─ logo512.png
|  |  ├─ manifest.json
|  |  ├─ rebots.txt
|  ├─ src
|  |  ├─ App.css
|  |  ├─ App.js
|  |  ├─ App.test.js
|  |  ├─ index.css
|  |  ├─ index.js
|  |  ├─ logo.svg
|  |  ├─ serviceWorker.js
|  ├─ .gitignore
|  ├─ package.json
|  ├─ README.md
|  ├─ yarn.lock
├─ docker
|  ├─ nginx
|    ├─ swagger.conf
|  ├─ node
|    ├─ Dockerfile
|  ├─ swagger-api
|    ├─ Dockerfile
|    ├─ swagger.json
├─ docker-compose.yml

3.サーバの起動

$ yarn start

4.ブラウザでアクセス
localhost:3000にアクセスして、以下のような画面になればOKです。
ちなみにホットリロードを有効化にしているため、コードを変更して保存すると即座に反映されます。
react-app.png

なお、毎回コンテナの中に入ってサーバを起動するのが面倒な場合は、nodeのDockerfileの末尾に以下を追記してビルドしなおせば、次回からはコンテナ起動時にサーバも一緒に起動してくれます。

CMD ["yarn", "start"]

Swagger

Swagger Editor

localhost:8081にアクセス。
左側がエディタ、右側が定義をもとにしたドキュメントになっています。

デフォルトではサンプルのAPI定義が表示されます。
swagger-editor.png

Swagger定義ファイルを読み込む場合は、File→Import Fileからインポートできます。
なお、エディタ上から定義をもとにしたコードを生成することもできます。

※注意点
エディタ上の変更は自動で保存されますが、インポートしたファイル自体を保存までは行ってくれません。
エディタで変更した定義ファイルをアプリケーションサーバに反映させたい場合は、File→Convert and save as JSONでファイルをダウンロードし、React(プロジェクトフォルダ)/docker/swagger-api配下に設置して、コンテナをビルドしなおす必要があります。

Swagger UI

Swagger Codegenを使用してコードを生成すると、あわせてドキュメントも作成してくれています。
localhost:8000/docsにアクセスすることで確認できます。
swagger-ui.png

ちなみに最初はSwagger UIのコンテナをまた別で立ち上げていたのですが、参照するSwagger定義ファイルを編集しているうちに、ファイルに間違いがない状態でもなぜかエラーが出てしまい、いまいち使えなくなってしまいました。(Swagger Editor上で何もエラーが出ていない状態で出力したファイルでもエラーになりました)

Swaggerのサーバ

localhost:8000でアクセスできます。
今回はSwagger定義ファイルで定義しているAPIにリクエストしてみます。

ブラウザからアクセス

GETのAPIなので、直接ブラウザからアクセスしてもレスポンスが返ってきます。
localhost:8000/userにアクセスすると以下のようになります。
swagger-api-browser.png

axiosでアクセス

まずはコンテナの中に入り、yarnでaxiosをインストール。

$ yarn add axios

src/App.jsを以下のように修正。
(あまりいい書き方ではないと思いますが、とりあえず動作確認用のコードです:bow:

import React from 'react';
import logo from './logo.svg';
import './App.css';
import axios from 'axios'; // 追記

function App() {
  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <p>
          Edit <code>src/App.js</code> and save to reload.
        </p>
        <a
          className="App-link"
          href="https://reactjs.org"
          target="_blank"
          rel="noopener noreferrer"
        >
          Learn React
        </a>
        {/* 以下を追記 */}
        <button
          onClick={
            () => axios.get('http://localhost:8000/user')
                       .then((result) => {
                         console.log(result)
                       })
                       .catch((error) => {
                         console.log(error)
                       })
          }
        >
          APIリクエスト
        </button>
        {/* ここまで追記 */}
      </header>
    </div>
  );
}

export default App;

localhost:3000にアクセスし、APIリクエストのボタンを押すとaxiosのリクエストが実行されます。
コンソールに出力しているので、Developer Toolsで確認すると以下のようにデータが取得できているのが確認できるかと思います。
swagger-api-axios.png

その他必要なもの

使用しているエディタにReactに関する拡張を導入するとより開発しやすくなります。

VSCode拡張

Full React/React Native/React Router/Redux/GraphQL/ES7/Testing/PropTypes snippets

名称にある通り、各種のスニペットが使用でき、慣れてくるとコーディングが捗るかと思います。
(自分はあまり使いこなせてなかったりします...)

Chrome拡張

React Developer Tools

Developer ToolsにComponentsとProfilerタブが追加されます。
コンポーネントの階層や、propsやstateの中身を確認したりすることができデバッグがしやすくなります。

  1. Chromeウェブストアからインストール

  2. ChromeのReact Developer Toolsアイコンを右クリックして、「拡張機能を管理」へ

  3. 「ファイルの URL へのアクセスを許可する」をONにする


静的解析ツールであるESLintも導入した方がいいとは思うのですが、VSCode上で動かすのがうまくいかなかったので今回は書いていません。
(いずれちゃんと設定できたら書くかも...)

参考

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

react-native-sqlite-2@3.0.1 リリースしました

原文: Announcing react-native-sqlite-2@3.0.1! - DEV Community ?‍??‍?

InkdropというアプリをReact Nativeで作っていて、PouchDBを動かすためにWebSQL互換のライブラリが必要だったのでreact-native-sqlite-2を作りました。

なんで?

すでにreact-native-sqlite-storageというSQLiteを扱うためのライブラリが存在しますが、拙作の方には少しアドバンテージがあります:

本ライブラリはこれらの問題を解決します。

What's new in 3.x

Androidにとって大きな改善点があります!

より新しいSQLite3をAndroidで

最新のAndroid OSのバージョンであっても、標準で搭載されているSQLiteのバージョンはいくつか古いものです。
iOSの方が新しいものが使えるのに。
React Native SQLite 2はこのアップデートによってsqlite-androidを使うようになり、SQLiteの新機能が使えるようになりました:

これでiOSでは出来たのにAndroidでは実装できないという悩みが減ります。
自分はFTS5が使いたかった。これはビルドフラグが単純に付いてないだけだったけど。

以降は使い方です。

Getting started

Add react-native-sqlite-2 to your dependencies:

$ npm install react-native-sqlite-2 --save

Link native dependencies

From react-native 0.60 autolinking will take care of the link step but don't forget to run pod install.

$ react-native link react-native-sqlite-2

iOS

If using cocoapods in the ios/ directory run

$ pod install

Android

Please make sure AndroidX is enabled in your project by editting android/gradle.properties and adding 2 lines:

android.useAndroidX=true
android.enableJetifier=true

Usage

import SQLite from "react-native-sqlite-2";

const db = SQLite.openDatabase("test.db", "1.0", "", 1);
db.transaction(function(txn) {
  txn.executeSql("DROP TABLE IF EXISTS Users", []);
  txn.executeSql(
    "CREATE TABLE IF NOT EXISTS Users(user_id INTEGER PRIMARY KEY NOT NULL, name VARCHAR(30))",
    []
  );
  txn.executeSql("INSERT INTO Users (name) VALUES (:name)", ["nora"]);
  txn.executeSql("INSERT INTO Users (name) VALUES (:name)", ["takuya"]);
  txn.executeSql("SELECT * FROM `users`", [], function(tx, res) {
    for (let i = 0; i < res.rows.length; ++i) {
      console.log("item:", res.rows.item(i));
    }
  });
});

There is a test app in the test directory.

Using with PouchDB

It can be used with pouchdb-adapter-react-native-sqlite.

import PouchDB from "pouchdb-react-native";
import SQLite from "react-native-sqlite-2";
import SQLiteAdapterFactory from "pouchdb-adapter-react-native-sqlite";

const SQLiteAdapter = SQLiteAdapterFactory(SQLite);
PouchDB.plugin(SQLiteAdapter);
var db = new PouchDB("mydb", { adapter: "react-native-sqlite" });

Further informations

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