- 投稿日:2020-02-18T23:57:13+09:00
【React】動的に用意したイベントハンドラのindexを取得する
2020/2/20追記
indexを引数で渡せないわけないよなと思いながらこの記事書いてたけど,やっと渡し方わかった笑
.jshandleChange_todohuken = (e, index) => {) <Autocomplete onChange={e => this.handleChange_todohuken(e, input)} >終わり笑
困った状況
今回はFormを登録したい数だけ動的に用意することを考えた.reactでの実装なので,Fromで入力された値はstateに持たせたい.この時に考えないといけないことは,動的に用意したどこのstateが変更されたのかを判定するためにそのFormが何番目のものであるかのindexを取得したいこと.
解決法
コンポーネントのidにindex番号を入れておいてあとで取り出す.
.jshandleChange_concert = e => { const index = e.target.id.split(' ')[0].replace('index', ''); // stateの配列を更新するためコピー先の配列全体を変更する const concerts_copy = this.state.concerts.slice(); concerts_copy[index] = e.target.value; this.setState({ concerts: concerts_copy }); } {this.state.inputs.map(input => <div key={input}> <TextField label="ライブ名" id={`index${input} standard-basic`} onChange={this.handleChange_concert} value={this.state.concerts[input]} required /> </div> )}MaterialUIのAutocompleteの余談
ここでstate.todohukensの配列に都道府県名を格納することにかなり悩んで時間かかった.
最初はAutocompleteの中のTextFieldにonChangeを持たせていたが,当然のことながら格納されるのはoptionsから選んだ都道府県名ではなく,キーボード入力された値のみ.
AutocompleteにonChangeを持たせることでoptionsから値が選ばれた時にコールバック関数が動くので正常に値がとれる..js<Autocomplete id={`index${input} combo-box-demo`} options={options} getOptionLabel={option => option.label} onChange={this.handleChange_todohuken} style={{ width: 300 }} renderInput={params => ( <TextField {...params} label="都道府県" id="standard-basic" required fullWidth /> )} />
あとAutocompleteには選んだ都道府県名を空文字に戻せるxボタンがテキストフィールド右側に配置されてるが,今回上のindexのとりかたをする時にe.targetを使っているためクリックする要素が変わるとindexの取り方も変えないといけなかった.やり方が全然スマートでない...
この辺,reactでどうやってきれいに書いたらいいのかまだ分かりません,教えてください〜.jshandleChange_todohuken = e => { // 都道府県プルダウン操作した時 let index; if (e.target.id){ index = e.target.id.split(' ')[0].replace('index', ''); } // 右のxボタンで都道府県名を消した場合 else{ index = e.target.parentElement.parentElement.parentElement.parentElement.querySelector('input').id.split(' ')[0].replace('index', ''); } const str = e.target.innerHTML[0] == '<' ? '' : e.target.innerHTML; // stateの配列を更新するためコピー先の配列全体を変更する const todohukens_copy = this.state.todohukens.slice(); todohukens_copy[index] = str; this.setState({ todohukens: todohukens_copy }); }
- 投稿日:2020-02-18T22:37:10+09:00
React Nativeで必要なAndroid対応
React Nativeでアプリ開発をする時、ほとんどの方が「iOS向けにまずアプリを作り、その後Android対応をする」といった進め方をするのではないでしょうか。
今回は自分がiOS向けに開発した後、Android対応で何が必要だったのかをまとめます(随時追加更新)なお、自分のメインスタックは下記の通りです。他にも色々パッケージは使っています
* expo v36.x
* react-native
* styled-components
* react-navigationbox shadowの対応
有名なやつですね。
iOSでは
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.2);
のようにかけるシャドウですが、Androidだとこれは効きません。
代わりにelevationというAndroid独自のプロパティを使います。この場合、Androidかどうかの判定は不要です。box-shadow: 0 6px 12px rgba(0, 0, 0, 0.17); elevation: 1;Androidのツールバーにアプリのコンテンツが隠れてしまう
画像のようなやつです。
via https://stackoverflow.com/questions/46865628/react-native-android-toolbar-going-below-status-bar
これはアプリのルートコンテナに対してpaddingを追加してあげれば良いです。
expoの場合expo-constantsからstatusBarHeightというAndroidのみで有効なAPIが用意されているのでこちらを使います。import Constants from 'expo-constants' paddingTop: Constants.statusBarHeight || 0Lottie Animationの対応
自分のアプリでは、lottie-react-nativeを使っていい感じのアニメーションを入れていました。iOSでは問題なく動いたものの、androidでは同じものをそのまま使っているとアプリがクラッシュしてしまう問題がありました。
lottie-react-nativeをv3.0.4に上げると直るという話もありますが、expo installで入れると今の所~2.6.1で入ってしまうので対応できません。
2.6.1では、lottie 3のJSONフォーマットに対応していないようで、After Effect上でExport Old Json FormatでJSONをExportし直すことで直りました。https://github.com/expo/expo/issues/4835#issuecomment-508932818
MapView内のMarkerコンポーネント
MapView内のは、ピンを立てる時によく使いますが、AndroidだとCustomコンポーネントやimageコンポーネントを使っている場合に表示されない問題があります。
対応としてはややhackyですが、Pinの高さを持った空のTextコンポーネントを入れてあげると解決します。
To Be Added
- 投稿日:2020-02-18T22:35:22+09:00
React HooksでFirestoreからクールに取得するライブラリを作成した
はじめに
はじめまして、オプティマインドでインターンをしている成川です。
今回お仕事として、 Fireclient というライブラリを作成しました。
このライブラリは Firestore 上のデータを React の Hooks を用いて取得するものです。実用例
Firestore の
/cities/tokyo
に存在するドキュメントを React コンポーネントから取得し表示してみます。
Fireclient なら、このデータの取得の部分を1行で書くことができます。const [tokyo, loading, error] = useGetDoc("cities/tokyo");<> {loading ? ( <div>Loading...</div> ) : ( <> <div>tokyo.id : {tokyo.id}</div> <div>tokyo.data.name : {tokyo.data.name}</div> <div>tokyo.data.country : {tokyo.data.country}</div> <div>tokyo.data.population : {tokyo.data.population}</div> <div>error : {JSON.stringify(error)}</div> </> )} </>なぜ作成したのか
まず、オプティマインドではフロントエンドを主に React と Firebase を用いて開発しています。
その中で、React 上で Firestore にアクセスする際には Redux-Saga を使用していました。しかし、その Redux-Saga 関連のコードの肥大化が問題になっていました。
例えば、Firestore 上の Job という1つのデータを扱うために、
- START_WATCHING_JOBS
- END_WATCHING_JOBS
- UPDATE_JOBS
- CLEAR_JOBS
- UPDATE_JOB
- UPDATE_JOB_SUCCESS
- UPDATE_JOB_FAILURE
という7つの Action を用意し、それぞれに対応する ActionCreator, Reducer を用意していました。
この Job 以外にも扱うデータはいくつかあり、あるページでは Reducer の数が 40 個にまで及んでしまっていました。このままではメンテナンスが非常に大変になってしまいます。
ライブラリの役割
上記のような問題を解決するため、Fireclient を作成しました。
このライブラリは Redux-Saga の通信部分の役割を代替することができ、
- ページに必要なデータのクエリの宣言的な定義
- 肥大化したアプリケーションでも使用できる、可読性の維持
- View にロジック部分を持ち込まない、シンプルな設計
という考えのもと開発されました。
開発には React 16.8 から実装された Hooks を主に使用しており、コンセプトの部分は GraphQL を用いた Apollo-Client の影響を大きく受けています。
何ができるの?
コードのシンプルさ
React 上で Firestore のドキュメントを取得する際、
useState
を使ってデータを入れる場所を作っておいて、useEffect
内でデータが取得できたらセットする、という形になると思います。const [tokyo, setTokyo] = useState(null); useEffect(() => { db.doc("cities/tokyo") .get() .then(doc => setTokyo(doc.data())); }, []);しかし、この手法ではコンポーネント側に
db.doc(path).get().then()
のようなデータ取得のロジック部分が入り込んでしまっており,
もし取得対象がどんどん増えるとコードの可読性が落ちてしまう原因になってしまいます。これに対し、Fireclient では同じことを1行で実現することができます。
取得結果のtokyo
、読み込み状態を表すloading
、エラーが発生した場合のerror
を取ってくることができます。const [tokyo, loading, error] = useGetDoc("cities/tokyo");宣言的なクエリ定義
Fireclient はクエリ内容を宣言的に書けることを重視して作られています。
1つのページで Fireclient 上の複数箇所からデータを取得する必要がある場合、FQL (Fireclient Query Language) を用いてまとめて取得することができます。const fql = { queries: { post: { location: `/posts/${postId}` }, comments: { location: `/posts/${postId}`, limit: 15, order: { by: "likeCount", direction: "desc" } }, relatedPosts: { location: `/posts/${postId}/relatedPosts`, limit: 5 } } };const [response, loading, error] = useQuery(fql); const { post, comments, relatedPosts } = response;このようにすることで、取得内容と View の部分を完全に分離することができるようになったので、コードの可読性を上げることができます。
また、取得対象やorder
などのオプションが1箇所に集約するので、コンポーネントで Firestore から取得しているドキュメントやコレクションが一目瞭然になります。State の再利用性
Fireclient では無駄なデータの取得を極力減らすように設計されています。
例えば、あるコンポーネントでリッスン(サブスクライブ)しているデータを、別のコンポーネントで取得しようとしたとします。この場合、リッスンされているデータは最新のものであることが分かっているにも関わらず、再度同じデータをサーバーから取得するのは無駄であると言えます。
このような場合、Fireclient ではリッスンされているデータをそのまま、データを取得しようとした別のコンポーネントに渡すことでサーバーとの無駄な通信をなくしています。
note
厳密にはこの機能は Firebase.js 側にも存在しますが、リッスンされているコレクションに含まれるドキュメントの取得など、対応されていない部分があります。また、取得するデータの更新頻度が高くないと分かっている場合、
useGetDoc
やuseGetCollection
などではオプションで{ acceptOutdated: true }
を指定することで、保存されたキャッシュを利用してネットワークアクセスを減らすことができます。カスタム Hooks による様々なタスクへの適応
Fireclient に含まれる
useGetDoc
やuseGetCollection
などを組み合わせることで、複雑な処理を 1 つの Hooks として作成することができます。例えば、ページ切り替えを行いたい場合、カスタム Hooks のおかげで次のコードだけで実現でき、ロジックの部分を全てカスタム Hooks で簡潔させることができています。
population
でソートを行い、3 個ずつ表示させています。const options = { order: { by: "population" }, limit: 3 }; const [cities, loading, error, prevHandler, nextHandler] = usePaginateCollection( "/cities", options );return ( <> <button onClick={prevHandler.fn} disabled={!prevHandler.enabled}> Prev </button> <button onClick={nextHandler.fn} disabled={!nextHandler.enabled}> Next </button> {loading ? ( <div>Loading...</div> ) : ( cities.map(city => ( <div> name: {city.data.name}, country: {city.data.country}, population: {city.data.population} </div> )) )} </> );まとめ
以上 Fireclient の特徴を紹介させていただきました。
より詳しい情報はドキュメントに記載していますので、ご興味あればぜひ御覧ください。
- 投稿日:2020-02-18T22:35:22+09:00
React HooksでFirestoreからデータをクールに取得するライブラリを作成した
はじめに
はじめまして、オプティマインドでインターンをしている成川です。
今回お仕事として、 Fireclient というライブラリを作成しました。
このライブラリは Firestore 上のデータを React の Hooks を用いて取得するものです。実用例
Firestore の
/cities/tokyo
に存在するドキュメントを React コンポーネントから取得し表示してみます。
Fireclient なら、このデータの取得の部分を1行で書くことができます。const [tokyo, loading, error] = useGetDoc("cities/tokyo");<> {loading ? ( <div>Loading...</div> ) : ( <> <div>tokyo.id : {tokyo.id}</div> <div>tokyo.data.name : {tokyo.data.name}</div> <div>tokyo.data.country : {tokyo.data.country}</div> <div>tokyo.data.population : {tokyo.data.population}</div> <div>error : {JSON.stringify(error)}</div> </> )} </>なぜ作成したのか
まず、オプティマインドではフロントエンドを主に React と Firebase を用いて開発しています。
その中で、React 上で Firestore にアクセスする際には Redux-Saga を使用していました。しかし、その Redux-Saga 関連のコードの肥大化が問題になっていました。
例えば、Firestore 上の Job という1つのデータを扱うために、
- START_WATCHING_JOBS
- END_WATCHING_JOBS
- UPDATE_JOBS
- CLEAR_JOBS
- UPDATE_JOB
- UPDATE_JOB_SUCCESS
- UPDATE_JOB_FAILURE
という7つの Action を用意し、それぞれに対応する ActionCreator, Reducer を用意していました。
この Job 以外にも扱うデータはいくつかあり、あるページでは Reducer の数が 40 個にまで及んでしまっていました。このままではメンテナンスが非常に大変になってしまいます。
ライブラリの役割
上記のような問題を解決するため、Fireclient を作成しました。
このライブラリは Redux-Saga の通信部分の役割を代替することができ、
- ページに必要なデータのクエリの宣言的な定義
- スケーラブル、大規模化したアプリケーションにおいても可読性を維持
- View にロジック部分を持ち込まない、シンプルな設計
という考えのもと開発されました。
開発には React 16.8 から実装された Hooks を主に使用しており、コンセプトの部分は GraphQL を用いた Apollo-Client の影響を大きく受けています。
何ができるの?
コードのシンプルさ
React 上で Firestore のドキュメントを取得する際、
useState
を使ってデータを入れる場所を作っておいて、useEffect
内でデータが取得できたらセットする、という形になると思います。const [tokyo, setTokyo] = useState(null); useEffect(() => { db.doc("cities/tokyo") .get() .then(doc => setTokyo(doc.data())); }, []);しかし、この手法ではコンポーネント側に
db.doc(path).get().then()
のようなデータ取得のロジック部分が入り込んでしまっており,
もし取得対象がどんどん増えるとコードの可読性が落ちてしまう原因になってしまいます。これに対し、Fireclient では同じことを1行で実現することができます。
取得結果のtokyo
、読み込み状態を表すloading
、エラーが発生した場合のerror
を取ってくることができます。const [tokyo, loading, error] = useGetDoc("cities/tokyo");宣言的なクエリ定義
Fireclient はクエリ内容を宣言的に書けることを重視して作られています。
1つのページで Fireclient 上の複数箇所からデータを取得する必要がある場合、FQL (Fireclient Query Language) を用いてまとめて取得することができます。const fql = { queries: { post: { location: `/posts/${postId}` }, comments: { location: `/posts/${postId}/comments`, limit: 15, order: { by: "likeCount", direction: "desc" } }, relatedPosts: { location: `/posts/${postId}/relatedPosts`, limit: 5 } } };const [response, loading, error] = useQuery(fql); const { post, comments, relatedPosts } = response;このようにすることで、取得内容と View の部分を完全に分離することができるようになったので、コードの可読性を上げることができます。
また、取得対象やorder
などのオプションが1箇所に集約するので、コンポーネントで Firestore から取得しているドキュメントやコレクションが一目瞭然になります。State の再利用性
Fireclient では無駄なデータの取得を極力減らすように設計されています。
例えば、あるコンポーネントでリッスン(サブスクライブ)しているデータを、別のコンポーネントで取得しようとしたとします。この場合、リッスンされているデータは最新のものであることが分かっているにも関わらず、再度同じデータをサーバーから取得するのは無駄であると言えます。
このような場合、Fireclient ではリッスンされているデータをそのまま、データを取得しようとした別のコンポーネントに渡すことでサーバーとの無駄な通信をなくしています。
note
厳密にはこの機能は Firebase.js 側にも存在しますが、リッスンされているコレクションに含まれるドキュメントの取得など、対応されていない部分があります。また、取得するデータの更新頻度が高くないと分かっている場合、
useGetDoc
やuseGetCollection
などではオプションで{ acceptOutdated: true }
を指定することで、保存されたキャッシュを利用してネットワークアクセスを減らすことができます。カスタム Hooks による様々なタスクへの適応
Fireclient に含まれる
useGetDoc
やuseGetCollection
などを組み合わせることで、複雑な処理を 1 つの Hooks として作成することができます。例えば、ページ切り替えを行いたい場合、カスタム Hooks のおかげで次のコードだけで実現でき、ロジックの部分を全てカスタム Hooks で完結させることができています。
population
でソートを行い、3 個ずつ表示させています。const options = { order: { by: "population" }, limit: 3 }; const [cities, loading, error, prevHandler, nextHandler] = usePaginateCollection( "/cities", options );return ( <> <button onClick={prevHandler.fn} disabled={!prevHandler.enabled}> Prev </button> <button onClick={nextHandler.fn} disabled={!nextHandler.enabled}> Next </button> {loading ? ( <div>Loading...</div> ) : ( cities.map(city => ( <div> name: {city.data.name}, country: {city.data.country}, population: {city.data.population} </div> )) )} </> );まとめ
以上 Fireclient の特徴を紹介させていただきました。
より詳しい情報はドキュメントに記載していますので、ご興味あればぜひ御覧ください。
- 投稿日:2020-02-18T19:48:17+09:00
【React】【MaterialUI】InlineDatePickerが正常に動作する環境
InlineDatePickerとは
material-ui-pickersモジュールが提供するモダンな日程入力コンポーネント.
導入環境
npx create-react-appでスターターキット使いました.
.jsimport { MuiPickersUtilsProvider, InlineDatePicker } from "material-ui-pickers"; import DateFnsUtils from "@date-io/date-fns";MaterialUIからのモジュール読み込み.
.js<MuiPickersUtilsProvider utils={DateFnsUtils}> <InlineDatePicker label="ライブ日程" format="yyyy/MM/dd" onChange={date => this.handleDateChange(date+input)} value={this.state.selectedDates[input]} /> </MuiPickersUtilsProvider>InlineDatePickerの呼び出し.
躓いた点
これ,普通にモジュールインストールを
yarn add モジュールみたいにしてると最新版がインストールされてしまい,なぜか最新版@date-ioはエラーを吐きます.
解決法
package.json"@date-io/date-fns": "^1.3.13"でバージョン1.3.13で指定.これで上手くいきます.
参考:https://stackoverflow.com/questions/59600125/cannot-get-material-ui-datepicker-to-work疑問
一番上の画像でもあるようにDatePickerウィンドウ内の言語は英語になっています.これ日本語にすることって可能ですかね?だれか教えていただけると嬉しいです〜
- 投稿日:2020-02-18T17:47:39+09:00
Reactにおけるstateのイミュータビリティ
この記事の目的
公式チュートリアルで三目並べを作ってもらうのが一番理解できるが、その中でも特に日本語で説明しておいたほうがよさそうなポイントを纏めている。
本資料を読んだ後に、チュートリアルを両方やってもらったほうがより理解が深まる。関連するQiita記事
Reactのざっくり概要
Reactコンポーネントとは
Reactコンポーネント間の値の受け渡し
Reactコンポーネントでstateをリフトアップ参考資料
ドキュメント
チュートリアル
※余力があれば、以下のチュートリアルも行うことを推奨する
Getting Started with React - An Overview and Walkthrough TutorialReactにおけるstateのイミュータビリティ
イミュータビリティとは
変更可能なデータオブジェクトの値を変更するときは、以下の2つの方法がある。
- 対象のデータオブジェクトを直接変更する → mutable(変異)
- 対象のデータオブジェクトをコピーし、コピーしたデータオブジェクトを変更してから対象のデータオブジェクトをコピーオブジェクトで入れ替える(書き換えではなく入れ替え) → immutable (不変)
Reactにおけるイミュータビリティ
Reactのstateを変更する際は
setState()
という関数を使用するが、この関数はイミュータブルの手法で値を変更する必要がある。コードの例
以下のBoardコンポーネントを例に説明する。
board.jsclass Board extends React.Component { constructor(props) { super(props); this.state = { squares: Array(9).fill(null), }; } handleClick(i) { const squares = this.state.squares.slice(); //ポイント1 squares[i] = 'X'; //ポイント2 this.setState({squares: squares}); //ポイント3 } renderSquare(i) { return ( <Square value={this.state.squares[i]} onClick={() => this.handleClick(i)} /> ); } render() { const status = 'Next player: X'; return ( <div> <div className="status">{status}</div> <div className="board-row"> {this.renderSquare(0)} {this.renderSquare(1)} {this.renderSquare(2)} </div> <div className="board-row"> {this.renderSquare(3)} {this.renderSquare(4)} {this.renderSquare(5)} </div> <div className="board-row"> {this.renderSquare(6)} {this.renderSquare(7)} {this.renderSquare(8)} </div> </div> ); } }BoardコンポーネントのhandleClick関数(Squareコンポーネントでマスをクリックされたときに動作する関数)においてsetState関数を使用してstateの置き換えをしているが、その中でイミュータブルな処理が行われている。
注目すべきポイント
見るべきポイントは以下の3点。
state.squares
の値を変更したいが、まずはconst squares = this.state.squares.slice();
でstate.squares
のコピーを作成する- コピーした
squares
に'X'を格納するthis.setState({squares: squares});
で、state.squares
をコピーしたsquares
で置き換える → 書き換えではなく置き換えをしているオブジェクトをコピーする方法
JavaScriptでオブジェクトのコピーを作成する方法はいろいろあるが、「配列をコピーする例」と「オブジェクトのプロパティをコピーする例」を示す。
配列をコピーする例
ArrayCopy.jsconst newSquares = this.state.squares.slice(); newSquares[i] = 'X'; this.setState({squares: squares});オブジェクトのプロパティをコピーする例
ObjectAssign.jsvar player = {score: 1, name: 'Jeff'}; var newPlayer = Object.assign({}, player, {score: 2}); this.setState({player: newPlayer});なぜイミュータブルであることが重要なのか
なぜ、書き換えによるmutable(変異)な値の変更を行わず、オブジェクトの入れ替えによってimmutable (不変)な値の変更を行うのが重要なのか、理由を3つ説明する。
複雑な処理を簡略化できる
stateをimmutable (不変)の方針でオブジェクトによって置き換えると、値の変更は「オブジェクトの置き換えをしていない」か「オブジェクトの置き換えをしている」かの2択になる。
ある一部分だけ値を書き換えたような中途半端な状態にはならない。対して、stateをmutable(変異)の方針で直接書き換えると、ある一部分だけ書き換わった中途半端な状態が存在する可能性がある。
仮に、途中で書き換えの処理が失敗した場合、「一部分だけ書き換わった中途半端な状態」を元に戻す複雑な処理が必要となってしまう。また、immutableで「オブジェクトの置き換えをした結果」を履歴として保持すれば、過去の状態に戻す処理もmutableよりも簡単に実現することができる。
変更の検出が容易
mutableで値を直接書き換えた場合、stateが参照するオブジェクトに変化がないため、state内のどのオブジェクトに変化があったのかをReactは検知することができない。
そうなると、Reactは仮想DOM内の新旧差分を全走査することになり、再描画が遅くなる。対して、imutableでstateのオブジェクトを入れ替えた場合、stateが参照するオブジェクトに変化があるため、state内のどのオブジェクトに変化があったのかをReactは検知できる。
ReactはViewの再描画をする際に、仮想DOMの新旧比較を変化があったオブジェクトでのみ行うため、再描画が高速になる。再描画のタイミングを決めやすい
上記で書いたとおり、imutableなオブジェクトはstate内のどこで変更があったのかを検知できる。
それを利用して、再描画が必要ない変更であれば再描画せず、再描画が必要な変更であれば差描画するといったことが可能となる。
それによって、不要な再描画を押さえて描画パフォーマンスを向上させることができる。
- 投稿日:2020-02-18T15:57:41+09:00
Reactコンポーネントでstateをリフトアップ
この記事の目的
公式チュートリアルで三目並べを作ってもらうのが一番理解できるが、その中でも特に日本語で説明しておいたほうがよさそうなポイントを纏めている。
本資料を読んだ後に、チュートリアルを両方やってもらったほうがより理解が深まる。関連するQiita記事
Reactのざっくり概要
Reactコンポーネントとは
Reactコンポーネント間の値の受け渡し参考資料
ドキュメント
チュートリアル
※余力があれば、以下のチュートリアルも行うことを推奨する
Getting Started with React - An Overview and Walkthrough TutorialReactコンポーネントでstateをリフトアップ
stateを子コンポーネントに持たせる場合の問題点
Reactコンポーネント間の値の受け渡しでは、Squareコンポーネントのstateで値を保持していたが、このままだとBoardコンポーネントで9つあるSquareコンポーネントの値を取得しようとした場合に、9つのSquareコンポーネントにそれぞれ問い合わせをする必要がある。
そのようなコードもReactは許容するが、コードがわかりにくくなりバグを発生しやすく、リファクタリングも難しくなるので、Reactは「stateのリフトアップ」を推奨している。
stateのリフトアップとは?
各Squareコンポーネントで持っていたstateをBoardコンポーネントに移し、各Squareコンポーネントのpropsに対してstateの値を渡すようにする。
このように、SquareコンポーネントにあったstateをBoardコンポーネントに移すようなリファクタリングを、「stateのリフトアップ」と呼ぶ。
stateのリフトアップをするコード例
Squareコンポーネントのstateをリフトアップすると、以下のようなコードになる。
コードの例
board.jsclass Square extends React.Component { //ポイント2 render() { return ( <button className="square" onClick={() => this.props.onClick()} > {this.props.value} //ポイント4 </button> ); } } class Board extends React.Component { constructor(props) { super(props); this.state = { //ポイント1 squares: Array(9).fill(null), }; } handleClick(i) { const squares = this.state.squares.slice(); squares[i] = 'X'; this.setState({squares: squares}); } renderSquare(i) { return ( <Square value={this.state.squares[i]} //ポイント3 onClick={() => this.handleClick(i)} /> ); } render() { const status = 'Next player: X'; return ( <div> <div className="status">{status}</div> <div className="board-row"> {this.renderSquare(0)}{this.renderSquare(1)}{this.renderSquare(2)} </div> <div className="board-row"> {this.renderSquare(3)}{this.renderSquare(4)}{this.renderSquare(5)} </div> <div className="board-row"> {this.renderSquare(6)}{this.renderSquare(7)}{this.renderSquare(8)} </div> </div> ); } }注目すべきポイント
stateのリフトアップを見る上でのポイントは以下の4点。
- Boardコンポーネントにstateを持たせる
- Squareコンポーネントからstateを除去
- Boardコンポーネントのstateを、Squareコンポーネントのpropsに渡す
- Squareコンポーネントは、propsで受け取ったBoardコンポーネントのstateを表示する
子コンポーネントから親コンポーネントのstateを変更する例
Boardコンポーネントから見て、Squareコンポーネントは子コンポーネントになるので、BoardコンポーネントとSquareコンポーネントには親子関係があることになる。
Squareコンポーネント(子コンポーネント)からBoardコンポーネント(親コンポーネント)のstateを変更したい場合、子コンポーネントから親コンポーネントのstateを直接変更することはできない。
なぜなら、コンポーネントのstateはプライベートなフィールドなので外部から直接値を変更することはできないためである。
そのため、SquareコンポーネントからBoardコンポーネントのstateを変更する場合は、以下のように親コンポーネントから子コンポーネントにstateを変更する関数をpropsとして渡すことになる。
子コンポーネントでは、propsで受け取った親の関数を通して、親のstateを変更する。コードの例
board.jsclass Square extends React.Component { render() { return ( <button className="square" onClick={() => this.props.onClick()} //ポイント3 > {this.props.value} </button> ); } } class Board extends React.Component { constructor(props) { super(props); this.state = { squares: Array(9).fill(null), }; } handleClick(i) { //ポイント1 const squares = this.state.squares.slice(); squares[i] = 'X'; this.setState({squares: squares}); } renderSquare(i) { return ( <Square value={this.state.squares[i]} onClick={() => this.handleClick(i)} //ポイント2 /> ); } render() { const status = 'Next player: X'; return ( <div> <div className="status">{status}</div> <div className="board-row"> {this.renderSquare(0)}{this.renderSquare(1)}{this.renderSquare(2)} </div> <div className="board-row"> {this.renderSquare(3)}{this.renderSquare(4)}{this.renderSquare(5)} </div> <div className="board-row"> {this.renderSquare(6)}{this.renderSquare(7)}{this.renderSquare(8)} </div> </div> ); } }注目すべきポイント
子コンポーネントから親コンポーネントのstateを変更する上でのポイントは以下の3点。
- 親コンポーネント(Boardコンポーネント)にstateを変更する処理(今回の場合は
handleClick
関数)を持たせる- 子コンポーネント(Squareコンポーネント)にstateを変更する処理(今回の場合は
handleClick
関数)をpropsとして渡す- 子コンポーネント(Squareコンポーネント)から親コンポーネント(Boardコンポーネント)のstateを変更するときは、propsで渡された親コンポーネント(Boardコンポーネント)の処理(今回の場合は
handleClick
関数)を呼び出すstateのリフトアップを検討するときのポイント
以下の流れで実装とリファクタリングを区別し、リファクタリングでstateのリフトアップを行うのが好ましい。
実装
- 親子関係にあるコンポーネントであっても、子コンポーネントに”state”と”stateを変更する処理”がある状態で実装を進める
- 実装が完了し、テストで動作を確認する
リファクタリング
- 子コンポーネントにある”state”を、親コンポーネントにリフトアップする
- 子コンポーネントにある”stateを変更する処理”を、親コンポーネントにリフトアップする
- 子コンポーネントのpropsに、親コンポーネントの”state”と”stateを変更する処理”を渡す
- 子コンポーネントは、propsで渡された”state”と”stateを変更する処理”を使って、子コンポーネント側の変更を親コンポーネントのstateに格納する
- リファクタリングが完了したら、テストで動作を確認する
- 投稿日:2020-02-18T15:08:21+09:00
Reactコンポーネント間の値の受け渡し
この記事の目的
公式チュートリアルで三目並べを作ってもらうのが一番理解できるが、その中でも特に日本語で説明しておいたほうがよさそうなポイントを纏めている。
本資料を読んだ後に、チュートリアルを両方やってもらったほうがより理解が深まる。関連するQiita記事
参考資料
ドキュメント
チュートリアル
※余力があれば、以下のチュートリアルも行うことを推奨する
Getting Started with React - An Overview and Walkthrough TutorialReactコンポーネント間の値の受け渡し
Reactコンポーネント間で値の受け渡しをするには、propsとstateというものを使用する。
ReactコンポーネントとはのSquareコンポーネントとBoardコンポーネントにpropsとstateを加えて説明する。propsの追加
propsとは、コンポーネントのプロパティ(引数)のことである。コンポーネントに外から値を引き渡したい場合、propsを使用する。
コードの例
Squareコンポーネントにpropsを追加すると以下のようなコードになる。
board.jsclass Square extends React.Component { render() { return ( <button className="square"> {this.props.value} //ポイント2 </button> ); } } class Board extends React.Component { renderSquare(i) { return <Square value={i} />; //ポイント1 } render() { return ( <div> <div className="status">{status}</div> <div className="board-row"> {this.renderSquare(0)} {this.renderSquare(1)} {this.renderSquare(2)} </div> <div className="board-row"> {this.renderSquare(3)} {this.renderSquare(4)} {this.renderSquare(5)} </div> <div className="board-row"> {this.renderSquare(6)} {this.renderSquare(7)} {this.renderSquare(8)} </div> </div> ); } }注目すべきポイント
ポイントは以下の2点。
- BoardコンポーネントのrenderSquare関数でSquareコンポーネントを返す際に、Squareコンポーネントに
value={i}
を渡している- Squareコンポーネントは渡されたvalueを
this.props.value
で取り出しているこれによって、Squareコンポーネントを描画するときに
value={i}
で0〜9の値が渡されることになり、渡された値はSquareコンポーネントのタグに挟まれる形で{this.props.value}
の部分で表示されることになる。実行結果
実行結果としては以下のようになる。
(実際にブラウザで表示させたい場合はReact公式チュートリアルのCode Penで確認)stateの追加
stateとは、コンポーネント内で値を保持するための記憶領域を指す。コンポーネント内で値を保持したい場合にstateを使用する。
コードの例
Squareコンポーネントにstateを追加すると以下のようなコードになる。
board.jsclass Square extends React.Component { constructor(props) { super(props); this.state = { //ポイント1 value: null, }; } render() { return ( <button className="square" onClick={() => this.setState({value: 'X'})} //ポイント2 > {this.state.value} </button> ); } } class Board extends React.Component { renderSquare(i) { return <Square />; } render() { return ( <div> <div className="status">{status}</div> <div className="board-row"> {this.renderSquare(0)} {this.renderSquare(1)} {this.renderSquare(2)} </div> <div className="board-row"> {this.renderSquare(3)} {this.renderSquare(4)} {this.renderSquare(5)} </div> <div className="board-row"> {this.renderSquare(6)} {this.renderSquare(7)} {this.renderSquare(8)} </div> </div> ); } }注目すべきポイント
ポイントは以下の2点。
- Squareコンポーネントのコンストラクタで、
this.state
にvalue: null
を含むオブジェクトを格納している(コード上で明示されていないが、Squareコンポーネントはthis.state
というフィールドを持っている)- SquareコンポーネントのボタンがクリックされたときにonClickイベントが発生し、
this.setState({value: 'X'})
が実行される。これによってSquareコンポーネントのthis.state
に{value: 'X'}
が格納される実行結果
実行結果としては以下のようになる。
(実際にブラウザで表示させたい場合はReact公式チュートリアルのCode Penで確認)
Squareコンポーネントをクリックする前
Squareコンポーネントのstateには、
value: null
が格納されている。
Squareコンポーネントをクリックした後
- 投稿日:2020-02-18T14:44:29+09:00
Reactコンポーネントとは
この記事の目的
公式チュートリアルで三目並べを作ってもらうのが一番理解できるが、その中でも特に日本語で説明しておいたほうがよさそうなポイントを纏めている。
本資料を読んだ後に、チュートリアルを両方やってもらったほうがより理解が深まる。関連するQiita記事
参考資料
ドキュメント
チュートリアル
※余力があれば、以下のチュートリアルも行うことを推奨する
Getting Started with React - An Overview and Walkthrough TutorialReactコンポーネントとは
ReactはUIを「コンポーネント」という部品単位で構築する。
1つのコンポーネントを描画する例
これをReactコンポーネントで定義すると、以下のようになる。(スタイルは省略している)
square.jsclass Square extends React.Component { render() { return ( <button className="square"></button> ); } }SquareというReactコンポーネントを定義しており、render関数で描画内容をreturnしている。
このときというHTMLタグをreturnしているように見えるが、これはJSX記法であり、以下のようにJSXを使わずにReact.createElementで記述した場合と等価である。
square.jsclass Square extends React.Component { render() { return ( React.createElement( 'button', {className: 'square'} ); ); } }複数のコンポーネントを描画する例
上記のSquareを更に3×3のマス目でボタンを並べて描画する場合を考える。
これをReactコンポーネントで定義すると、以下のようになる。(スタイルは省略している)
board.jsclass Square extends React.Component { render() { return ( <button className="square"></button> ); } } class Board extends React.Component { renderSquare(i) { return <Square />; } render() { return ( <div> <div className="board-row"> {this.renderSquare(0)} {this.renderSquare(1)} {this.renderSquare(2)} </div> <div className="board-row"> {this.renderSquare(3)} {this.renderSquare(4)} {this.renderSquare(5)} </div> <div className="board-row"> {this.renderSquare(6)} {this.renderSquare(7)} {this.renderSquare(8)} </div> </div> ); } }新たに加わったBoardというReactコンポーネントは、Squareコンポーネントを9つ組み合わせて3×3のマス目を実現している。
- 投稿日:2020-02-18T00:43:08+09:00
料理のレシピをJSONで表現してみる試み
はじめに
ある日、ふと「料理のレシピを機械的に表現したい」という思いに駆られました。料理において、例えば「じゃがいもを切る」という工程は、入力として「じゃがいも」を渡すと、出力として「切られたじゃがいも」が出てくる関数のようなものなわけで、その関数群を一定のルールで表記できれば、レシピ全体を表現できそうです。
考えたことを記録として残してみようと思います。表記ルール
取り扱いやすさを考え、JSONで表現することにしました。AWSのCloudFormationやStep Functionsなどを参考に組み立てています。
概要
レシピは、大きく分けて以下の3つの要素から構成される、と考え、各要素を列挙することにより全体を表現します。
要素 説明 素材(material) 「玉ねぎ」「砂糖」「牛肉」など、料理の材料となるもの 容器(container) 「鍋」「どんぶり」「ボウル」など、素材を入れるもの 操作(action) 「切る」「煮る」など、素材の状態を変えるもの 全体像は以下のような感じです。
全体像{ "title": "名前", "description": "説明", "materials": { /* 素材を列挙 */ }, "containers": { /* 容器を列挙 */ }, "actions": { /* 操作を列挙 */ } }素材(material)
素材は、名前の通り料理の材料となるものです。記載方法の例は以下です。
素材(玉ねぎ){ … "materials": { "onion1": { "title": "玉ねぎ", "type": "onion", "description": "新鮮な玉ねぎ" "quantity": [ { "amount": 0.5, "unit": "pieces" }, { "amount": 100, "unit": "g" } ] }, … } }
要素 説明 title 素材の表示名。省略した場合はtypeにより自動で決まる。typeとしてcustomを指定した場合は必須。 type 素材の種別。onion(玉ねぎ)、sugar(砂糖)などを事前に定義しておく。任意の素材としてcustomを指定することも可。 description 素材の説明。省略可。 quantity 分量。「玉ねぎ半個(100g)」のように、1つの素材に複数の分量がある場合があるので配列にする。 amount 素材の量 unit 素材の量の単位。g(グラム)、pieces(個)、tbsp(大さじ)など 後述しますが、今回は「牛丼」を題材として考えたため、typeやunitは牛丼に必要なものしか定義していないです。レシピを増やしながら定義を拡充していきたいところです。
容器(container)
容器は、複数の素材をグループ化するときに利用します。例えば「煮る」という操作は複数の素材に対して同時に行われます。そのような場合は、容器を使って素材をひとまとめにし、その容器に対して「煮る」操作を適用する、と考えます。容器の記載例は以下です。
容器(鍋){ … "containers": { "pot1": { "title": "お鍋", "type": "pot", "description": "大き目の鍋。蓋も必要。" }, … } }
要素 説明 title 容器の表示名。省略した場合はtypeにより自動で決まる。typeとしてcustomを指定した場合は必須。 type 容器の種別。pot(鍋)、riceBowl(どんぶり)などを事前に定義しておく。任意の容器としてcustomを指定することも可。 description 容器の説明。省略可。 実際に容器を使って素材をグループ化するには、後述する「add」アクションを使います。
操作(action)
操作は、素材や容器に対して適用され、何等かの形で状態を変化させるものです。例は以下です。
操作(玉ねぎを切る->玉ねぎを鍋に入れる){ … "actions": { "cutOnion": { "type": "cut", "source": "onion1", "description": "繊維と平行に、幅1センチ程度になるようにスライスする。" }, "addOnionToPot": { "type": "add", "source": "onion1", "target": "pot1", "description": "玉ねぎを鍋に投入する。", "depend": "cutOnion" }, … } }
要素 説明 title 操作の表示名。省略した場合はtypeにより自動で決まる。typeとしてcustomを指定した場合は必須。 type 操作の種別。cut(切る)、add(加える)、stew(煮る)などを事前に定義しておく。任意の操作としてcustomを指定することも可。 source 操作の入力元となる素材、又は容器。typeによっては配列で複数指定も可。 target 操作の出力先となる素材、又は容器。 description 操作の説明。省略可。 depend 依存関係。その操作を行う前に終わらせておくべき操作を指定する。配列で複数指定も可。 until アクションに終了条件(…分経過するまで、柔らかくなるまで、など)がある場合は指定。 type 終了条件のタイプ。今のところtime(…分経過するまで)のみ value untilの値 上の例だと、玉ねぎを鍋に入れる前には玉ねぎを切っておく必要があるので、addOnionToPot(玉ねぎを鍋に入れる)のdepend属性としてcutOnion(玉ねぎを切る)を指定しています。実行順序ではなく依存関係で考えるのがポイントで、例えば「人参を切る」と「玉ねぎを切る」はどちらを先に実行してもいいので依存関係はありませんが、「玉ねぎの皮をむく」と「玉ねぎを切る」は、皮むきのほうが先のはずなので依存関係を持たせるべきです。
また、addOnionToPotは、容器を使って素材をグループ化している例です。addアクションを使って容器「pot1(鍋)」に「onion1(玉ねぎ)」を追加しています。素材をグループ化するためには実際に手を動かす(鍋に入れる、ボウルに入れる、など)必要があるので、その都度アクションを割り当てる、という発想になります。
全体像
ここまでを踏まえて作った牛丼のレシピが以下となります。
牛丼.json{ "title": "牛丼", "description": "薄く切った牛肉とタマネギなどを醤油などで甘辛く煮込み、丼に盛った飯の上に載せた料理", "containers": { "pot1": { "type": "pot" }, "riceBowl1": { "type": "riceBowl", "description": "一般的などんぶり" } }, "materials": { "rice1": { "type": "rice", "quantity": [ { "amount": 1, "unit": "go" } ] }, "water1": { "type": "water", "quantity": [ { "amount": 150, "unit": "ml" } ] }, "sugar1": { "type": "sugar", "quantity": [ { "amount": 1, "unit": "tbsp" } ] }, "soySauce1": { "type": "soySauce", "description": "一般的なこいくち醤油", "quantity": [ { "amount": 3, "unit": "tbsp" } ] }, "mirin1": { "type": "mirin", "quantity": [ { "amount": 3, "unit": "tbsp" } ] }, "ginger_tube1": { "type": "ginger_tube", "quantity": [ { "amount": 1, "unit": "cm" } ] }, "beefRib": { "title": "牛バラ肉", "type": "beef", "description": "薄く細切りの牛バラ肉", "quantity": [ { "amount": 200, "unit": "g" } ] }, "onion1": { "type": "onion", "quantity": [ { "amount": 0.5, "unit": "pieces" }, { "amount": 100, "unit": "g" } ] } }, "actions": { "cookRice": { "type": "cookRice", "source": "rice1", "description": "炊飯器で米を炊く。" }, "makeBroth": { "title": "煮汁を作る", "type": "add", "source": [ "water1", "sugar1", "soySauce1", "mirin1", "ginger_tube1" ], "target": "pot1", "description": "各調味料を計量し、鍋に入れる。" }, "cutBeefRib": { "type": "cut", "source": "beefRib", "description": "長辺5センチ程度になるようにカットする。大きさを揃えておくと、味の染み込み具合や食感を均一にできる。" }, "peelOnion": { "type": "peel", "source": "onion1", "description": "2分割して固いところを切り落とした後、素手で皮をむく。" }, "cutOnion": { "type": "cut", "source": "onion1", "description": "繊維と平行に、幅1センチ程度になるようにスライスする。", "depend": "peelOnion" }, "boil": { "type": "bringToABoil", "source": "pot1", "description": "調味料が入った鍋を強火で沸騰させる。", "depend": "makeBroth" }, "addBeefRibToPot": { "type": "add", "source": "beefRib", "target": "pot1", "description": "中火に変更し、牛肉を加えて解きほぐす。", "depend": [ "cutBeefRib", "boil" ] }, "stew1": { "type": "stew", "source": "pot1", "until": { "type": "time", "value": 5 }, "description": "中火のまま5分間煮込む。灰汁が気になる場合は取り除いておく。", "depend": "addBeefRibToPot" }, "addOnionToPot": { "type": "add", "source": "onion1", "target": "pot1", "description": "玉ねぎを鍋に投入する。", "depend": [ "cutOnion", "stew1" ] }, "serve": { "type": "serve", "source": [ "rice1", "pot1" ], "target": "riceBowl1", "description": "どんぶりにご飯と具材を盛り付ける。", "depend": [ "cookRice" ] } } }視覚化
JSONでの表現ができたところで、せっかくなのでビューアを作って視覚化してみました。
素材や容器で指定されたtypeに応じて、わかりやすいように画像を表示させています。依存関係をきちんと定義すれば、ワークフローのようにレシピを表現できるはずです。さいごに
まだ牛丼のことしか考えていないので、表現力が足りていないところがあるかもしれません。他にもレシピをJSON化しながら足りないところを足していきたいです。
また、レシピをこのように構造的に表現できれば、クリティカルパスの計算や、それに基づいた作業順序の推奨、進捗の管理などなど、できることの想像が広がります。色々試してみようと思います。参考文献
レシピ部分で以下を参考にさせて頂きました。
・チューブ生姜適量ではなくて1cmがいい人の理系の料理