- 投稿日:2019-10-23T19:40:18+09:00
Gatsby.js + NetlifyCMSのデータの流れまとめ(+投稿画面、URLのカスタマイズ)
Gatsby(とNetlifyCMSを使ったとき)のデータの流れを書きたいと思います。
どのディレクトリ/ファイルが何をしているか、俯瞰できる記事がなかったので。
また、ある箇所をカスタマイズするためにはどのファイルをいじればいいかも逆引き的に書きました。
この記事では、CMSでの投稿画面やURLのカスタマイズをしています。Gatsbyの説明や、Netlifyとの連携については他の記事に任せます。
ちなみに使ったスターターはgatsby-starter-netlify-cmsです。(https://github.com/netlify-templates/gatsby-starter-netlify-cms)
このスターター特有の話があったらごめんなさい!データの流れ
CMSで記事を書く
↓
マークダウンファイルができる
↓
マークダウンファイルからデータを抜き出してGraphQLに入れる
↓
GraphQLからデータを取り出して、テンプレートに埋め込むって感じです。
流れがわかったので、具体的に、ある箇所をいじるためにはどのファイルを触ればいいか見ていきます。逆引き ~これをするにはどこを触ればいいのか~
CMSの投稿編集ページの管理 → static/admin/config.yml
このファイルでCMSの投稿画面をカスタマイズします。
デフォルトだと、PostとPagesしか編集できません(左側)。
この編集可能なページの種類のことをcollectionと言うらしいです。
static/admin/config.ymlのcollectionsに設定を追加することで、他のフォルダのページを作成/編集することができるようになります。
例えば、Post、Pagesの他にSpecialPostという種類のページを編集できるようにしたいときは以下を追加します。
簡単のため、fieldは、埋め込むテンプレートとタイトルだけにしています。
fieldのwidgetの種類などは以下を参照のこと。
https://www.netlifycms.org/docs/widgets/static/admin/config.ymlcollections: - 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"}
無事、新たに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というラベルで文字列が編集できる欄が現れます(一番上)。
この欄に、使いたいテンプレートの名前を書いてあげれば良いのです。
これで無事テンプレートを使い分けられるようになりました。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.jsposts.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.jsexports.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.jsexports.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。
- 投稿日:2019-10-23T19:40:10+09:00
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.tsximport 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.tsimport 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.tsimport 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 で難がありますが)。
- 投稿日:2019-10-23T19:40:10+09:00
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.tsximport 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.tsimport 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.tsimport 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 で難がありますが)。
- 投稿日:2019-10-23T18:11:49+09:00
【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.jsconst name = 'taro'; const element = <h1>Hello {name}</h1>; ReactDOM.render(element, document.getElementbyId('root'));JSXも式である
コンパイル後、JSXは普通のJavascriptの関数呼び出しに変換されて、Javascriptオブジェクトとして評価される。
formula.jsfunction getGreeding(user){ if(user){ return <h1>Hello {user}.</h1>; } return <h1>Hello foo.</h1>; }属性指定
文字列リテラルを属性として指定するために
""(引用符)を使用できる。
また、Javascript埋め込む際は、{}(中括弧)を使用する。
Javascriptを使用時には、中括弧を囲む引用符を使用しない。str.jsconst 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.jsconst child = ( <div> <h1>Hello React</h1> <h2>Hello JSX</h2> </div> ); const close = <img src={foo} />;JSXはインジェクション攻撃を防ぐ
デフォルトでReactDOMは、JSXに埋め込まれた値をレンダリングされる前にエスケープするため、レンダーの前に全てが文字列に変換される。これはXSS攻撃の防止になる。
xss.jsconst title = response.malInput; const element = <h1>{title}</h1>;JSXはオブジェクトの表現
BabelはJSXを
React.createElement()の呼び出しへコンパイルする。
下記の2つは同じになる。element.jsconst 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.jsconst element = <h1>Hello React</h1>; ReactDOM.render(element, document.getElementbyId('root'));レンダリングされた要素の更新
React要素はImmutableのため一度作成すると変更ができない。
更新する方法は、新しい要素を作成して、ReactDOM.render()に渡す。
以下は、秒ごとに動く時計の例です。clock.jsfunction 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.jsfunction Hello(props){ return <p>Hello {props.name}.</p>; }クラス component
class.jsclass Hello extends React.Component{ render(){ return <p>Hello {this.props.name}.</p>; } }componentのrender
DOMのタグを表すReact要素だけではなく、ユーザ定義のcomponentを表すことができる。
Reactがユーザ定義のコンポーネントを見つけた場合、JSXの属性を単一のオブジェクトとして、コンポーネントに渡す。
このオブジェクトのことはpropsと呼ぶ。comp.jsfunction 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.jsfunction 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.jsfunction 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.jsfunction Avatar(props){ return( <img src={props.user.imageUrl} alt={props.user.name} className="avatar" /> ); }と
userinfo.jsfunction UserInfo(props){ render( <div className="userinfo"> <Avatar user={props.user} /> <div className="username"> {props.user.name} </div> </div> ); }↓ よりシンプルにする。
alternateBase.jsfunction 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.jsfunction 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を利用する。
stateはpropsに似ているが、コンポーネントに管理されるプライベートなもの。関数をクラスに変換する
以下の流れで関数コンポーネントをクラスに変換する。
1.React.Componentを継承する同名のes6のクラスを作成。
2.render()と呼ばれる空のメソッドを1つ追加。
3.関数の中身をrender()メソッドに移動。
4.render()内のpropsをthis.propsに書き換える。
5.空になった関数の宣言部分を削除。以下コード
clock.jsclass 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を設定
以下のステップで
dateをpropsからstateに移動する。
1.render()メソッド内のthis.props.dateをthis.state.dateに書き換える。
2.this.stateを初期化するクラスコンストラクタを作成する。
3.<Clock />要素から、dateプロパティを削除する。以下コード
clock.jsclass 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.jsclass 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.jsconst this.state.foo = 'abc'; // NG2.stateの更新は非同期で行われる時がある。
this.propsとthis.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.jsconstructor(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.jsfunction 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.jsclass 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.jsfunction 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
以下、
unreadMsgがtrueなら&&以降が返され、falseならfalseが返されるinlineIf.jsfunction 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.jsrender(){ const isLogin = this.state.isLogin; return ( <div> The user is <b>{isLogin ? 'currently' : 'not'}</b> log in </div> ); }コンポーネントのレンダーを防ぐ
render()の代わりにnullを返す。
コンポーネントのrenderメソッドからnullを返してもコンポーネントのライフサイクルメソッドのトリガーには影響しない。preventRender.jsfunction 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.jsconst 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.jsfunction 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.jsconst numbers = [1,2,3,4,5]; const listNumber = numbers.map(number => { return <li key={number.toString()}>{number}</li> });兄弟間で項目を一意に特定できる
keyを設定するのが最良、
多くの場合はデータ内のidをkeyとして使用することになる。key2.jsconst 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.jsconst todos = [ {text: "a"}, {text: "b"}, {text: "c"}, {text: "d"}, {text: "e"} ]; const todoItem = todos.map((todo, index) => { return <li key={index}>{todo.text}</li> });要素の並び順を変更される可能性がある場合は、
indexをkeyとして使用することは、パフォーマンスに悪影響を与え、コンポーネントの状態に問題を起こさせる可能性がある。詳しくは、Robin Pokornyの解説を参照。keyのあるコンポーネントの抽出
keyが意味を持つのは、それを取り囲んでいる配列側の文脈になる。例:ダメパターン
ngKey.jsfunction 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.jsfunction 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.jsfunction 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.jsconst 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.jsfunction NumberList(props){ const numbers = props.numbers: const listItems = numbers.map(number => <ListItem key={number.toString()} value={number} /> ); return ( <ul> {listItems} </ul> ); }
map()の結果をインライン化することもできる。embInline.jsfunction 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.jsclass 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.jsclass 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.jsclass 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.jsclass 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.jsReactDOM.render(<input value="hi" />, document.getElementById('train')); setTimeout(function() { ReactDOM.render(<input value={null} />, mountNode); }, 1000);制御されたコンポーネントの代替手段
制御されたコンポーネントは、あらゆる種類のデータの変更に対してイベントハンドラをかく場合などは、入力フォームを実装する代替手段である非制御コンポーネント – Reactを検討する。
本格的なソリューション
完全なソリューションを探している場合はformikが人気がある選択肢の一つ。
10.stateのリフトアップ
いくつかのコンポーネントが同一の変化するデータを反映する必要がある。そのような時は最も近い共通の祖先コンポーネントに
stateをリフトアップすることが良い。コード例:水が沸騰するかどうか計算する
base.jsfunction 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.jsfunction 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.temperatureをthis.props.temperatureに変える。
this.props.temperatureは既にあるものとする。(後でこれはCalculatorから渡すようにする)first.jsclass TemperatureInput extends React.Component{ // 省略 render(){ // const temperature = this.state.temperature; const temperature = this.props.temperature; // 省略 } }2.
TemperatureInputが自身の温度を更新したい場合は、this.props.onTemperatureChangeを呼び出す。(名前は任意)second.jsclass TemperatureInput extends React.Component{ // 省略 handleChange(e){ // this.setState({temperature: e.target.value}); this.props.onTemperatureChange(e.target.value); } // 省略 }↑
onTemperatureChangeプロパティは親コンポーネントCalculatorからtemperatureプロパティと共に渡される。親コンポーネントは入力の変化に応じて自身のローカルstateを更新し、両方の入力フォームは新しい値で再レンダーされる。3.現時点の
temperatureとscaleの入力を、このコンポーネントのローカルstateに保存する。(両方の入力の保存は不必要)liftup.jsconst 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') );これで、どちらの入力コンポーネントを編集したかに関係なく、
Calculatorのthis.state.temperatureとthis.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.jsfunction 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タグの要素はchildrenとpropsで渡される。一般的ではないが、
childrenの代わりに独自のpropsを作成して、渡すこともできる。orgChild.jsfunction 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.jsfunction 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.jsfunction 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の流儀
こちらからどうぞ
最後に
だらだらと長くなってしまいましたが、なんとか推敲することができたかなと...汗
間違っている!気になる!等ありましたら、
暖かい目で編集リクエストをいただければと思います。参考
- 投稿日:2019-10-23T18:11:49+09:00
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.jsconst name = 'taro'; const element = <h1>Hello {name}</h1>; ReactDOM.render(element, document.getElementbyId('root'));JSXも式である
コンパイル後、JSXは普通のJavascriptの関数呼び出しに変換されて、Javascriptオブジェクトとして評価される。
formula.jsfunction getGreeding(user){ if(user){ return <h1>Hello {user}.</h1>; } return <h1>Hello foo.</h1>; }属性指定
文字列リテラルを属性として指定するために
""(引用符)を使用できる。
また、Javascript埋め込む際は、{}(中括弧)を使用する。
Javascriptを使用時には、中括弧を囲む引用符を使用しない。str.jsconst 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.jsconst child = ( <div> <h1>Hello React</h1> <h2>Hello JSX</h2> </div> ); const close = <img src={foo} />;JSXはインジェクション攻撃を防ぐ
デフォルトでReactDOMは、JSXに埋め込まれた値をレンダリングされる前にエスケープするため、レンダーの前に全てが文字列に変換される。これはXSS攻撃の防止になる。
xss.jsconst title = response.malInput; const element = <h1>{title}</h1>;JSXはオブジェクトの表現
BabelはJSXを
React.createElement()の呼び出しへコンパイルする。
下記の2つは同じになる。element.jsconst 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.jsconst element = <h1>Hello React</h1>; ReactDOM.render(element, document.getElementbyId('root'));レンダリングされた要素の更新
React要素はImmutableのため一度作成すると変更ができない。
更新する方法は、新しい要素を作成して、ReactDOM.render()に渡す。
以下は、秒ごとに動く時計の例です。clock.jsfunction 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.jsfunction Hello(props){ return <p>Hello {props.name}.</p>; }クラス component
class.jsclass Hello extends React.Component{ render(){ return <p>Hello {this.props.name}.</p>; } }componentのrender
DOMのタグを表すReact要素だけではなく、ユーザ定義のcomponentを表すことができる。
Reactがユーザ定義のコンポーネントを見つけた場合、JSXの属性を単一のオブジェクトとして、コンポーネントに渡す。
このオブジェクトのことはpropsと呼ぶ。comp.jsfunction 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.jsfunction 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.jsfunction 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.jsfunction Avatar(props){ return( <img src={props.user.imageUrl} alt={props.user.name} className="avatar" /> ); }と
userinfo.jsfunction UserInfo(props){ render( <div className="userinfo"> <Avatar user={props.user} /> <div className="username"> {props.user.name} </div> </div> ); }↓ よりシンプルにする。
alternateBase.jsfunction 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.jsfunction 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を利用する。
stateはpropsに似ているが、コンポーネントに管理されるプライベートなもの。関数をクラスに変換する
以下の流れで関数コンポーネントをクラスに変換する。
1.React.Componentを継承する同名のes6のクラスを作成。
2.render()と呼ばれる空のメソッドを1つ追加。
3.関数の中身をrender()メソッドに移動。
4.render()内のpropsをthis.propsに書き換える。
5.空になった関数の宣言部分を削除。以下コード
clock.jsclass 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を設定
以下のステップで
dateをpropsからstateに移動する。
1.render()メソッド内のthis.props.dateをthis.state.dateに書き換える。
2.this.stateを初期化するクラスコンストラクタを作成する。
3.<Clock />要素から、dateプロパティを削除する。以下コード
clock.jsclass 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.jsclass 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.jsconst this.state.foo = 'abc'; // NG2.stateの更新は非同期で行われる時がある。
this.propsとthis.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.jsconstructor(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.jsfunction 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.jsclass 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.jsfunction 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
以下、
unreadMsgがtrueなら&&以降が返され、falseならfalseが返されるinlineIf.jsfunction 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.jsrender(){ const isLogin = this.state.isLogin; return ( <div> The user is <b>{isLogin ? 'currently' : 'not'}</b> log in </div> ); }コンポーネントのレンダーを防ぐ
render()の代わりにnullを返す。
コンポーネントのrenderメソッドからnullを返してもコンポーネントのライフサイクルメソッドのトリガーには影響しない。preventRender.jsfunction 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.jsconst 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.jsfunction 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.jsconst numbers = [1,2,3,4,5]; const listNumber = numbers.map(number => { return <li key={number.toString()}>{number}</li> });兄弟間で項目を一意に特定できる
keyを設定するのが最良、
多くの場合はデータ内のidをkeyとして使用することになる。key2.jsconst 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.jsconst todos = [ {text: "a"}, {text: "b"}, {text: "c"}, {text: "d"}, {text: "e"} ]; const todoItem = todos.map((todo, index) => { return <li key={index}>{todo.text}</li> });要素の並び順を変更される可能性がある場合は、
indexをkeyとして使用することは、パフォーマンスに悪影響を与え、コンポーネントの状態に問題を起こさせる可能性がある。詳しくは、Robin Pokornyの解説を参照。keyのあるコンポーネントの抽出
keyが意味を持つのは、それを取り囲んでいる配列側の文脈になる。例:ダメパターン
ngKey.jsfunction 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.jsfunction 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.jsfunction 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.jsconst 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.jsfunction NumberList(props){ const numbers = props.numbers: const listItems = numbers.map(number => <ListItem key={number.toString()} value={number} /> ); return ( <ul> {listItems} </ul> ); }
map()の結果をインライン化することもできる。embInline.jsfunction 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.jsclass 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.jsclass 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.jsclass 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.jsclass 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.jsReactDOM.render(<input value="hi" />, document.getElementById('train')); setTimeout(function() { ReactDOM.render(<input value={null} />, mountNode); }, 1000);制御されたコンポーネントの代替手段
制御されたコンポーネントは、あらゆる種類のデータの変更に対してイベントハンドラをかく場合などは、入力フォームを実装する代替手段である非制御コンポーネント – Reactを検討する。
本格的なソリューション
完全なソリューションを探している場合はformikが人気がある選択肢の一つ。
10.stateのリフトアップ
いくつかのコンポーネントが同一の変化するデータを反映する必要がある。そのような時は最も近い共通の祖先コンポーネントに
stateをリフトアップすることが良い。コード例:水が沸騰するかどうか計算する
base.jsfunction 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.jsfunction 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.temperatureをthis.props.temperatureに変える。
this.props.temperatureは既にあるものとする。(後でこれはCalculatorから渡すようにする)first.jsclass TemperatureInput extends React.Component{ // 省略 render(){ // const temperature = this.state.temperature; const temperature = this.props.temperature; // 省略 } }2.
TemperatureInputが自身の温度を更新したい場合は、this.props.onTemperatureChangeを呼び出す。(名前は任意)second.jsclass TemperatureInput extends React.Component{ // 省略 handleChange(e){ // this.setState({temperature: e.target.value}); this.props.onTemperatureChange(e.target.value); } // 省略 }↑
onTemperatureChangeプロパティは親コンポーネントCalculatorからtemperatureプロパティと共に渡される。親コンポーネントは入力の変化に応じて自身のローカルstateを更新し、両方の入力フォームは新しい値で再レンダーされる。3.現時点の
temperatureとscaleの入力を、このコンポーネントのローカルstateに保存する。(両方の入力の保存は不必要)liftup.jsconst 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') );これで、どちらの入力コンポーネントを編集したかに関係なく、
Calculatorのthis.state.temperatureとthis.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.jsfunction 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タグの要素はchildrenとpropsで渡される。一般的ではないが、
childrenの代わりに独自のpropsを作成して、渡すこともできる。orgChild.jsfunction 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.jsfunction 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.jsfunction 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の流儀
こちらからどうぞ
最後に
だらだらと長くなってしまいましたが、なんとか推敲することができたかなと...汗
間違っている!気になる!等ありましたら、
暖かい目で編集リクエストをいただければと思います。参考
- 投稿日:2019-10-23T16:49:08+09:00
DockerでReact + Swagger 環境を作ろう
現在業務でReactを使用しているのですが、事前に勉強する際にDocker環境を作成していたので、それを共有します。
ちなみにSwagger環境に関しては、自分で作成してうまくいかなかったので、上司が作成した環境を参考にさせてもらいました。感謝です前提
- 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.ymlnginx/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-appswagger-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 -dReactプロジェクト作成と起動
1.コンテナの中に入る
$ docker-compose exec node sh2.プロジェクトのひな型作成
$ 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.yml3.サーバの起動
$ yarn start4.ブラウザでアクセス
localhost:3000にアクセスして、以下のような画面になればOKです。
ちなみにホットリロードを有効化にしているため、コードを変更して保存すると即座に反映されます。
なお、毎回コンテナの中に入ってサーバを起動するのが面倒な場合は、nodeのDockerfileの末尾に以下を追記してビルドしなおせば、次回からはコンテナ起動時にサーバも一緒に起動してくれます。
CMD ["yarn", "start"]Swagger
Swagger Editor
localhost:8081にアクセス。
左側がエディタ、右側が定義をもとにしたドキュメントになっています。Swagger定義ファイルを読み込む場合は、File→Import Fileからインポートできます。
なお、エディタ上から定義をもとにしたコードを生成することもできます。※注意点
エディタ上の変更は自動で保存されますが、インポートしたファイル自体を保存までは行ってくれません。
エディタで変更した定義ファイルをアプリケーションサーバに反映させたい場合は、File→Convert and save as JSONでファイルをダウンロードし、React(プロジェクトフォルダ)/docker/swagger-api配下に設置して、コンテナをビルドしなおす必要があります。Swagger UI
Swagger Codegenを使用してコードを生成すると、あわせてドキュメントも作成してくれています。
localhost:8000/docsにアクセスすることで確認できます。
ちなみに最初はSwagger UIのコンテナをまた別で立ち上げていたのですが、参照するSwagger定義ファイルを編集しているうちに、ファイルに間違いがない状態でもなぜかエラーが出てしまい、いまいち使えなくなってしまいました。(Swagger Editor上で何もエラーが出ていない状態で出力したファイルでもエラーになりました)
Swaggerのサーバ
localhost:8000でアクセスできます。
今回はSwagger定義ファイルで定義しているAPIにリクエストしてみます。ブラウザからアクセス
GETのAPIなので、直接ブラウザからアクセスしてもレスポンスが返ってきます。
localhost:8000/userにアクセスすると以下のようになります。
axiosでアクセス
まずはコンテナの中に入り、yarnでaxiosをインストール。
$ yarn add axiossrc/App.jsを以下のように修正。
(あまりいい書き方ではないと思いますが、とりあえず動作確認用のコードです)
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で確認すると以下のようにデータが取得できているのが確認できるかと思います。
その他必要なもの
使用しているエディタにReactに関する拡張を導入するとより開発しやすくなります。
VSCode拡張
Full React/React Native/React Router/Redux/GraphQL/ES7/Testing/PropTypes snippets
名称にある通り、各種のスニペットが使用でき、慣れてくるとコーディングが捗るかと思います。
(自分はあまり使いこなせてなかったりします...)Chrome拡張
React Developer Tools
Developer ToolsにComponentsとProfilerタブが追加されます。
コンポーネントの階層や、propsやstateの中身を確認したりすることができデバッグがしやすくなります。
Chromeウェブストアからインストール
ChromeのReact Developer Toolsアイコンを右クリックして、「拡張機能を管理」へ
「ファイルの URL へのアクセスを許可する」をONにする
静的解析ツールであるESLintも導入した方がいいとは思うのですが、VSCode上で動かすのがうまくいかなかったので今回は書いていません。
(いずれちゃんと設定できたら書くかも...)参考
- 投稿日:2019-10-23T16:43:37+09:00
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を扱うためのライブラリが存在しますが、拙作の方には少しアドバンテージがあります:
- React Native側の問題により、
\u0000混じりの文字列が格納できない.
- PouchDBがインデックス構築時にドキュメントIDにNull文字を多用するので、この問題の解決は重要です。
- PouchDBで添付ファイルを格納する動作が不安定: #6037.
本ライブラリはこれらの問題を解決します。
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 --saveLink 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-2iOS
If using cocoapods in the
ios/directory run$ pod installAndroid
Please make sure AndroidX is enabled in your project by editting
android/gradle.propertiesand adding 2 lines:android.useAndroidX=true android.enableJetifier=trueUsage
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
- GitHub repos: https://github.com/craftzdog/react-native-sqlite-2
- Inkdrop - Markdown note-taking app: https://inkdrop.app/
- Twitter: https://twitter.com/craftzdog








