- 投稿日:2020-03-29T22:37:17+09:00
React/Redux/Firebaseを使ったポートフォリオ
はじめに
2019年12月末にそれまで興味を持っていたプログラミングの学習を開始し、そのうちに楽しくなってきたので2月頃からエンジニア転職に向けて本格的な学習を開始した。
教材としてオンライン教材のFrontHacksを使用し、React/Reduxを学習。
一通りの学習が完了したので、可能な限り学習した内容を盛り込んだアプリを作成した。
アプリの一連の内容の振り返りの意味も込めて、簡単にアプリの内容と実装した機能の紹介をさせていただきます。
↓
https://daily-timemanegement.firebaseapp.com
(E-mail:1234@gmail.com , Password:12345678)Github
https://github.com/Ken-Takahashi-go/Invest-Expense-Counterまだ初学者ですので、言葉足らずな部分や理解が間違っている部分などあるかと思いますがご容赦ください。
何かありましたらご連絡ください。1、技術要素
・React
・React-router
・React-redux
・redux-thunk
・Firebase (Hosting,Authentication,Database)
・Material-UI教材では、React,React-Redux,Redux-thunkを学習したが、データベースとの連携とログイン機能をつけてみたいと思ったのでFirebaseを使用してみることにした。
2、アプリ概要
その日その日にやったことをそれぞれカテゴライズしてカウントしていくというシンプルなアプリ
(ログイン機能・データベース連携あり)
<カテゴリ>
①自己投資した時間
②浪費した時間
③単純な癒しの時間3、React/Reduxの大まかな流れ
React/Reduxは基本的に下図の流れでデータの受け渡しが進む。
①Reactで作ったComponentでデータの入力 →②ActionCreatorによって入力内容に応じたActionが発動→③Reducerが受け取ったaction Typeに応じてstoreのstateの値が更新される→④更新されたstateの値がComponentに表示される
※厳密にはこんな単純な説明ではないと思いますが、ざっくりこのように理解しています。
この際データベース(今回の場合はFirebase)とのやり取りは、ActionCreatorで行うようにする4、アプリの構成
ログイン画面、サインアップ画面、メイン画面(データ入力・表示)を作成
React-routerにより画面に表示されるComponentを切り替えて遷移App.jsfunction App() { return ( <BrowserRouter> <Box> <Container maxWidth="sm"> <div className="App"> <NavBar /> <Switch> <Route exact path="/" component={Login} /> <Route path="/main" component={Main} /> <Route path="/login" exact component={Login} /> <Route path="/signup" exact component={SignUp} /> </Switch> </div> </Container> </Box> </BrowserRouter> ); }5、メイン画面の構成
①Counter ComponentでItemListにリストアップされた時間を表示
②Form Componentで、やったこと、カテゴリー、掛かった時間を入力
③ItemList Componentで入力した内容をリスト表示
※ItemLIst Component内にFilter Componentを埋め込み、カテゴリによってリストの表示を絞り込む機能を追加Main.jsconst Main = () => { return ( <Box> <Counter /> <Form /> <ItemList /> </Box> ); };6、各Componentの構成 (1)Form Component
①:React Hooks を使ってstate(状態)の値を維持・更新できるようにする
useStateのカッコ内は初期値となるForm.jsxconst Form = props => { const [text, setText] = useState(""); const [hour, setHour] = useState(0); const [status, setStatus] = useState("投資");②:後に出てくる入力ボタンを押した際に発動する関数
下で入力するstatus,text,hourの値をaddItem関数を使いprops経由で受け渡し、
setXXXで画面上の値をリセットするForm.jsxconst onClickButton = () => { if ((text, hour, status)) { props.addItem(status, text, hour); setText(""); setHour(0); setStatus("投資"); } };③:入力フォーム
ここで入力・選択したstatus,text,hourの値が、ボタンを押した際にデータとして受け渡されるForm.jsxreturn ( <Box color="text.primary"> <Container> <h3>今日の積み上げ</h3> <div className="text-field"> <Input variant="outlined" className="input-text" type="text" value={text} onChange={e => { setText(e.target.value); }} placeholder="please input your activity" /> <select name="chooseStatus" className="radio-select" value={status} onChange={e => { setStatus(e.target.value); }} > <option value="投資">投資</option> <option value="浪費">浪費</option> <option value="癒し">癒し</option> </select> <input variant="outlined" label="Hour" className="input-hour" type="number" value={hour} onChange={e => { setHour(e.target.value); }} placeholder="please input hour" /> <p className="hour">Hour</p> <Button variant="contained" color="primary" onClick={onClickButton} className="button" > Go </Button> </div> </Container> </Box> ); }; const mapStateToProps = state => { return { auth: state.firebase.auth }; }; const mapDispatchToProps = dispatch => { return { addItem: (status, text, hour) => { const action = addItem(status, text, hour); dispatch(action); } }; }; export default connect(mapStateToProps, mapDispatchToProps)(Form);7、各Componentの構成 (2)ItemList Component
①:条件によってClassNameを変える *classnamesライブラリを使用
入力したstatus(投資、浪費、癒し)によってCSSで色を変える方法を探していたところ、classnamesライブラリというものを発見したので、status応じてclassNameを切り替える設定をしたItemList.jsxconst ItemList = props => { const itemLists = props.items.map((item, index) => { const classNameForListItem = ClassNames( { invested: item.status === "投資" }, { expensed: item.status === "浪費" }, { rested: item.status === "癒し" } );②:入力した値がリスト表示されるようリストの表示内容を設定
リストを削除するためのボタンとdeleteItem関数を設定
また、mapメソッド(上から続いている)によりprops経由で渡ってきた値をリストの項目として設定
あと、リスト項目を作成する場合、固有のKeyを設定しリスト毎を個別管理できるようにする必要
があるため、ここでは Key=item.idを設定(今回のアプリではFirebaseのドキュメント IDとリンクしている)ItemList.jsxreturn ( <Container key={index} maxWidth="sm"> <li key={item.id} className={classNameForListItem}> <span className="item-status">{item.status}</span> <span className="item-text">{item.text}</span> <span className="item-hour">{item.hour} Hour</span> <button className="item-button" onClick={() => props.deleteItem(item.id)} > X </button> </li> </Container> ); });③:itemListsに格納されたli要素をulタグ内で表示、Filter Componentによりfilter機能を盛り込む
ItemList.jsxreturn ( <Container maxWidth="sm"> <div className="item-box"> <h4>積み上げ履歴</h4> <Filter /> <ul className="itemContainer">{itemLists}</ul> </div> </Container> ); };④:Filter Component
"全て"、"投資"、"浪費"、"癒し"のボタンを設定
ボタンを押すと、それぞれshowAll,showInvest,showExpense,showHealing関数が実行されるFilter.jsxconst Filter = props => { return ( <Box color="text.primary"> <Container> <div className="container Filter-container"> <Button variant="outlined" className="showAll" onClick={props.showAll} > 全て </Button> ~以下繰り返しのため略~ </div> </Container> </Box> ); };8、各Componentの構成 (3)Counter Component
投資、浪費の合計値が表示されるようJSのfilterメソッド、reduceメソッドを使用し計算
Counter.jsxconst Counter = props => { const investLists = props.items .filter(item => item.status === "投資") .map(item => { return Number(item.hour); }); const expenseLists = props.items .filter(item => item.status === "浪費") .map(item => { return Number(item.hour); }); const invest = investLists.reduce((acc, amount) => acc + amount, 0); const expense = expenseLists.reduce((acc, amount) => acc + amount, 0); return ( <Box> <Container maxWidth="sm"> <h2>積み上げカウンター</h2> <div id="displayInevstExpense"> <div id="invest-field"> <h4> 投資 : {invest} <span> Hour</span> </h4> </div> <div id="expense-field"> <h4> 浪費 : {expense} <span> Hour</span> </h4> </div> </div> </Container> </Box> ); };9、Action Creatorの構成
①:itemActionCreator
Form Componentで入力したデータの処理と、ItemList Componentの削除処理をここで記述する
ここでFirebaseとの連携を行いデータベース機能であるFirestoreを使用
具体的なFirebaseの使い方については、FrontHacks講師であるつよぽんさんの動画(Firebase入門)で
学習(FrontHacksとは別教材)
Firebaseの具体的な連携方法はここでは解説しません。ぜひ動画をご参照いただければと思います。itemActionCreator.jsexport const addItem = (status, text, hour) => { return async dispatch => { try { const db = await firebase.firestore(); db.collection("activities").add({ status, text, hour }); dispatch({ type: ADD_ITEM, status, text, hour }); } catch (err) { dispatch({ type: ADD_ITEM_ERROR, err }); } }; }; export const deleteItem = id => { return async dispatch => { try { const db = await firebase.firestore(); db.collection("activities") .doc(id) .delete(); dispatch({ type: DELETE_ITEM, id }); } catch (error) { dispatch({ type: DELETE_ITEM_ERROR, error }); alert("delete,NG!!!"); } }; };②:visibleFilterCreator
Filter Componentのボタンに対応したアクションを設定
Firebaseとの連携をリアルタイムに行うためにonSnapshotメソッドを使う
詳細は上述の動画もしくは公式ドキュメントを参照ください
Cloud Firestore でリアルタイム アップデートを入手する
showAll,showInvest,showExpense,showHealingでほぼ同じ処理なので省略しますvisibleFilterCreator.jsexport const showAll = payload => { return async dispatch => { try { const db = await firebase.firestore(); await db.collection("activities").onSnapshot(querySnapshot => { const refAll = querySnapshot.docs.map(doc => { return { ...doc.data(), id: doc.id }; }); dispatch({ type: SHOW_ALL, payload: refAll }); }); } catch (error) { dispatch({ type: "SHOW_ALL_ERROR", error }); alert("NG"); } }; }; ~以下略~③:authActionCreator
ユーザー登録とログインに使う処理を記載
Firebaseの機能のうちAuthenticationを使用して実装
今回はメールアドレスを使ったログイン方法です
具体的な実装方法は、Reducerの処理含め下記の記事を参考にさせてもらいました参考: React + Redux + Firebase を使ってログイン機能あり掲示板アプリ開発
authActionCreator.jsexport const signIn = (email, password) => { return async (dispatch, getState, { getFirebase }) => { try { const firebase = await getFirebase(); firebase.auth().signInWithEmailAndPassword(email, password); dispatch({ type: "LOGIN_SUCCESS" }, email, password); } catch (err) { dispatch({ type: "LOGIN_ERROR" }, err); } }; }; export const signOut = () => { return async (dispatch, getState, { getFirebase }) => { try { const firebase = await getFirebase(); await firebase.auth().signOut(); dispatch({ type: "SIGNOUT_SUCCESS" }); } catch (err) { dispatch({ type: "SIGNOUT_ERROR" }, err); } }; }; export const signUp = (email, password, firstName, lastName) => { return async (dispatch, getState, { getFirebase, getFirestore }) => { try { const firebase = await getFirebase(); firebase.auth().createUserWithEmailAndPassword(email, password); dispatch({ type: "SIGNUP_SUCCESS" }); } catch (err) { dispatch({ type: "SIGNUP_ERROR", err }); } }; };10、Reducerの構成
①itemReducer
itemActionCreatorでdispatchされたアクションのTypeに基づいてstateの値を更新する処理を行うitemReducer.jsexport const itemReducer = (state = [], action) => { switch (action.type) { case ADD_ITEM: const item = new Item(action.id, action.status, action.text, action.hour); return [...state, item]; case ADD_ITEM_ERROR: return state; case DELETE_ITEM: return state.filter((item, id) => { return action.id !== item.id; }); case DELETE_ITEM_ERROR: return state; default: return state; } };②visibleFilterReducer
visibleFilterCreatorでdispatchされたアクションのTypeに基づいてstateの値を更新する処理を行う
詳細コードは省略(もし興味があればGithubをご覧ください)③authReducer
authActionCreatorでdispatchされたアクションのTypeに基づいてstateの値を更新する処理を行う
詳細コードは省略(もし興味があればGithubをご覧ください)④rootReducer
3つのreducerがあるので、combineReducersを使って一本化するrootReducerimport { itemReducer } from "./itemReducer"; import { authReducer } from "./authReducer"; import { visibleFilterReducer } from "./visibleFilterReducer"; import { combineReducers } from "redux"; const rootReducer = combineReducers({ itemInfo: itemReducer, visibleFilter: visibleFilterReducer, auth: authReducer }); export default rootReducer;11、storeの構成
reduxのcreateStoreメソッドでstoreを作る
また、Firebaseとの連携で非同期処理が必要となるため、非同期処理に必要なredux-thunkとapplyMiddlewareを使用し、10で一本化したrootReducerと合わせて設定するstore/index.jsimport { createStore, applyMiddleware, compose } from "redux"; import rootReducer from "../reducers/rootReducer"; import thunk from "redux-thunk"; import { reduxFirestore, getFirestore } from "redux-firestore"; import { reactReduxFirebase, getFirebase } from "react-redux-firebase"; import fbConfig from "./../Config/fbConfig"; const store = createStore( rootReducer, compose( applyMiddleware(thunk.withExtraArgument({ getFirebase, getFirestore })), reactReduxFirebase(fbConfig, { attachAuthIsReady: true }), window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__() ) ); export default store;12、苦労したこと、身についたこと
・文法的なエラーから、タイポ的なエラー、importミスからくるエラーなど、あらゆるエラー
により時間が奪われ、想定以上に時間がかかった
→エラー対応の中で、まずはエラー文をシンプルに受け止めることが大事だとわかった。
また、英語文献(stack overflow)やQiitaの記事、Githubのコードを参考にすることで、
エラーの対応方法やそもそもの言語に対する理解も深まった
苦労してエラーを解決して、自分の思った通りに動いた時の快感を覚えたことで、
あまりめげなくなった
Console.logと少し友達になれたおかげで、データの流れが分かるようになった
・React/Reduxだけで実装した際には、それぞれの機能の役割の理解が不十分でアプリ作成が
進まなかった
→教材を何度も見返したり、ノートにデータフローを書き出して、それぞれのつながりを何度も
確認することで理解を深めることができた。
・データベース(Firebase)と連携をさせた際に、非同期処理の理解が不十分でデータのやり取りが
スムーズに出来ない、画面の表示がリアルタイムに更新されない、データベースから特定の
データを削除するための固有のidの取得方法が分からない、というところで苦労した
→教材を見返す、公式ドキュメントを読む、Youtubeの海外動画を見まくる、
などで関連した情報を集約して実装したら、何とか動くコードが書けるようになった
とにかくまずは動くコードを書くという姿勢が身に付いた
・Filter機能(特定条件で絞り込む)を実装できるようになれば今後応用が利くと思い、
実装にこだわった。
→データベースからうまく配列を作り直して、JSのfilterメソッドで絞り込むという手法が
身に付いた
・正直、アプリとしては大したものではないが、学習した内容を総復習するという意味
で作ってよかったし、自分で何かアプリやサービスを作れる事が単純に楽しいと思った
13、今後取り組んでいきたいこと
バックエンドに取り組みたい
・Webフレームワークを1つ覚える(現在Expressを学習中)
・RDBを身につける(今回のFirebaseのようなNoSQL以外も覚える)
・WebフレームワークとRDBを連携したAPIサーバーを実装する
・APIサーバーとフロントを連携したWebアプリを作る
テストの方法を覚える
・テストコードを理解することで、より効率的な開発を行えるようになる
- 投稿日:2020-03-29T21:46:12+09:00
React Tutorial 中
node.jsがインストールされたら自動的にnpmも一緒にセットされていることを確認する
それと、npxのセットアップが必要?
→javascript package management module(node Package Module)
npm@5.2.0verから新しく追加されたツールらしい。
->npmの場合はresistryからhostingされた以下のものが使いやすかった一方、
npxの場合はrestiryからhositngされたCLI?? および他実行ファイルが使いやすいらしい。
何か単純化されたらいいけど。。わからん!※ローカルでセットされたツールをnpm run scriptsなしで使用する場合
※臨時使いもの、あまり使わないコマンドをする時
→重いpackageがインストールされたまま残ってしまうとか。
※ほかのnode.jsと一緒に交換できること
npmはできなかったっけ?
※gistを基づいたscriptを共有する時https://blog.npmjs.org/post/162869356040/introducing-npx-an-npm-package-runner
韓国語:https://geonlee.tistory.com/32React自体がfacebookが作ったもの
aribnbはもうreactを利用しない(2019~
https://softwareengineeringdaily.com/2018/09/24/show-summary-react-native-at-airbnb/
React-> spotify/ Netflixyarn... First Look at the New Package Manager for Javasciprt?
Happy Hacking!
- 投稿日:2020-03-29T21:18:13+09:00
【23日目】React(Reactとは,コンポーネント,props)
はじめに
こんばんは。
目標が決まったので再スタート。
目標って大事ですね。学び
Reactとは
サイトの見た目を作るJsのライブラリ
JSXとHTMLの違い
return内に複数の要素があるとエラーになる。
imgタグは最後に/が必要。
classはclassNameとする。class App extends React.Component { render() { return ( <div> {/*divで一つにまとめて複数の要素を入れる。*/} <h1>Hello World</h1> <p>一緒にReactを学びましょう!</p> <img src="https://s3-ap-northeast"/> {/*最後の/を忘れないように*/} </div> ); } } export default App;APP.jsの構成
renderメソッドの、returnの外にはJavaScriptを記述できる。
import React from 'react'; {/*Reactのインポート*/} class App extends React.Component{ {/*React.Componentを継承するクラスの定義*/} render(){ {/*JSXを戻り値とするrenderメソッドの定義*/} const name ='にんじゃわんこ' //js return ( <h1>Hello React</h1> {/*JSXの部分。この部分がブラウザに表示される。*/} <h1>{name}</h1> {/*JSXにjsを埋め込むときには{}を使う*/} ); } } export default App; {/*クラスをエクスポート*/}クリック動作
イベント
class App extends React.Component { render() { return ( <div> <h1>こんにちは、にんじゃわんこさん!</h1> <button onClick={() => {console.log('ひつじ仙人')}}>ひつじ仙人</button> {*/クリックされた時 ={アロー関数}/*} </div> ); } }state
ユーザーに合わせて動きが変わる
下記順で設定
- 定義
- 表示
- 変更
- Reactでは、下図のようにstateの値に直接代入することで値を変更してはいけない
class App extends React.Component { constructor(props) { super(props); this.state = {name: 'にんじゃわんこ'}; //jsの定義 this.stateに代入 } handleClick(name){ this.setState({name:name}); } render() { return ( <div> <h1>こんにちは、{this.state.name}さん!</h1> {*/定義したものを表示/*} <button onClick={() => {this.setState({name: 'ひつじ仙人'})}}>ひつじ仙人</button> {*/指定されたプロパティに対応するstateの値が変更される。this.state.nameで表示できる値も変更される/*} <button onClick={() => {this.setState({name: 'にんじゃわんこ'})}}>にんじゃわんこ</button> </div> ); } }メソッド化
今回はクリックで表示名を変えるため
class App extends React.Component { constructor(props) { super(props); this.state = {name: 'にんじゃわんこ'}; } handleClick(name) { //2メソッドnameの値を受取り this.setState({name: name}); //3nameプロパティの変更 } render() { return ( <div> <h1>こんにちは、{this.state.name}さん!</h1> <button onClick={() => {this.handleClick('ひつじ仙人')}}>ひつじ仙人</button> {*/1メソッドに/*} <button onClick={() => {this.handleClick('にんじゃわんこ')}}>にんじゃわんこ</button> </div> ); }Reactの表示の仕組み
上から順に
- App.js
- index.js (変換)
- index.html (表示)
ファイルの構成は
- React
- index.html
- src
- index.js
- component
- App.js
//index.js ReactDOM.render(<App />, document.getElementById('root')); //Appのrenderが入る<div id='root'></div> <!---指定したidの所に入る(root)--->コンポーネント
部分的な構成、部品のこと
コンポーネントを組み合わせて見た目を作る。
- React
- index.html
- src
- index.js
- component
- App.js
- Language.js (ここに追加)
//Language.js import React from 'react'; class Language extends React.Component { //Languageのコンポーネントを作る render() { return ( <div className='language-item'> <div className='language-name'>HTML & CSS</div> <img className='language-image' src='https://s3-ap-northeast-1.amazonaws.com/progate/shared/images/lesson/react/html.svg' /> </div> ); } } export default Language; //コンポーネントをエクスポート//App.js import React from 'react'; import Language from './Language'; //Languageをインポート class App extends React.Component { render() { return ( <div> <h1>言語一覧</h1> <div className="language"> <Language /> {/* Language登場 */} <Language /> {/* 何度でも呼び出せる */} <Language /> </div> </div> ); } } export default App;props
App.jsから、各言語の名前と画像のデータをLanguageコンポーネントに渡すことによって、言語ごとに表示を変えることができる。App.jsから渡すこのデータのこと。
//Language.js import React from 'react'; class Language extends React.Component { render() { return ( <div className='language-item'> <div className='language-name'> {this.props.name} {*/受け取るprops/*} </div> <img className='language-image' src={this.props.image} {*/受け取るprops/*} /> </div> ); } } export default Language;//App.js class App extends React.Component { render() { return ( <div> <h1>言語一覧</h1> <div className='language'> <Language name='HTML & CSS' {*/渡すprops/*} image='https://a' /> </div> </div> ); } }map
まとめて書くと
//App.js class App extends React.Component { render() { const languageList = [ { name: 'HTML & CSS', image: 'https://s3-ap-northeast-1.amazonaws.com/progate/shared/images/lesson/react/html.svg' }, { name: 'JavaScript', image: 'https://s3-ap-northeast-1.amazonaws.com/progate/shared/images/lesson/react/es6.svg' } ]; return ( <div> <h1>言語一覧</h1> <div className='language'> {languageList.map((languageItem) => { //上で設定した変数 return ( <Language name={languageItem.name} image={languageItem.image} /> ) })} </div> </div> ); } }//Language.js class Language extends React.Component { render() { return ( <div className='language-item'> <div className='language-name'>{this.props.name}</div> <img className='language-image' src={this.props.image} /> </div> ); } }所感
Railsを勉強した恩恵なのか、ファイルの構成とデータの流れの重要性は分かりました。
疑問なのはデータの流れは今回の場合だとLanguage.js→App.jsなのに、
propsになると逆になるのはなぜなのか・・・今はそういうものと覚えておくべきですかね。
- 投稿日:2020-03-29T19:49:52+09:00
Concurrent Mode時代のReact設計論 (3) SuspenseやuseTransitionが何を解決するか
この記事は「Concurrent Mode時代のReact設計論」シリーズの3番目の記事です。
シリーズ一覧
- Concurrent Mode時代のReact設計論 (1) Concurrent Modeにおける非同期処理
- Concurrent Mode時代のReact設計論 (2) useTransitionを活用する
- Concurrent Mode時代のReact設計論 (3) SuspenseやuseTransitionが何を解決するか
- Concurrent Mode時代のReact設計論 (4) render-as-you-fetchのためのコンポーネント設計(仮)
- Concurrent Mode時代のReact設計論 (5) トランジションを軸に設計する(仮)
- Concurrent Mode時代のReact設計論 (6) ステート管理ライブラリの展望(仮)
- Concurrent Mode時代のReact設計論 (7) まとめ(仮)
Suspense
やuseTransition
が何を解決するか前回までは、Promiseを
throw
してSuspense
がキャッチするというConcurrent Modeの特徴、そして「非同期処理そのもの(Promise)をステートで管理する」という設計指針において欠かせない部品であるuseTransition
について見てきました。
useTransition
は「2つのステートを同時に扱う」という斬新な概念を導入しました。そうまでしてConcurrent Modeが「Promiseをステートで管理する」という設計を貫く理由はおもに3つあると考えられます。まず非同期処理にまつわるロジックを分割するため、そして非同期処理をより宣言的に扱うためです。最後に、これは公式ドキュメントでも強調されていることですが、render-as-you-fetchパターンの実現です。ここからは、この3つを達成するためにどのような設計が必要かについて議論します。前回出てきた「画面Aから画面Bに遷移するためにデータを読み込んでいる間は、画面Aに留まって読み込み中の表示にしたい」というシチュエーションについて再考してみます。従来(Concurrent Modeより前)の考え方では、画面Bへの遷移は2つの段階に分割できます。すなわち、「画面B用のデータをロード中の段階」と「ロードが終わって画面Bをレンダリングする段階」です。
この指針に基づいて作った従来型の実装をまず考えてみます。
非同期処理を含む画面遷移の従来型実装
画面Aと画面Bという2つの画面が存在しますから、今どちらの画面かといったステートを司る存在が必須です。とりあえずこれを
Root
と呼びましょう。画面Bは前回から例に出てきているUser[]
型のデータを表示するとすると、Root
はこんな感じで定義できます。type AppState = | { page: "A"; } | { page: "B"; users: User[]; }; export const Root: FunctionComponent = () => { const [state, setState] = useState<AppState>({ page: "A" }); const goToPageB = () => { fetchUsers().then(users => { setState({ page: "B", users }); }); }; if (state.page === "A") { return <PageA goToPageB={goToPageB} />; } else { return <PageB users={state.users} />; } };
Root
コンポーネントの最後に注目すると、今画面AにいるときはPageA
をレンダリングし、画面BにいるときはPageB
をレンダリングするようになっています。画面Aは画面Bに行くボタンを持っている想定なのでgoToPageB
という関数をpropsで受け取ります。一方の画面BはUser[]
を表示するのでUser[]
をpropsで受け取ります。goToPageB
が呼ばれた場合、fetchUsers()
が完了するまでは現在の画面にとどまり、完了し次第setState
により画面Bを表示という実装です。
PageA
の実装はこんな感じになりますね。const PageA: FunctionComponent<{ goToPageB: () => void; }> = ({ goToPageB }) => { const [isLoading, setIsLoading] = useState(false); return ( <p> <button disabled={isLoading} onClick={() => { setIsLoading(true); goToPageB(); }} > {isLoading ? "Loading..." : "Go to PageB"} </button> </p> ); };画面Aは「画面B用のデータを読み込み中はローディング中の表示にする」というロジックのために
isLoading
ステートを持っています。それ以外は特筆すべき点はありませんね。このステートをPageA
の内部に持つか、それとも前述のAppState
の一部にするかは一考の余地がありますが、どちらも一長一短です。この設計では、「画面Bのデータをロード中の段階」は、
PageA
のisLoading
ステートがtrue
になり、Root
がfetchUsers()
の結果を待っている段階として現れます。そして、「ロードが終わって画面Bをレンダリングする段階」はRoot
のsetState
でステートを変更して画面Bをレンダリングする部分に対応しています。従来型設計の欠点と限界
この設計(従来型設計)で注目すべきは、ページ遷移に係るロジックが
Root
に集約されているという点です。ページ遷移というのはそもそもページ横断的なロジックなので、Root
が一枚噛んでいることは不自然ではありません。しかし、「画面B用のデータを待つ」という機能を
PageB
ではなくRoot
が担っている点が残念です。今回のように単純なパターンならば大きな問題にはなりませんが、Reactが提唱する「render-as-you-fetch」パターンを実装したいときに問題となります。また、細かいことをいえば、「fetchUsers()
の結果が帰ってきたらsetState
する」という処理は命令的な書き方であり、宣言的にUIを記述する流れに逆行しています。ここで登場したrender-as-you-fetchパターンとは何かというと、複数のデータを表示してロードする際に、ロードできた部分から順次表示していくというパターンです。なるべく早く情報を表示するという目的のためにこの戦略が取られることもあるでしょう。そして明らかに、これを実現するには「データを待つ」という部分が画面Bの中で制御される必要があります。上述の「データがロードされるまで画面Bに制御を渡さない」という設計はこれと明らかに逆行しています。
さらに、これと上記の要件を組み合わせると、「画面Bのメインのデータがロードできるまでは画面Aに留まるが、それ以外のデータがまだでも画面Bに遷移して良い」みたいな仕様が誕生するかもしれません。これをそのまま実現しようとすると、データローディングのロジックが
Root
内と画面B内に分割され、設計が壊滅的状況に陥ります。すぐに思い当たる解決策は「メインのデータのみ
Root
で読み込んで、それ以外のデータは画面Bがレンダリングされた後にuseEffect
なり何なりから別途非同期処理を発火して読み込む」というものです。しかし、これには「メイン以外のデータの読み込みが画面Bがレンダリングされるまで始まらない」という致命的な問題があります。最近のWebアプリケーションにとってパフォーマンスは命なので、たかだか設計の都合程度の理由でデータ読み込み開始を送らせていいわけがありません。ということで、ベストなUXを追求しようとすれば、手続き的なロジックにまみれた壊滅的な設計ができあがります。Concurrent Modeはこの状況に一石を投じました。
Concurrent Mode時代のデータローディング設計
前項で挙がった問題を纏めると、データを待つというロジックを
Root
が握っていること、ロジックが手続き的であること、そしてrender-as-you-fetchパターンが困難であることでした。次は、これらの問題を解決するためのConcurrent Mode的設計パターンを見ていきます。まず
Root
はこのように書き換えられるでしょう。type AppState = | { page: "A"; } | { page: "B"; usersFetcher: Fetcher<User[]>; }; export const Root: FunctionComponent = () => { const [state, setState] = useState<AppState>({ page: "A" }); const goToPageB = () => { setState({ page: "B", usersFetcher: new Fetcher(() => fetchUsers()) }); }; return ( <Suspense fallback={null}> <Page state={state} goToPageB={goToPageB} /> </Suspense> ); }; const Page: FunctionComponent<{ state: AppState; goToPageB: () => void; }> = ({ state, goToPageB }) => { if (state.page === "A") { return <PageA goToPageB={goToPageB} />; } else { return <PageB usersFetcher={state.usersFetcher} />; } };まず
Root
内に目を向けると、fetchUsers()
はnew Fetcher()
の中に押し込まれました。これにより、goToPageB
が持つロジックはステートを画面Bのものに更新するだけになりました。新しく
Page
というコンポーネントができてstate.page
による分岐がPage
の中に入りましたが、これはページの外側にSuspense
を配置することが目的です。Suspense
コンポーネントをどこに配置すべきかは別途解説しますが、今回のようにページ遷移でサスペンドが発生するかもしれないときはページより外側に配置するのが適しています。いちいちgoToPageB
を受け渡す必要があるのがダサいと思われるかもしれませんが、それはコンテキストなり何なりを使って解消できるのであまり本質的な問題ではありません。続いて、
PageA
コンポーネントはこのようになります。const PageA: FunctionComponent<{ goToPageB: () => void; }> = ({ goToPageB }) => { const [startTransition, isLoading] = useTransition({ timeoutMs: 10000 }); return ( <p> <button disabled={isLoading} onClick={() => { startTransition(() => { goToPageB(); }); }} > {isLoading ? "Loading..." : "Go to PageB"} </button> </p> ); };
isLoading
をuseState
で宣言するのをやめてuseTransition
を使うようになりました。画面Bへの遷移(goToPageB()
)をstartTransition
で囲むことで、遷移時にサスペンドが発生したらボタンにLoadinng...
が表示されるという制御がされています。目ざとい方は、この設計は微妙だと思ったかもしれません。というのも、
startTransition
は中でステートを更新することで意味を発揮する関数なのに、goToPageB
という関数は「画面Bに遷移する」という抽象化された意味を持たされており、中でステートの更新が行われることが明らかではありません。今回はgoToPageB
の実態がsetState({ ... })
なので偶々うまくいっていますが、startTransition
とsetStage
という2つがセットで扱われないといけないことが設計に現れていないのがどうにも微妙です。Reactの公式ドキュメントを読む限りはこれが大きな問題であるとは考えられていないようですが、個人的には改善の余地ありと感じるところです。
最後の
PageB
は特筆すべきところがありませんが、一応出しておきます。const PageB: FunctionComponent<{ usersFetcher: Fetcher<User[]>; }> = ({ usersFetcher }) => { const users = usersFetcher.get(); return ( <ul> {users.map(({ id, name }) => ( <li key={id}>{name}</li> ))} </ul> ); };以上のコードでは、最初に述べた従来の設計の3つの問題が解消されています。まず、「データを待つというロジックを
Root
が握っていること」及び「ロジックが手続き的であること」については、Root
が持つロジックがsetState
だけになったことによって解消されました。画面Bがデータを待つという部分も、Suspenseの機能およびFetcher
によって、手続き的な部分がReactの内部に隠蔽され、宣言的な書き方ができています。最後の「render-as-you-fetchパターンが困難であること」については、この例が簡単なので現れていません。これについては次の記事で詳しく扱います。
まとめ
この記事では、ページ遷移という課題を例にとり、従来型の設計とConcurrent Mode時代の設計を比較し、Concurrent Modeによって従来存在した問題が解決できることを示しました。
尤も、何が問題で何か問題でないかということについて唯一解は存在しませんから、Concurrent Modeの視点からということにはなります。Reactはだんだんとopinionatedなライブラリの色を強くしてきていますから、この記事の内容に同意できなくてもそれは悪いことではありません。
この記事までが「Concurrent Mode時代のReact設計論」シリーズの前半です。前半ではConcurrent Modeの基礎を解説し、Concurrent Modeがどのような問題を解決したいのかについて示しました。
シリーズ後半では、Concurrent Modeを前提とした設計について議論します。先ほど少しだけ触れたように、この記事で出てきたConcurrent Modeのコードは従来の問題を解決しますが、これがベストな設計かどうかは疑う余地があります。次回以降の記事では、Concurrent Modeの恩恵をより受けるためにどのような設計がベストかについて考えていきます。
次の記事: 鋭意執筆中です。
- 投稿日:2020-03-29T19:49:45+09:00
Concurrent Mode時代のReact設計論 (2) useTransitionを活用する
この記事は「Concurrent Mode時代のReact設計論」シリーズの2番目の記事です。
シリーズ一覧
- Concurrent Mode時代のReact設計論 (1) Concurrent Modeにおける非同期処理
- Concurrent Mode時代のReact設計論 (2) useTransitionを活用する
- Concurrent Mode時代のReact設計論 (3) SuspenseやuseTransitionが何を解決するか
- Concurrent Mode時代のReact設計論 (4) render-as-you-fetchのためのコンポーネント設計(仮)
- Concurrent Mode時代のReact設計論 (5) トランジションを軸に設計する(仮)
- Concurrent Mode時代のReact設計論 (6) ステート管理ライブラリの展望(仮)
- Concurrent Mode時代のReact設計論 (7) まとめ(仮)
useTransition
を活用する前回の記事ではConcurrent Modeの基礎的な機能と、それを扱うための考え方を説明しました。ボタンを押すとステートに
Fetcher
が突っ込まれて、それにより再レンダリング・サスペンドが発生するという流れでした。実は、その例ではサスペンドが発生した際に次のようなワーニングが発生します。
Warning: Container triggered a user-blocking update that suspended. The fix is to split the update into multiple parts: a user-blocking update to provide immediate feedback, and another update that triggers the bulk of the changes. Refer to the documentation for useTransition to learn how to implement this pattern.これは、ボタンの
onClick
のようにユーザーの操作をきっかけとして、再レンダリング→サスペンドが発生したときに表示されるワーニングです。これが意味するところを噛み砕いて説明すると、「ユーザーの入力に対してはすぐにフィードバックを返すべきだから、サスペンドする(=新しいステートが表示されるまでに時間がかかる)のは良くない」ということです。そして、このワーニングに対する対処法はずばり
useTransition
を使うことです。useTransition
を使うことで、ステートの更新でサスペンドが発生した場合に元々のステートを基にフィードバックを描画できるのです。
useTransition
の使用例さっそく、先ほどの例に
useTransition
を追加してみましょう。useTransition
はユーザーへのフィードバックを念頭に置いた機能なので、ユーザーへのフィードバックとしてボタンを押したらローディング中はボタンがdisabledになるという実装を入れてみましょう。Container
をこのように変更します。const Container: FunctionComponent = () => { // useTransitionの呼び出しを追加 const [startTransition, isLoading] = useTransition({ timeoutMs: 10000 }); const [usersFetcher, setUsersFetcher] = useState< Fetcher<User[]> | undefined >(); return ( <> <p> <button onClick={() => { // ステート更新をstartTransitionで囲む startTransition(() => { setUsersFetcher(new Fetcher(fetchUsers)); }); }} // isLoadingがtrueのときはdisabledに disabled={isLoading} > {isLoading ? "Loading..." : "Load Users"} </button> </p> <Suspense fallback={<p>Loading...</p>}> {usersFetcher ? <UserList usersFetcher={usersFetcher} /> : null} </Suspense> </> ); };
useTransition
はフックの一種なので、このように関数コンポーネントから呼び出します。結果はstartTransition
関数とisLoading
(真偽値)の組です。このstartTransition
はボタンのonClick
ハンドラの中で使われており、ステートの更新がstartTransition
で囲われています。startTransition
に渡されたコールバック関数は即座に呼び出されます。この実装では、ボタンを押すと以下のスクリーンショットのような挙動となります。
これを理解するために。
useTransition
の挙動を簡単に説明します。startTransition
の内部で行われたステートの更新がサスペンドを発生させた場合、変更後ではなく変更前のステートがレンダリングされます。ただし、このとき変更前のステートではuseTransition
が返すisLoading
がtrue
になっています。投げられたPromiseが解決された場合は変更後のステートで再レンダリングされます。
useTransition
を使わない場合との違いはサスペンド中に現れます。useTransition
を使わない場合はSuspense
によるフォールバックが表示されますが、useTransition
を使う場合はフォールバックは表示されず、代わりにステート更新前の状態が(isLoading
がtrue
で)レンダリングされるのです。
useTransition
にオプションとして渡したtimeoutMs
は、この「isLoading
がtrue
の状態」の最大持続時間を表します。この時間が過ぎてもPromiseが解決されなかった場合、諦めて変更後のステートがレンダリングされます。ただし、まだPromiseが解決されていないのでSuspense
によりフォールバックが表示されます。ボタンがクリックされてからの流れは次のようになります。
- 初期状態では、
usersFetcher=undefined, isLoading=false
である。(上のスクリーンショットの左の状態)startTransition
内でsetUsersFetcher
が呼ばれ、usersFetcher
ステートが更新される。(このときnew Fetcher
で作られたオブジェクトをF
とする)useTransition
の効果ににより、まずusersFetcher=undefined, isLoading=true
の状態でContainer
がレンダリングされ、DOMに反映される。(上のスクリーンショットの真ん中の状態)- 次に、新しいステート(
usersFetcher=F, isLoading=false
)でContainer
がレンダリングされる。これはUserList
のレンダリングに繋がり、UserList
のレンダリングはサスペンドする。useTransition
の効果により、この状態はDOMに反映されない。F
が持つPromiseが解決されると、新しいステート(usersFetcher=F, isLoading=false
)でContainer
が再レンダリングされる。今回はサスペンドが発生せずにレンダリングが完了し、この状態がDOMに反映される。(スクリーンショットの右の状態)ポイントは、
useTransition
内でステートの更新を行なった場合、新しいステートよりも「元のステート+isLoading=true
」のレンダリングが優先されるということです。これは、isLoading=true
の状態でユーザーへのフィードバックを表すことを意図しているためです。ユーザーへのフィードバックは最優先で画面に反映されるべきであるため、これが最初に処理されます。ちなみに、
startTransition
の中と外の両方でステートの更新を行うことができます。この場合、startTransition
の外で行なった更新は3の段階で反映されています(もちろん5の段階にも反映されます)。また、
timeoutMs
で設定した時間を超えない限り、Suspense
のfallback
で指定した内容は表示されなくなります。useTransition
をきちんと使っている限りは、Suspense
のfallback
はいわば最終防衛ラインのような扱いになり、高頻度でユーザーが目にするものではなくなります。
useTransition
の必要性Concurrent Modeにおける設計ではPromiseをステートに持つことになると前回述べましたが、この立場では
useTransition
の存在は必然的なものとなります。そもそも、アプリの状態・画面表示といったものの変化は、Reactにおいてはステートの変化として表されます。ステートの変化によって起こることは再レンダリングです。そして、非同期処理によって発生するサスペンドは、再レンダリングの結果として起こります。
ということは、当然ながら、ステートを更新しないとサスペンドが発生しないということです。ステートを更新するということは、(
Suspense
によるフォールバックになるかもしれませんが)新しい画面がレンダリングされるということであり、そうなると普通は古いステートは捨てられます。しかし、これは時に問題となります。例えば、「画面Aから別の画面Bに遷移したい。ただし、画面Bを表示するには非同期処理によるデータの読み込みが必要」という場合を考えてみましょう。しかも、データの読み込み中は画面Aに留まって読み込み中の表示にしたいとします。このとき、非同期処理が完了し次第画面Bに遷移するようにするには、とにかく画面Bをレンダリングしてサスペンドさせる必要があります。しかし画面Bをレンダリングしてしまうと画面Aは消えてしまいます。
この問題に対して、
useTransition
は「古い状態(画面A)と新しい状態(画面B)を同時に扱う」という方法で対処します。これはちょうど、gitでブランチを切って2つのバージョンのステートをメンテナンスするようなものです(Reactの公式ドキュメントでもこの例えが用いられています)。これによって、「まだ画面には反映されないけど新しいステートをレンダリングする」ということが可能になりました。まとめ
この記事ではReactが発するワーニングをきっかけとして
useTransition
を導入しました。Promiseをステートに入れるという設計方針をとったとき、useTransition
は欠かせない部品となります。次回は、なぜそこまでしてPromiseをステートに入れたいのかについて議論します。
次の記事: Concurrent Mode時代のReact設計論 (3) SuspenseやuseTransitionが何を解決するか
- 投稿日:2020-03-29T19:49:45+09:00
Concurrent Mode時代のReact設計論 (2) `useTransition`を活用する
この記事は「Concurrent Mode時代のReact設計論」シリーズの2番目の記事です。
シリーズ一覧
- Concurrent Mode時代のReact設計論 (1) Concurrent Modeにおける非同期処理
- Concurrent Mode時代のReact設計論 (2) useTransitionを活用する
- Concurrent Mode時代のReact設計論 (3) SuspenseやuseTransitionが何を解決するか
- Concurrent Mode時代のReact設計論 (4) render-as-you-fetchのためのコンポーネント設計(仮)
- Concurrent Mode時代のReact設計論 (5) トランジションを軸に設計する(仮)
- Concurrent Mode時代のReact設計論 (6) ステート管理ライブラリの展望(仮)
- Concurrent Mode時代のReact設計論 (7) まとめ(仮)
useTransition
を活用する前回の記事ではConcurrent Modeの基礎的な機能と、それを扱うための考え方を説明しました。ボタンを押すとステートに
Fetcher
が突っ込まれて、それにより再レンダリング・サスペンドが発生するという流れでした。実は、その例ではサスペンドが発生した際に次のようなワーニングが発生します。
Warning: Container triggered a user-blocking update that suspended. The fix is to split the update into multiple parts: a user-blocking update to provide immediate feedback, and another update that triggers the bulk of the changes. Refer to the documentation for useTransition to learn how to implement this pattern.これは、ボタンの
onClick
のようにユーザーの操作をきっかけとして、再レンダリング→サスペンドが発生したときに表示されるワーニングです。これが意味するところを噛み砕いて説明すると、「ユーザーの入力に対してはすぐにフィードバックを返すべきだから、サスペンドする(=新しいステートが表示されるまでに時間がかかる)のは良くない」ということです。そして、このワーニングに対する対処法はずばり
useTransition
を使うことです。useTransition
を使うことで、ステートの更新でサスペンドが発生した場合に元々のステートを基にフィードバックを描画できるのです。
useTransition
の使用例さっそく、先ほどの例に
useTransition
を追加してみましょう。useTransition
はユーザーへのフィードバックを念頭に置いた機能なので、ユーザーへのフィードバックとしてボタンを押したらローディング中はボタンがdisabledになるという実装を入れてみましょう。Container
をこのように変更します。const Container: FunctionComponent = () => { // useTransitionの呼び出しを追加 const [startTransition, isLoading] = useTransition({ timeoutMs: 10000 }); const [usersFetcher, setUsersFetcher] = useState< Fetcher<User[]> | undefined >(); return ( <> <p> <button onClick={() => { // ステート更新をstartTransitionで囲む startTransition(() => { setUsersFetcher(new Fetcher(fetchUsers)); }); }} // isLoadingがtrueのときはdisabledに disabled={isLoading} > {isLoading ? "Loading..." : "Load Users"} </button> </p> <Suspense fallback={<p>Loading...</p>}> {usersFetcher ? <UserList usersFetcher={usersFetcher} /> : null} </Suspense> </> ); };
useTransition
はフックの一種なので、このように関数コンポーネントから呼び出します。結果はstartTransition
関数とisLoading
(真偽値)の組です。このstartTransition
はボタンのonClick
ハンドラの中で使われており、ステートの更新がstartTransition
で囲われています。startTransition
に渡されたコールバック関数は即座に呼び出されます。この実装では、ボタンを押すと以下のスクリーンショットのような挙動となります。
これを理解するために。
useTransition
の挙動を簡単に説明します。startTransition
の内部で行われたステートの更新がサスペンドを発生させた場合、変更後ではなく変更前のステートがレンダリングされます。ただし、このとき変更前のステートではuseTransition
が返すisLoading
がtrue
になっています。投げられたPromiseが解決された場合は変更後のステートで再レンダリングされます。
useTransition
を使わない場合との違いはサスペンド中に現れます。useTransition
を使わない場合はSuspense
によるフォールバックが表示されますが、useTransition
を使う場合はフォールバックは表示されず、代わりにステート更新前の状態が(isLoading
がtrue
で)レンダリングされるのです。
useTransition
にオプションとして渡したtimeoutMs
は、この「isLoading
がtrue
の状態」の最大持続時間を表します。この時間が過ぎてもPromiseが解決されなかった場合、諦めて変更後のステートがレンダリングされます。ただし、まだPromiseが解決されていないのでSuspense
によりフォールバックが表示されます。ボタンがクリックされてからの流れは次のようになります。
- 初期状態では、
usersFetcher=undefined, isLoading=false
である。(上のスクリーンショットの左の状態)startTransition
内でsetUsersFetcher
が呼ばれ、usersFetcher
ステートが更新される。(このときnew Fetcher
で作られたオブジェクトをF
とする)useTransition
の効果ににより、まずusersFetcher=undefined, isLoading=true
の状態でContainer
がレンダリングされ、DOMに反映される。(上のスクリーンショットの真ん中の状態)- 次に、新しいステート(
usersFetcher=F, isLoading=false
)でContainer
がレンダリングされる。これはUserList
のレンダリングに繋がり、UserList
のレンダリングはサスペンドする。useTransition
の効果により、この状態はDOMに反映されない。F
が持つPromiseが解決されると、新しいステート(usersFetcher=F, isLoading=false
)でContainer
が再レンダリングされる。今回はサスペンドが発生せずにレンダリングが完了し、この状態がDOMに反映される。(スクリーンショットの右の状態)ポイントは、
useTransition
内でステートの更新を行なった場合、新しいステートよりも「元のステート+isLoading=true
」のレンダリングが優先されるということです。これは、isLoading=true
の状態でユーザーへのフィードバックを表すことを意図しているためです。ユーザーへのフィードバックは最優先で画面に反映されるべきであるため、これが最初に処理されます。ちなみに、
startTransition
の中と外の両方でステートの更新を行うことができます。この場合、startTransition
の外で行なった更新は3の段階で反映されています(もちろん5の段階にも反映されます)。また、
timeoutMs
で設定した時間を超えない限り、Suspense
のfallback
で指定した内容は表示されなくなります。useTransition
をきちんと使っている限りは、Suspense
のfallback
はいわば最終防衛ラインのような扱いになり、高頻度でユーザーが目にするものではなくなります。
useTransition
の必要性Concurrent Modeにおける設計ではPromiseをステートに持つことになると前回述べましたが、この立場では
useTransition
の存在は必然的なものとなります。そもそも、アプリの状態・画面表示といったものの変化は、Reactにおいてはステートの変化として表されます。ステートの変化によって起こることは再レンダリングです。そして、非同期処理によって発生するサスペンドは、再レンダリングの結果として起こります。
ということは、当然ながら、ステートを更新しないとサスペンドが発生しないということです。ステートを更新するということは、(
Suspense
によるフォールバックになるかもしれませんが)新しい画面がレンダリングされるということであり、そうなると普通は古いステートは捨てられます。しかし、これは時に問題となります。例えば、「画面Aから別の画面Bに遷移したい。ただし、画面Bを表示するには非同期処理によるデータの読み込みが必要」という場合を考えてみましょう。しかも、データの読み込み中は画面Aに留まって読み込み中の表示にしたいとします。このとき、非同期処理が完了し次第画面Bに遷移するようにするには、とにかく画面Bをレンダリングしてサスペンドさせる必要があります。しかし画面Bをレンダリングしてしまうと画面Aは消えてしまいます。
この問題に対して、
useTransition
は「古い状態(画面A)と新しい状態(画面B)を同時に扱う」という方法で対処します。これはちょうど、gitでブランチを切って2つのバージョンのステートをメンテナンスするようなものです(Reactの公式ドキュメントでもこの例えが用いられています)。これによって、「まだ画面には反映されないけど新しいステートをレンダリングする」ということが可能になりました。まとめ
この記事ではReactが発するワーニングをきっかけとして
useTransition
を導入しました。Promiseをステートに入れるという設計方針をとったとき、useTransition
は欠かせない部品となります。次回は、なぜそこまでしてPromiseをステートに入れたいのかについて議論します。
次の記事: Concurrent Mode時代のReact設計論 (3) SuspenseやuseTransitionが何を解決するか
- 投稿日:2020-03-29T19:49:37+09:00
Concurrent Mode時代のReact設計論 (1) Concurrent Modeにおける非同期処理
Concurrent Modeは、現在(2020年3月)実験的機能として公開されているReactの新しいバージョンです。Reactの次のメジャーバージョン(17.x)で正式リリースされるのではないかと思っていますが、確証はありません。なお、React公式からもすでに結構詳細なドキュメントが出ています。
Concurrent Modeに適応したアプリケーションを作るためには、従来とは異なる新しい設計が必要となります。筆者はConcurrent Modeを使ったアプリケーションをひとつ試作してみました。この記事から始まる「Concurrent Mode時代のReact設計論」シリーズでは、ここから得た知見を共有しつつ、Concurrent Mode時代に適応したReactアプリケーションの設計を提案します。
なお、Concurrent Modeはまだ正式リリース前の機能です。今後正式リリースまでの間にAPIの変更などが発生してこの記事の内容が当てはまらなくなる可能性は否定できませんが、その際はご容赦ください。
ちなみに、作ったアプリケーションはこれです。(宣伝)
プルリクエストも大募集しています。問題の追加はConcurrent Modeを理解していなくても大丈夫です。(宣伝)
シリーズ一覧
- Concurrent Mode時代のReact設計論 (1) Concurrent Modeにおける非同期処理
- Concurrent Mode時代のReact設計論 (2) useTransitionを活用する
- Concurrent Mode時代のReact設計論 (3) SuspenseやuseTransitionが何を解決するか
- Concurrent Mode時代のReact設計論 (4) render-as-you-fetchのためのコンポーネント設計(仮)
- Concurrent Mode時代のReact設計論 (5) トランジションを軸に設計する(仮)
- Concurrent Mode時代のReact設計論 (6) ステート管理ライブラリの展望(仮)
- Concurrent Mode時代のReact設計論 (7) まとめ(仮)
現在は(3)まで公開済です。
イントロダクション
Concurrent ModeにおいてはReactの内部の実装が変更され、レンダリングの中断・再開をサポートするようになります。これにより、ユーザーの入力により素早く反応するなど、ReactアプリケーションのUX向上が期待できます。
Concurrent Modeは、useTransitionに代表される新しいAPIを搭載しており、Concurrent Modeを完全に活かすには新しいAPIを使いこなさなければいけません。useTransitionについては筆者の以前の記事が詳しいので、気になる方は合わせてお読みください。この記事の理解に必須ではありません。
冒頭で述べた通り、このシリーズでは筆者がConcurrent Modeを試してみた経験を基にして、Concurrent Mode時代に適応したReactアプリケーションの設計を提案します。もちろんこれが唯一解であると主張したいわけではありませんが、最も基本的な考え方として通用するものだと考えています。
なお、このシリーズではステート管理やデータフェッチング用の外部ライブラリを使わない、最も基本的なConcurrent Mode向け設計を議論します。これから先Concurrent Modeに適応したライブラリが増えることと思いますが、そのライブラリを使う場合はまた異なる設計となるかもしれない点はご了承ください。まあライブラリを使うかどうかで設計が変わるのは当たり前の話ですが。
なお、実際に手を動かしながら読みたいという方向けに、TypeScript + React Concurrent Modeの設定がしてあるCodeSandboxを用意してあります。適当にいじって試してみましょう。
非同期処理の扱い方が変わる
React Concurrent Modeの最大の特徴として「Promiseを
throw
する」という衝撃的な仕様のみを知っていたという方も多いでしょう。Promiseというのは、非同期処理を表すのに非常に広く使われるオブジェクトです。レンダリング時にPromiseを
throw
するには、コンポーネントがPromiseを持っている必要があります。コンポーネントがPromiseを持つ場合の選択肢は主にステートに持つ(useState
とか)かrefで持つ(useRef
)のどちらかです。もちろんpropsやuseContext
で受け取ることもできますが、それは親のコンポーネントが何らかの手段でPromiseを調達しているので本質的にはやはり前記のどちらかです。一般に、レンダリング結果に関わるものを
useRef
で持つのは良くありません(後述しますが、Concurrent Modeではこれまで以上にこれを厳守する必要があります)。よって、Promiseをステートに持つことが必要になります。ただ、実際には生のPromiseでは機能不足なので、適当なラッパーを作ることになります(あとで具体例が出てきます)。Promiseをステートに持つことで、コンポーネントは「非同期処理の途中」というステートをもはや表現する必要がなくなります。それは「レンダリングの中断(サスペンド)」で表せば良いのですから。つまり、例えば「データがあればロード済、データが無ければロード中」のようなロジックをコンポーネントが持つことは無くなります。
言い換えれば、コンポーネントはデータがロード中の場合の処理を気にする必要が無くなります。ただし、実際には「レンダリングの中断」の場合を別の場所(
Suspense
のフォールバック、あるいはuseTransition
のトランジション中状態)でハンドリングする必要がありますから、非同期処理について全く考えなくていいわけではありません。その意味では、より正確に言えばConcurrent Modeは非同期処理の扱いをより疎結合に表現する手段を提供してくれるというところでしょう。従来我々が手ずから扱っていた非同期処理対応の一部分を、Reactが組み込みの機能として受け持ってくれるという見方もできます。Concurrent Modeにおける非同期処理
では、改めてConcurrent Modeにおける非同期処理について説明します。
Concurrent Modeでは、コンポーネントがPromiseを投げることでサスペンド(レンダリングの中断)を表すことができます。その場合、当該のPromiseが解決されたら再度レンダリングが試みられます。まだ、サスペンドが発生したときに代替のビューを提供する機能が提供されます(
Suspense
やuseTransition
)。これらの機能を使うことで、Concurrent Modeではより宣言的に非同期処理を扱えるようになったと言えます。ただし、同時にこの機能はReactと非同期処理をより密結合なものにするという側面を持ち合わせています。その意味で、ReactやConcurrent Modeでよりopinionatedなライブラリになったと言えます。
まずは、Concurrent Modeにおける基本的な非同期処理の例を示します。例を通してConcurrent Modeの感覚を掴みましょう。
まず、先ほど少し言及したPromiseのラッパーを定義します。
Promiseをラップする
Fetcher<T>
Fetcher<T>
という名前は我ながら微妙な気がするのですが、いい命名が思いつかないので募集中です。Fetcher<T>
は内部にPromiseを持っており、さらに現在Promiseが現在どういう状態なのか(State<T>
)を知っています。これにより、「Promiseがまだ解決されていなかったらそのPromiseを投げる」という、Promiseの現在の状態に基づく分岐を実装しています。type State<T> = | { state: "pending"; promise: Promise<T>; } | { state: "fulfilled"; value: T; } | { state: "rejected"; error: unknown; };この
State<T>
型はPromiseの3つの状態(解決前、成功、失敗)を表現する型です。解決前の場合はそのPromiseを、成功済みの場合は結果の値(T
型)を、そして失敗の場合はエラーの値を保持します。このState<T>
を用いて書かれたFetcher<T>
の実装は以下の通りです1。export class Fetcher<T> { private state: State<T>; constructor(fetch: () => Promise<T>) { const promise = fetch().then( value => { this.state = { state: "fulfilled", value, }; return value; }, error => { this.state = { state: "rejected", error, }; throw error; }, ); this.state = { state: "pending", promise, }; } public get(): T { if (this.state.state === "pending") { throw this.state.promise; } else if (this.state.state === "rejected") { throw this.state.error; } else { return this.state.value; } } }
Fetcher<T>
のコンストラクタはPromiseを返す関数を受け取ってすぐに呼び出します。ここで返されたPromiseの状態が監視され、this.state
に反映されます。
Fetcher<T>
が唯一もつメソッドget()
は、Promiseが解決済だった場合はその値を返します。まだ解決されていない場合はPromiseをthrow
します。一応、Promiseが失敗していた場合はエラーを投げる処理も入れています。ポイントは、
get
の返り値がT
型になっている点です。Promiseをthrow
して大域脱出するという荒技によって、get
を呼んだ側は非同期処理の途中かどうかを意識しなくても良くなります。何せ、T
型の値が返ってきているということはもうT
型の値がある、つまり非同期処理の結果があるということなのですから。つまり、get()
を呼んでT
型の値を得たコンポーネントは、あたかも非同期処理がすでに完了しているかのように処理を進めればよいのです。まだ完了していなかった場合はPromiseが投げられてしまいますが、その場合はReactが頑張って処理してくれます。React Hooksが登場した時に「Algebraic Effectだ」なんて騒がれもしましたが、それと根本的な思想は同じです。すなわち、Reactが裏で頑張ることでシンプルなAPIを外向きに提供しているのです。
また、これだけ単純なラッパーでも、Promiseを投げるという点ですでにReactと癒着しています。しかし、前述の利点を得るためにはこれは欠かせません。これが、冒頭で触れた「Reactと非同期処理がより密結合になる」ということの意味です。
Fetcher
を使う例
Fetcher
を使うコンポーネントは、例えばこんな見た目になります。type User = { id: string, name: string }; const UserList: FunctionComponent<{ usersFetcher: Fetcher<User[]>, }> = ({ usersFetcher }) => { const users: User[] = usersFetcher.get(); return ( <ul> {users.map(({ id, name }) => ( <li key={id}>{name}</li> ))} </ul> ); };
UserList
コンポーネントは受け取ったFetcher<User[]>
のget
メソッドをいきなり呼び出してUser[]
を取得します。あとはそれを適当に表示するだけです。ここで、Fetcher<User[]>
は「User[]
型の結果を取得する非同期処理」そのものを表しています。get()
メソッドは、「その結果を取得する。まだ取得できない場合は取得できるまでサスペンドする」という意味になります。この
UserList
コンポーネントは例えば次のように使用できます(fetchUsers
が実際にUser[]
を取得する非同期処理を担当すると思ってください)。「Load Users」ボタンを押すとusersFetcher
にFetcher<User[]>
のインスタンスが入ってUserList
がレンダリングされます。なお、UserList
はサスペンドする可能性があるので、このようにSuspense
で囲んでフォールバックコンテンツ(中でサスペンドが発生したときに代わりにレンダリングされる内容)を指定しておく必要があります。なお、
Suspense
の中身でサスペンドが発生した場合はSuspense
の中身全体がフォールバックコンテンツに置きかわります。そのため、Suspense
をどこに置くかは、レンダリングが中断した時にどこまでフォールバックコンテンツになってほしいかによって決めることになります。Suspense
がネストしていた場合は一番内側のSuspense
が反応します。const Container: FunctionComponent = () => { const [usersFetcher, setUsersFetcher] = useState< Fetcher<User[]> | undefined >(); return ( <Suspense fallback={<p>Loading...</p>}> <p> <button onClick={() => { setUsersFetcher(new Fetcher(fetchUsers)); }} >Load Users</button> </p> {usersFetcher ? <UserList usersFetcher={usersFetcher} /> : null} </Suspense> ); };以上のようにして、実際に非同期処理を発生させて(
fetchUsers
を呼び出して)以降の流れが全部実装できました。これを実際に動作させると、非同期処理の途中は「Loading...」と表示されて読み込まれたらUserList
の中身がレンダリングされます。より具体的な流れとしては以下のことが発生しています。
Container
内でsetUsersFetcher
が呼び出されることでusersFetcher
ステートにFetcher
が入る。Container
が再レンダリングされてUserList
がレンダリングされる。UserList
がレンダリングされる(関数UserList
が呼び出される)最中に、get()
でPromiseがthrow
される(UserList
がサスペンドする)。- サスペンドが発生したので、
Suspense
の中身として<p>Loading...</p>
がレンダリングされる。- しばらくして
usersFetcher
が返したPromiseが解決される。- ReactがPromiseの解決を検知し、以前サスペンドした
UserList
が再レンダリングされる。- 今回は
get()
がPromiseを投げない(解決済のため)のでUserList
はサスペンドされずに描画される。一応画面の動きを示しておくと、このようになります。
従来の方式との比較
一応、従来の方式(Concurrent Modeより前の書き方)との比較を行なっておきます。一例ですが、素朴に書くならこんな感じでしょう。
const Container: FunctionComponent = () => { const [isLoading, setIsLoading] = useState(false); const [users, setUsers] = useState<User[] | undefined>(); return ( <> <p> <button onClick={() => { setIsLoading(true); fetchUsers().then(users => { setIsLoading(false); setUsers(users); }); }} > Load Users </button> </p> {isLoading ? ( <p>Loading...</p> ) : users ? ( <UserList users={users} /> ) : null} </> ); };ロード中・ロード完了という状態を表すために
isLoading
というステートが新設されました(TypeScript wayでReactを書くで説明したようにこれはベストなステートの表現ではありませんが、今回の本質にはあまり関わりません)。ボタンがクリックされたときは、「ローディング状態をにする→非同期処理を発火→終わったら結果をステートに反映」というステップを踏みます。Concurrent Modeに比べるとやはり複雑化しており、とくに
Container
コンポーネントが非同期処理をハンドリングするためのロジックを内包するようになったのが気になります。これが非同期処理の辛い点であり、各種のライブラリが頑張って解決しようとしている点でもあります。Concurrent Modeは、これに対して「非同期処理を表すオブジェクトそのものをステートに突っ込む」という斬新な解決策を提示しました。これは、非同期処理の扱いのつらい部分をサスペンドという機構に押し込むことで達成されています。
Concurrent Modeにおけるエラー処理
ここまでの例ではエラー処理を全く扱ってきませんでしたが、Concurrent Modeでは非同期処理に係るエラー処理も様変わりします。
というのも、非同期処理はPromiseで表されますが、Promiseというのは失敗(reject)する可能性があります。非同期処理におけるエラーはPromiseの失敗で表されます。では、
throw
したPromiseが失敗したらどうなるのでしょうか。答えは、Error Boundaryでキャッチされます。Error BoundaryはReact 16で導入された機能で、コンポーネントのレンダリング中にエラーが発生した場合にそれをキャッチしてエラー時のコンテンツをレンダリングできるものです。
従来は、非同期処理によるエラーはError Boundaryではキャッチされず、自前でハンドリングして必要なら自前でいい感じにUIに反映させるロジックを書く必要がありました。それは、非同期処理によって発生したエラーはレンダリング中に発生したエラーではないからです。
Concurrent ModeではPromiseを
throw
するという機構によって非同期処理がレンダリングによって組み込まれますから、非同期処理によって発生したエラーもレンダリング中に発生したエラーとして扱われるのは自然なことです。Error Boundaryは宣言的なエラー処理機構なので、Concurrent Modeでは非同期処理に対しても宣言的なエラー処理が可能になったということです。たいへん嬉しいですね。
まとめ
この記事では、Concurrent Modeの基礎である「Promiseを
throw
する」という方針を実現するためにPromiseをステートに持って扱う方法について説明しました。これにより、より宣言的に非同期処理を扱えるようになると共に、エラー処理をError Boundaryの機構で統一的に扱えるようになりました。次の記事: Concurrent Mode時代のReact設計論 (2) useTransitionを活用する
実際に上述のアプリで使われているバージョンではさらに
getOrUndefined
というメソッド(解決前だったらthrow
するのではなくundefined
を返す)があるのですが、これが本質的に必要なのかは悩んでいます。設計力の不足により必要になってしまっただけかもしれません。 ↩
- 投稿日:2020-03-29T18:12:42+09:00
callback形式refってちょっときになるやつをやってみる
概要
React hooksのドキュメントを眺めていたら、ちょっと気ななるuseRefの使い方が書かれていた。
いつどんな状況で使えるのかも含めて書いていく。https://ja.reactjs.org/docs/hooks-faq.html#how-can-i-measure-a-dom-node
なにをしたいやつか
リファレンスを見ている限り、refで指定しているコンポーネントがマウントされたタイミングで実行できる関数をrefに書くことができるように見えた。
export default function App() { const callbackref = useCallback(() => console.log("appのref"), []); return <div ref={callbackref} className="App" />; } // appのref再レンダリングされたとき
再レンダリングされたときは、もう一回関数が実行されるか試してみる。
予想:実行されるexport default function App() { // 再レンダリング用 const [state, setState] = useState(false); const toggleState = useCallback(() => setState(!state), [state]); console.log({ state }); const callbackref = useCallback(() => console.log("appのref"), []); return ( <div ref={callbackref} className="App"> <button onClick={toggleState}>toggle</button> </div> ); }予想と違って、初回のレンダリングときだけ処理されるようだ。
子ノードにも持たせてみる
子のノードにもcallbackrefを渡したときはどんな順番で処理されるのか見てみる
予想:親→子の順番export default function App() { // 再レンダリング用 const [state, setState] = useState(false); const toggleState = useCallback(() => setState(!state), [state]); console.log({ state }); const callbackref = useCallback((node) => console.log(node), []); return ( <div ref={callbackref} className="pearent"> <div ref={callbackref} className="child" /> <button onClick={toggleState}>toggle</button> </div> ); }これも予想とは違い、子→親の順番で処理された。
これはの順番もあるかもだけど、レンダリング処理が終わった順番から処理されるのかもしれない。useEffectと比べて
useEffectの処理との順番を見る
予想:子→親→useEffectexport default function App() { // 再レンダリング用 const [state, setState] = useState(false); const toggleState = useCallback(() => setState(!state), [state]); useEffect(() => { console.log('useEffect'); }, []); const callbackref = useCallback((node) => console.log(node), []); return ( <div ref={callbackref} className="pearent"> <div ref={callbackref} className="child" /> <button onClick={toggleState}>toggle</button> </div> ); }これは予想通り、最後にuseEffectの処理が来た。
ここまででわかったこと
最初に述べたように、書くrefのノードがマウントされたあとにだけ処理されるようだ。
今まで、ノードに対して直接処理をしたい場合はuseEffectに書いていたんだが、これを使えば、useEffectを書かずとも、そのノードのrefに書いてあげればうまく動いてくれることがわかった。
使いみち
そして使いみちだけど、正直なにも思いつかないw
ライブラリ作るときとか、ちょっとこったUI作ったりするときは使うのかもしれないが、ちょっと考えつかない。。。もしこんなので使ってるよみたいなのがあったら教えてほしいです。
- 投稿日:2020-03-29T17:21:26+09:00
インベントリー管理系のElasticsearchフロントアプリを1日で作る
はじめに
Elasticsearchを使って、インベントリーデータをささっと検索して必要な情報を取り出すようなアプリをゼロから1日で作る手順です。所持品やナレッジを分類データ化したものをパソコンやスマホから検索できるようにします。当然データ自体は自分で用意する必要があります。
作成するもの
バックエンドはAmazon Elasticsearch Serviceを用意し、データはそこに蓄積します。
フロントエンドはブラウザで動作するReactアプリで、Reactivesearchというコンポーネントを活用して、Elasticsearch Serviceのデータを検索できるようにします。ブラウザ(のJavaScript)から直接Elasticsearch Serviceにアクセスします。
また、Reactアプリを自動でビルドしデプロイする環境もCodepipelineで作ります。アプリの画面イメージはこちら。
(この画面はインベントリー管理になっていませんが流用はできると思います)
手順
詳細手順を記述した記事をこの順番でやっていけば作れます。右端の時間は、順調に進められたときの目安の時間です。
- Amazon Elasticsearch Serviceで検索できる状態まで最速で立ち上げる(30分)
- React版Reactivesearch v3を使ってゼロから最速でElasticsearchフロントアプリを作る(1時間)
- React版ReactivesearchアプリをiPhone縦でも見やすくする(15分)
- AWS S3 + CloudFrontでReactアプリをHTTPS公開するための正しい構成(1時間)
- AWS Codepipelineを使ってReactアプリのCI環境をゼロから作る(1時間)
- Bracketsエディタからgit pushボタンで自動でデプロイされるCI環境を作る(15分)
番外編
今回は対応していませんが、派生での参考記事です。
Amplifyを使えば今回と同等の環境はもっと簡単に作れます。ただし、制約はあります。
AWS Amplify Consoleを使ってReactアプリのCICD環境を10分で作るElasticsearch Serviceに認証を付けたいところですが、今回は制約から見送りました。
Amazon Elasticsearch ServiceのKibana Cognito認証設定をゼロから最小限の設定で実現する
- 投稿日:2020-03-29T17:21:26+09:00
インベントリー/ナレッジ管理系のElasticsearchフロントアプリを1日で作る
はじめに
Elasticsearchを使って、インベントリーデータをささっと検索して必要な情報を取り出すようなアプリをゼロから1日で作る手順です。所持品やナレッジを分類データ化したものをパソコンやスマホから検索できるようにします。当然データ自体は自分で用意する必要があります。
作成するもの
バックエンドはAmazon Elasticsearch Serviceを用意し、データはそこに蓄積します。
フロントエンドはブラウザで動作するReactアプリで、Reactivesearchというコンポーネントを活用して、Elasticsearch Serviceのデータを検索できるようにします。ブラウザ(のJavaScript)から直接Elasticsearch Serviceにアクセスします。
また、Reactアプリを自動でビルドしデプロイする環境もCodepipelineで作ります。アプリの画面イメージ
(この画面はインベントリー管理になっていませんが流用はできると思います)
インフラの概要図
環境
Elasticsearch v7.4
Node.js v13.10.1
React v16.13.0
Reactivesearch v3.5.0手順
詳細手順を記述した記事をこの順番でやっていけば作れます。右端の時間は、順調に進められたときの目安の時間です。
- Amazon Elasticsearch Serviceで検索できる状態まで最速で立ち上げる(30分)
- React版Reactivesearch v3を使ってゼロから最速でElasticsearchフロントアプリを作る(1時間)
- React版ReactivesearchアプリをiPhone縦でも見やすくする(15分)
- AWS S3 + CloudFrontでReactアプリをHTTPS公開するための正しい構成(1時間)
- AWS Codepipelineを使ってReactアプリのCI環境をゼロから作る(1時間)
- Bracketsエディタからgit pushボタンで自動でデプロイされるCI環境を作る(15分)
番外編
今回は対応していませんが、派生での参考記事です。
Amplifyを使えば今回と同等の環境はもっと簡単に作れます。ただし、制約はあります。
AWS Amplify Consoleを使ってReactアプリのCICD環境を10分で作るElasticsearch Serviceに認証を付けたいところですが、今回は制約から見送りました。
Amazon Elasticsearch ServiceのKibana Cognito認証設定をゼロから最小限の設定で実現する
- 投稿日:2020-03-29T17:15:48+09:00
今更ながらReactのHooksを使ってみた
はじめに
2017年~2018年あたりでReact 16.x.xを使っていましたが、それ以降しばらく触っていませんでした。
去年の終わりくらいから改めてReactを触ろうとしたところ、Hooksなる機能がReact 16.8から追加されたということで、触ってみた際の学びを備忘録として残しておきます。Hooksいいですね!対象Ver: 16.12.0
公式ドキュメント
https://ja.reactjs.org/docs/hooks-intro.html※以降のコードは、私はこんな雰囲気で書いたんじゃよ、という備忘録ですので動作保証は致しません。
※私の検証ベースで記載している部分があるので、間違っていた場合はご指摘頂けると嬉しいです。useState
state管理のHook。管理したいstate単位にuseStateを実行し、戻り値としてstate自身とそのsetter(setStateみたいなもの)を受け取る。useStateの引数は初期値。
loading.jsximport React, { useState } from 'react' import LoadingIcon from '../icons/loading' const App = props => { const [content, setContent] = useState() // 何かアクションに応じてデータを取得 const loadData = () => { apiCall().then(result => { setContent(result) // 取得したコンテンツを表示 }) } return ( <div> <button onClick={loadData}>Load</button> <p>{content}</p> </div> ) }らくちん。
useEffect
stateの変化を検知して処理を行う場合に使う。
loading.jsximport React, { useState, useEffect } from 'react' const App = props => { const [content, setContent] = useState() const [filteredContent, setFilteredContent] = useState() const loadData = () => { // 省略 } useEffect(() => { // 何か処理をしてセット const filteredContent = filter(content) setFilteredContent(filteredContent) }, [content]) return ( <div> <button onClick={loadData}>Load</button> <p>{filteredContent}</p> </div> ) }第二引数の配列には、ウォッチしたいstateを指定する。今回であればcontentが変化した際に処理を実行したいので、contentを指定。
useRef
何かしらの参照を持っておくためのハコみたいなイメージ。(段々説明が雑になってきました)
公式ドキュメントにもありますが、Reactコンポーネントにref={}で渡して、コンポーネントの参照を持って置くためのものと思っていましたが、汎用的な箱として利用可能です。具体例を示した方が分かりやすいので、実際に私がはまった例とその解決策を。
データロード中かどうかをstateで管理して、多重ロードを避けるために書いたコードが以下。loading.jsximport React, { useState } from 'react' const App = props => { const [content, setContent] = useState() const [loading, setLoading] = useState(false) const loadData = () => { // ロード中ならスキップ if (loading) return setLoading(true) // ロード中に設定 apiCall().then(result => { setContent(result) setLoading(false) // ロード中ステータスを解除 }) } return ( <div> <button onClick={loadData}>Load</button> <p>{content}</p> </div> ) }これだとうまくいきませんでした。
なぜか。loadData関数を定義した時点でClosureにその時点のloading変数の内容を保持されるので、いつまで経ってもloadingはfalseのままでした。
なのでこうしました。loading.jsximport React, { useState, useRef } from 'react' const App = props => { const [content, setContent] = useState() const loadingRef = useRef() loadingRef.current = false // 初期化 const loadData = () => { // ロード中ならスキップ if (loadingRef.current) return loadingRef.current = true // ロード中に設定 apiCall().then(result => { setContent(result) loadingRef.current = false // ロード中ステータスを解除 }) } return ( <div> <button onClick={loadData}>Load</button> <p>{content}</p> </div> ) }useRefの戻りはオブジェクトなので、それをClosureで持っておけば現在の値が参照可能なので、正しく動作するようになりました。
たぶん使い方は合っているハズ。。useReducer
最初に書きましたが、Reduxのaction/reducerなどの記述量の多さが苦手で、できれば避けたいと思っていましたが、避けられない場面が出てきました。
サーバから取得した結果を順々に配列に追加していくような、以下のコードを書いてみました。
loading.jsximport React, { useState, useRef } from 'react' const App = props => { const { seq } = props const [results, setResults] = useState([]) // コールバック参照用 const resultsRef = useRef() resultsRef.current = results useEffect(() => { resultsRef.current = results }, [results]) const addResult = result => { // 別オブジェクトにしないとReactが変更を検知しないので、別配列として処理 const newResults = resultsRef.current.concat(result) setResults(newResults) } // 何かアクションに応じてデータを取得 const loadData = (sequence) => { apiCall(sequence).then(result => { if (result.status === 404) return addResult(result) // 結果を配列に追加 loadData(sequence + 1) // 最新を取得するまでループ }) } return ( <div> <button onClick={() => { loadData(seq) }}>Load</button> <p>{content}</p> </div> ) }これでうまくいくかと思いきや、追加したデータが消えていたりする。。。
今試してみたサンプルコードは以下。sample.jsxconst [arr, setArr] = useState([]) const arrRef = useRef() arrRef.current = arr useEffect(() => { arrRef.current = arr console.log('-----from-----') console.log(arrRef.current.length) console.log(arrRef.current) console.log('-----to-----') }, [arr]) useEffect(() => { for(let i=0; i<100; i++) { const newArr = arrRef.current.slice() newArr.push(i) setArr(newArr) } }, [])結果はこう。
-----from----- 0 [] -----to----- -----from----- 1 [99] -----to-----
前のstateを踏まえて何か処理する場合、useRefで参照を持っていても不十分だったようです。
そこでuseReducerの出番。sample.jsxconst sampleReducer = (state, action) => { switch(action.type) { case 'add': const newArr = state.slice() newArr.push(action.payload) return newArr default: return state } } const [arr, dispatch] = useReducer(sampleReducer, []) useEffect(() => { console.log('-----from-----') console.log(arr.length) console.log(arr) console.log('-----to-----') }, [arr]) useEffect(() => { for(let i=0; i<100; i++) { dispatch({ type: 'add', payload: i }) } }, [])結果はこう。
-----from----- 0 [] -----to----- -----from----- 100 (100) [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99] -----to-----
100回出力されていないのは、おそらくdispatchを頻繁に実行したため、reducer側が良しなに更新タイミングを減らしてくださったのではと予想。
Reduxをご存知の方はお分かりと思いますが、前のstateを受けて処理が可能なので、addした分だけ情報が格納されています。
なので、前の状態に+αで変更する際はuseReducerを使うべき、というのが学びです。
そしてuseReducerを使ってみて思いましたが、結構簡素に書けますね。
以前はTypeScriptを使っていたこともあり、余計冗長に感じてしまったのかもしれません。まとめ
- useStateはstateとそのsetterを返す
- stateがオブジェクトの場合、setterに指定するのは新しいオブジェクトにすること(Reactが検知できないっぽい)
- useEffectはstateの変更を検知して処理を行うヤツ
- useRefは使いやすいハコ
- useReducerは前のstateを踏まえて処理したい場合に有効
- 投稿日:2020-03-29T15:17:40+09:00
npmでReactのプロジェクト作成
npx create-react-app プロジェクト名プロジェクト作成すると下記画面が表示される
Success! Created react-app at /Users/xxx/Myapp/React/learning/react-app Inside that directory, you can run several commands: npm start Starts the development server. npm run build Bundles the app into static files for production. npm test Starts the test runner. npm run eject Removes this tool and copies build dependencies, configuration files and scripts into the app directory. If you do this, you can’t go back! We suggest that you begin by typing: cd react-app npm start上記の画面にはコマンド毎の操作説明が表示されており、こちらのコマンドは
npm
の他にyarn
でも実行可能1.
yarn start
又はnpm start
・webブラウザを起動
2.yarn build
又はnpm run build
・プロジェクトのビルド(プロジェクトのファイルから実際のWebサーバーにアップロードして利用するファイル類を生成する)
3.yarn test
又はnpm test
・テストプログラムを実行して、アプリケーションのテストを行う
4.yarn eject
又はnpm run eject
・プロジェクトのインジェクトを行う
- 投稿日:2020-03-29T13:29:25+09:00
フロントエンド知らないマンが始めるTypeScript/React/Redux入門
- backend(Ruby/Scala)/infraしかやったことない自分がReact/Redux (with TypeScript)について入門した内容をまとめてます
- 手を動かしていく上で必要そうな概念やキーワードを中心にまとめています
- 具体的なコードの書き方は公式などをどうぞ
1. JS(ES6以降のJS)
以下が分からないと割と読めない...
- アロー関数
- スプレッド演算子
- Promise, async/await
- export default
- import時に好きな名前をつけて読み込める
2. 関数型プログラミング
- 詳細は置いておいて、そういうものがあるのと、Reactでも一部そういうのが使われてる
2-2. コレクション
- map()
- filter()
- reduce()
- etc.
2-2-1. 高階関数
2-2-2. クロージャー
2-2-3. ジェネレーター
2-2-4. カリー化/部分適応
3. TypeScript
3-1. 型の種類
- number
- string
- []
- boolean
- symbol
- null
- undefined
- etc.
3-2. 型推論
- 型を明示しなくてもTypeScriptが良い感じで型を推論して当てはめてくれる
3-2-1. interface
- オブジェクトに対してinterfaceを使って型を定義する
interface Human { firstName: string; lastName: string; age: number; } const human1: Human = { firstName: 'hello', lastName: 'world', age: 20 }3-2-2. Type Alias
- interfaceに対して別名でエイリアスを付けられる
- 主に
型の合成
の際に使われるinterface Student { studentId: number; name: string; grade: number; } interface Professof { name: string; subject: string; } type StudentOrProcessof = Student | Professor; // {studentId: number, name: string, grade: number} or {name: string, subject: string}3-2-3. 交差型
- 複数の型をひとつにまとめたものであり、
&
を使って定義する- 合成した型のすべてのプロパティを備えるが、同じ名前のプロパティが省略可能と必須だと、
必須
が優先される。3-2-4. 共用体型
- 渡された複数の型のいずれかが適応され、
|
を使って定義する3-3. immutableな配列/オブジェクト
- constで定義した配列/オブジェクトは中の値を上書きできてしまう
const arr: number[] = [1,2,3]; arr[1] = 100; arr // [1, 100, 3]; const obj: {} = {a: 1, b: 2 }; obj.a = 0; obj // {a: 0, b: 2 }Readonlyな型を使うことで内部への上書きを防止できる
const arr: readonly string[] = ['hello', 'world']; arr[0] = 'good night'; // error TS2542 const obj: Readonly<{ hoge: number }> = { hoge: 2 }; obj.hoge = 100; // error TS2540ただし、比較的最近出た機能なので使うかは微妙
代わりに、スプレッド演算子を使うのが無難かもconst obj: {} = { hoge: 1, fuga: 2 }; const obj2: {} = { ...obj, piyo: 3 }; obj3 // { hoge: 1, fuga: 2, piyo: 3 }3-4. 関数の型
- 引数には必ず型を指定する必要あり
- 戻り値は型推論で省略できる
3-5. ジェネリクス
- Scalaでいう型パラメータ
- 対象の型を指定することで汎用性の高いインターフェースとしての役割を担う
- ref: ジェネリクスをもう少しだけ使いこなす。
4. JSX / TSX
JavaScript eXtension
/TypeScript eXtension
の略- JS/TSにHTMLっぽい構文を拡張したものであるが、基本的にはJS/TSがベース
- 基本的にはJSX/TSXを使ってReactを書いていく
- 拡張子は
.jsx
/.tsx
5. React入門する上で重要な概念
5-1. 仮想DOM
- 画面上で何か変更がある度に、実際のDOMを全て再読み込みするとコストが高くつく
- 仮想的にDOMを構成し、処理結果の差分だけを更新するようにしている
5-2. コンポーネント指向
- 考え方としてはWeb Componentsから来ているらしい
- 再利用可能なカプセル化された要素を組み合わせてアプリケーションを作成する
5-3. 単方向データフロー
- データは親コンポーネントから子コンポーネントへ単一方向で渡される(逆はできない)
- データの流れを単一方向にすることで、処理の把握が簡単になり、バグも出にくくなる
- 画面上の状態が複雑になってくると、複数方向でのデータフローはパターンが多すぎて対処できない
6. React
6-1. 重要な概念
6-1-1. Class ComponentとFunctional Component
- 前までは
Class Component
がメインだったけど、this
の扱いやら記述の多さがいやとのことで、現在はFunctional Component
によるコード記述が主流になってる
- Reactの過去の遺産の多くはClass Componentで書かれていることもあり、Class Componentが廃止されることはそうなさそうだけど、今後自分たちが書くコードはFunctional Componentにするのが無難
6-2. コンポーネント
6-2-1. Props
- 親コンポーネントから渡されてくる値
6-2-2. local state
- コンポーネント内部の状態
6-2-3.ライフサイクル
- コンポーネントはライフサクルを持つ
- 画面描画時に初期化/マウント/レンダリングされ、何らかの処理が行われて再レンダリングされたりして、最後にアンマウントされる
- Reactが提供しているメソッド(ライフサイクルメソッド)を使うことで、特定のライフサイクルの時点で処理を実行することができる
- componentDidMount() や componentWillUnmount() など
# フェーズ 説明 1 Mounting コンポーネントが生成されてDOMノードに挿入されるフェーズ 2 Updating 変更を検知してコンポーネントが再レンダリングされるフェーズ 3 Unmounting コンポーネントがDOMノードから削除されるフェーズ 6-3. Presentational ComponentとCotainer Component
- 以下のように2つの役割に分けることで、マークアップとそれに対するロジックを分離できる&再利用性が上がる
- 関数コンポーネントでPresentational Componentを作り、Cotainer Componentでimportしてhooksなどで機能追加する
- ref: Presentational and Container Components
6-3-1. Presentational Component
- いわゆコンポーネント
- 見た目(マークアップ)を担当する(ので、ロジックとは切り離されてる)
6-3-2. Cotainer Component
- いわゆコンテナ
- ロジックを持つ(のでマークアップは担当しない)
6-4. Hooks
- 関数コンポーネントとReactの機能(local state, lifecycleなど)を繋げる
- 従来クラスコンポーネントでやってたことを、関数コンポーネントを使ってやるためのもの
6-4-1 State Hook
- local stateに相当するもの
useState
の第一引数に初期値を入れると、戻り値としてstate変数
とセッター関数
を返すimport React, { FC, useState} from 'react'; const App: FC = () => { const [hoge, setHoge] = useState('fuga'); setHoge('FUGA'); ... }6-4-2 Effect Hook
- クラスコンポーネントでいうライフサイクルメソッドに相当する
- 第一引数に引数なしの関数を持つ
- 第二引数は配列で指定する
- 配列の中に変数を入れておくと、再レンダリング時にその変数の値が変更されると第一引数の関数が実行される
- 変数の値に変更がなければ実行されない
- 引数を省略した場合には毎回のレンダリングの際に中の処理が実行される
- 空配列を渡すと、初回のレンダリングでのみ実行される
import React, { FC, useEffect, useState} from 'react'; const App: FC = () => { const [hoge, setHoge] = useState('fuga'); useEffect(() => { // コンポーネントがレンダリングされた直後に実行される。 //componentDidMount(), componentDidUpdate()に相当する // hogeの値が変わったらdoSomethingHere()は再度実行される doSomethingHere(); // 戻り値を設定すると、コンポーネントのアンマウントの直後に実行される。 // componentWillUnmount()に相当する return cleanUpSomething(); }, [hoge]); ... }6-4-3. Custom Hook
- 独自でHookを定義する
- 関数名は
useXxxx
と、use
を先頭につけるのが一般的6-4-4 そのほかのHooks
いろいろある
- useReducer()
- useRef()
- useMemo()
- etc6-5. Routing
- ルーティングの適応単位はコンポーネント
- React Routerがデファクト
7. Redux
- Reactはコンポーネントを組み合わせてアプリケーションを作成するが、コンポーネントは状態を持つものと持たないものがある
- 実際のアプリケーションでは状態を保持したままコンポーネントを跨ぎたい場合がある
- ログイン状態など
7-1. Flux
- Reactで状態を保持するための考え方
- この考えをもとに、Reactを使ったフレームワークがいろいろでてる
7-2 Redux
- Fluxのフレームワーク競争の中で勝ち残って今やデファクト
- 以下の3つの原則を持つ
7-2-1. Single source of truth(信頼できる唯一の情報源)
- アプリケーションの状態がただ一つのStoreオブジェクトによるツリー構造で表現されている
- 複数のStoreが存在するとStore間でのデータのやり取りが複雑になる
7-2-2. State is read-only(状態は読み取り専用)
- ViewやイベントのコールバックはStoreの状態を直接書き換えることはできない
- Storeの変更には必ずActionを発行する必要がある
- 変更がActionに集約されることで思わぬ変更を避けられる
- ReduxのActionはプレーンオブジェクトなのでデバッグも容易
7-2-3. Changes are made with pure functions(変更は純粋関数にて行われる)
- Reducerという純粋関数によって状態を変更する
- Reducerは古い状態を引数にとり、新しい状態を返す関数
- viewから発行されたActionはDispatcherで割り振られたReducerに渡される
- DispatcherはActionを振り分けるだけ
- ReducerはそのActionと現在のStateを受け取って新しいStateを返す
7-3. ざっくり使い方
- Redux公式のチュートリアルを一通りやって雰囲気を掴んだら、業務のプロジェクトのソースコード上で処理を追いかけるのが良さそう
7-4. Flux Standard Action
- Reduxには原則はあるが規約がない
[Flux Standard Action](https://github.com/redux-utilities/flux-standard-action)
とい規約が有名で、デファクト的なポジジョンだそう- ただ、割と煩雑なので、 typesafe-actions や Typescript FSなどの外部ライブラリが使われることも
- 一方、ライブラリのメンテが止まってたりもするので、どこまで使うかは要検討
- Flux Standard Actionの思想を取り入れつつ、ベストエフォートでやるのが良さそう
8. Redux-Saga
- 外部とのAPI通信を行うことで副作用を持つ非同期処理を扱う必要がある
- 対処としては2つある
8-1. Redux ThunkとRedux Saga
8-1-1. Redux Thunk
長所
- Action Creatorの中で副作用を伴う処理を扱うことができるようになる
- 概念が理解しやすい
- コードの記述量が比較的少ない
短所
- Action Creatorが本来のReduxの思想と大きくかけ離れる
- Action Creatorでなんでもできてしまうので、入れ子のコールバック地獄化しやすく、処理も煩雑になりやすい
8-1-2. Redux Saga
- 実行させたい副作用を伴う処理を「タスク」として登録する
- Actionが発行されるとDispatcherはStoreだけでなく、SagaのタスクにもActionを渡す
- タスクはActionが受け渡されたらタスクの中で定義した処理を実行し、実行結果をAction Creatorに渡す
- 副作用をReduxのエコシステムの外で管理できる
- 処理の進行状況を管理する状態(Action Typeに開始/成功/失敗のステータスを持たせる)と、各サービスが返すドメインのデータ(Store Stateのようなもの)を持つ
長所
- Reduxの仕組みの外で副作用を扱うことで、Reduxの形を崩さずに導入できる
- 非同期処理を動機的に記述できるのでコールバック地獄を防げる
短所
- 学習コストが高い
- コードの記述量が多め
8-2-1. Sagaで使われる関数
詳細はAPI Referenceに載ってる
- select
- Store Stateから必要なデータを取得
- put
- Action Creatorを実行してActionをdispatchする
- take
- 特定のActionを待つ
- call
- 外部の非同期処理関数をコールする
- fork
- 自分とは別のスレッドを起動し、タスクを実行し、Taskオブジェクトを返す
- join
- forkの戻り値のTaskオブジェクトを指定し、タスク完了を待つ
9. 所感
- 流れが早いので具体的なコードの記述はこれからもどんどん変わりそうなので、フロント専任でない場合にはそこまで深入りしなくても、必要に応じて調べれば良さそう
- 一方で、思想や背景を理解していれば、流れの速いフロントエンド開発にもキャッチアップできそう
10. 参考
- りあクト! TypeScriptで始めるつらくないReact開発 第2版
- TSとReactをかなり分かりやすくまとめてあって個人的には超おすすめ
- フック API リファレンス
- redux.js.org
- Redux入門 1日目 Reduxとは(公式ドキュメント和訳)
- 投稿日:2020-03-29T13:09:49+09:00
DeepLearning を使った手書き文字採点の Web サービスをリリースした
ブラウザ上で文字を手書きすると字の読みやすさを AI が自動採点してくれる、という web サービスを趣味で個人開発し、 2 週間ほど前にリリースしました
コア技術は、DeepLearning の画像認識と Preact によるフロントエンド実装です。
この記事では、開発したアプリに関して、サービス概要・技術詳細・所感を記載いたします。サービス概要
機能紹介
ページ数が少なく軽量なため、機能に関してはアプリを見ていただいたほうがおそらく早いのですが、使用した技術の概要も含めた機能紹介をスクリーンショットと共にご説明します。
トップページからの手書き文字採点
- トップページでは文字が書ける枠が表示され、文字を書くことができます。
- HTML5 の Canvas で文字を書けるようにしています。
- 文字を書いて、「採点ボタン」をクリックすると候補が表示されます。
- Canvas に書かれたものを PNG ファイルに画像化し、サーバ上に送信
- サーバ上では受信した画像に対して、機械学習の推論を実行
- 推論結果の、書かれた確率が高いものをクライアントに返信
- 書いた文字を選択すると、採点結果(100点満点)が表示されます。
- 推論結果の確率の値を元に、直感的な点数になるような計算式を適用し点数を算出
- 丁寧で読みやすい字を書いたほうが高得点が出る可能性が高いです。
- 書く線の太さを変えることもできます(線の太さによって採点結果が大いに変わることもあります)
これまで書いた文字の採点実績表示
- これまで書いた文字(採点した文字)の一覧を見ることができます。
- 書いた文字一覧はサーバ上にログとして保管している
- cookie をキーに書いた文字一覧を取得
- 別の端末・ブラウザを使用した場合や、シークレットブラウザを使っている場合は、cookie が変わるため書いた文字の履歴は残りません
- 他のユーザには書いた文字自体・書いた文字の一覧は一切見えないようになっています。
高得点の例の表示
- なかなか高い得点がだせない場合、どういう文字を書くと高得点が出るのかという例を表示することができます。
- 実際にサーバ上で実行される推論処理によって高得点が出る例を、学習データ、テストデータから抽出し、推論された際の点数と共に画像を表示しています。
開発の背景・目的
今回の開発の目的は 2 つあります。
- 手書き文字を採点してくれるアプリがあったら楽しそう
- DeepLearning を応用した Web アプリケーションを作りたかった
開発してみた結果としては、とりあえずどっちも満たしたので満足です。
今回開発したものと同等の機能を提供しているようなサービスは現時点ではまだ存在しないのでは…と思っています。(いや、そこまで他のサービスのことは調査していません…)こだわりポイント
軽快な動作
基本的にスマホやタブレットで使用されることを前提としているため、初期表示などをサクサクと動くように頑張りました。
具体的には、なるべくフロントエンドのファイル群が軽量になるようにする等の対応をしています。採点可能文字の種類数の担保
採点対象は、日本語において多く用いられる、ひらがな・カタカナ・漢字・ローマ字・数字の合計 3175 文字で、日本で日常的に使われている文字をほぼ網羅するようにしました1。
数字だけであれば MNIST のデータセットを使えれば容易に用意できるし、英数字だけでも様々なデータセットがありますが、ひらがな・カタカナ・漢字を含んだ上で英数字も混ぜ合わせ、さらに英数字の大文字/小文字も含めたデータセットは若干作るのが面倒でした。
技術詳細
アーキテクチャ概要
使用した技術・言語・ツール
フロントエンド
- Preact
- JavaScript / HTML / CSS
Preact は React の軽量版みたいなフレームワークです。React の主要な API をほぼそのまま使用できつつ、めちゃくちゃ軽量に実装されています2。
僕は React を使い倒しているというほどでもないので Preact で困ることは全くありませんでした3。バックエンド
- Nginx
- uWSGI
- Flask
- Python3
機械学習部分は Python3 + Keras を使用しているため、リアルタイムで推論処理を実行する必要のあるバックエンドは親和性と利便性から Python を選定しています。
Flask は単純に軽快で使いやすいという理由で選定していますが、Python 上のアプリケーションサーバでも非同期処理を行いたいことがあるので、FastAPI へのリプレースも考えています。
Nginx, uWSGI の選定理由は特にありませんが、基本的に Flask のようなアプリケーションサーバは、本番環境の Web のフロントエンドの動作には最適化されていないため、それらの用途に最適なものかつ Flask と親和性が高いものを使用しているというだけです。機械学習部分
- Keras
- Python3
学習・推論ともにディープラーニングフレームワークの Keras を使用しています。
実行環境などのインフラ
- Google Cloud Platform (GCP)
- Google Domains
- Docker
全般的に GCP を使っています(GCP 以外のクラウドサービスは使用していません)。
バックエンドのサーバはGoogle Compute Engine
、画像ファイルなどの保存場所はGoogle Cloud Storage
、ロードバランサーとしてGoogle Cloud Load Balancing
を使っています。
Google Cloud Load Balancing
は、基本的な使用料だけで月額約 2700 円(2020/03/23 現在)とわりとお高いためかなり悩んだのですが、証明書の管理を自動でやってくれたり、万が一アクセスが多くなった場合にインスタンスを増やせる安心感が大きかったため、いったん使ってみることにしています。デプロイは Docker コンテナをリリース単位として、下記の流れで実行します。
- 開発環境でフロントエンドをビルド
- ビルド済み・実行可能なコードを含んだコンテナイメージを作成
- 作成したコンテナイメージを、プライベートのコンテナレジストリにプッシュ
- プッシュしたコンテナイメージをベースにし、インスタンステンプレートを作成
- インスタンステンプレートを元に、インスタンスグループを更新
カタカナが多い…w
学習データに関してはいまのところアップデートは考えていないため、すでに学習済のモデルを使用しています。その他お世話になったツール・サービスなど
Inkscape
Adobe Illustrator とほぼ同等の機能が提供されているフリーのソフトです。
ロゴやアイコンの作成で用いました。ファビコン favicon.icoを作ろう!
色々なサイズ・形式に対応した favicon の一括作成の際、とても便利です。EZGIF.COM - Animated GIF editor and GIF maker
アプリのデモ動画を Twitter に Qiita 上にアップロードしたい際に GIF 化するのに便利なサービスです。
いくつか同様のサービスを試していますが、変換の柔軟性や処理時間などより、個人的にはこのサービスが一番使い勝手がよいと思いました。Wikitionary
漢字の一覧表を作成するために、学校で習う学年などで分類したかったため、データを収集しました。
読みや総画数・部首などのデータも取得済なのですが、現状それらを使っての表示分類・検索機能は実装していません。機械学習モデルの学習に使用したデータ
ETL Character Database(ETL CDB)
手書き文字(+少量の印刷文字)画像データのデータセットです。
日本で使われている文字 3200 種類ほどからなるデータセットで、画像データとしては 111万5065枚 あります。
文字の種類はひらがな・カタカナ・漢字・英数字・記号からなりますが、ラテンアルファベット(ローマ字)の小文字は含んでいません。
また、ETLCDB は下記の点から若干扱いづらいデータセットです。
- 各データセットは、モダンな JSON や XML などではなく、固定長の決められたフォーマットを持つバイナリデータ
- 内部的に保持されている文字コードが JIX X 0201 だったり CO-59(六社協定新聞社用文字コード) というナゾイ文字コード
ETLCDB 全データセットの画像を取り出す Python スクリプトを作成して公開しています。データセット上のバイナリデータから生の画像データを取得して、Unicode のコードポイントラベルとして扱えるよう、各コードをディレクトリとして PNG で保存します。
データセットは無料で使用可能ですが、商用使用を目的とする場合は条件についてお問い合わせください
と記載がありますので、ご注意くださいThe EMNIST Datset
NIST(アメリカ国立標準技術研究所)の提供している手書き文字のデータセットです。
ローマ字の大文字小文字と数字を含む全 62 種の文字に関して 81万4255枚の画像があります。
なお、ディープラーニングのチュートリアルでよく登場する MNIST は、このデータセットのサブセットです。
上述の ETL CDB は多くの文字を含んでいますが、ローマ字の小文字のデータがありません。しかし、英数字全ても採点対象としてどうしても含めたかったため、すべてのローマ字を含んだデータセットを学習データとして追加しました。ソースコードなど
基本的なソースは GitHub のレポジトリにて公開しています。
ただし、学習済のモデルなどアプリケーションの実行に必要な一部のファイルは GitHub 上には置いていないため、実行環境をそのまま開発環境として再現させることはできません。所感
アプリで遊んでいて感じたこと
さて、突然関係のない話題ですが、次の文章の意味を解読できるでしょうか。
(※投稿時からちょっとだけ変更しました)「卜口 卜 力二 力工夕」
※再度表示するので、じっくり見て読んでいただくと分かるかもしれません。
「卜口 卜 力二 力工夕」すぐに正解が見えないように、無駄な画像を貼り申し訳ございません
そして画像は完全なる引掛けで「とろ と かに かえた」のカタカナ表記だと思われた方は一文字もあっていません…
正解は下記の通りで、実は文章として全く成り立たない文字の羅列に過ぎません。卜:水卜アナの「卜」
口:口内炎、口角の「口」
力:力士、チカラの「力」
二:二子玉川、二項分布の「二」
工:工事中、工場、斎藤工さんの「工」
夕:夕方、夕刊、夕食の「夕」
(コピペして Google 検索にかけると全て漢字であることが分かります。)おそらく全ての文字を正しく認識できた方は多くないのではないかと思います。
もしそうだとすると「ちゃんとした活字ならば、どんな文字でも人間には認識可能で読み分けることができる」というのは真ではない、ということがわかります4。
例えば「工」という表示に対して読みあげて下さい、と言われた際に、何もヒントがなければ、「工事の『工』かカタカナの「エ」(え)のどちらか」としか答えられないかと思います5。つまり、文字単体での判別はとても難しい場合がある、ということです。また、本節の冒頭の「次の文章の意味を解読できるでしょうか」が「次の文字列の各文字はそれぞれ何が書いてあるでしょうか」 という問いであればまた見方がまた違った方も多いと思います。
よく言われることではありますが、機械学習、特に深層学習分野を勉強したり試したりしていると、人間の認識は文脈への依存が多分にあるということを強く感じることが多々あります。今回の文字認識はその代表的な例で、我々は文字を読む時、その字単体の図形のみを認識して判別しているわけではなく、「周辺にはどのような文字が書かれているのか」「どういった単語や形態素の要素となるか」「そこにはどういった単語が書かれていることが自然か」という「文脈と呼べる情報」に大いに依存している、ということが分かります。
今回開発したアプリでは、文脈情報を全く使わずに画像認識をして採点しているため、高得点が出にくい文字が少なからず存在します。その代表例が上記の5文字ですが、その他にも 0 (数字のゼロ)と O (ラテンアルファベットのオー)などのように、文字単体での認識が難しいために高得点が出にくい文字はそれなりの数があります。
機械学習関連の開発では「人間はどうやって認識しているのか」ということを考えつつ取り組むことが多いのですが、それを純粋に掘り下げることはとても興味深く、人間の脳の出来の良さに驚くことがしばしばあります。また「現時点での AI には何が出来て何が出来ないのか」というのも、そういった側面から考えるとヒントのようなものもたくさんあると思います。
なお個人的には、単語のみではない文脈情報も自然言語処理では解析可能なレベルになってきていますし、マルチモーダルの深層学習なども今後数年のうちに大きく発展するのではないかと思いますので、文脈を読んだ上での精度の高い推論も AI には自然にできるようになっていくと考えています。それらの進歩により、いわゆる「気が利く AI」も遠からぬ未来に実現されていくと思っています。
個人開発におけるリリースの難しさ
個人開発において「どこまで作ったらリリースするべきか」という判断は、業務のように契約等で納期が決まっている場合と異なり、とても難しいものです。
業務にせよ趣味にせよ、ソフトウェア開発をがっつりやったことのある方なら分かるかと思いますが、開発途中では、直さなければいけないバグ以外にも、改良点やあった方がいいだろうなと思う機能が無数に出てきます。今回の個人開発でも、追加した機能や改良した方がよい点がいまだにたくさんあります…そのような中で「どこまでできた時点でリリースするか」というのはとても決めがたいものです。そのような状況で最も大切なのは「絶対に世に出す」という強い意志ではないかと思います。
Facebook 社内の標語として使われるらしい "Done is better than perfect" は、「ソフトウェアが完璧な状態になることなんてない。だからこそスピード感を持って世に出し、少しずつずっと改良し続けていくことが大切だ」という意図が背景にあるそうです6。
この言葉に代表されるように、リリースの際にはある程度の見切りが必要です。
が、その一方、一度離れたユーザは戻ってくるまでに長い時間がかかるとよく言われますし、品質が低いものを気軽に世に出すというのも、技術者としてのプライドなども邪魔してなかなか難しいものです。それらを踏まえると結局のところ、個人開発においても リリースは日付を決めてしまう(あらかじめリリース日を決めて、なにがなんでもそこにリリースするようにするようにするのがベストなのではないかと近頃は考えています。(それ以外にもよい方法があるかもしれません…もし知っていたら本当に知りたいので教えていただければと思います…!)
この「リリースタイミングの難しさ」に関しては他にももっと色々考えている・思うこともあるので、そのうち別途ポエムとして記事にまとめたいなーと思っています。
最後に
東京では外出の自粛要請の上に大雪と、家からなかなか出にくい環境のさなかですし、もしよければちょっとだけでも遊んで頂けると幸いです。
まだまだ不完全な点も多々あり機能的にも不十分かもしれませんが、ぜひぜひ楽しんでいただければと思います!なお最近 Twitter も細々とやり始めていますので、もしよければTwitterアカウントのフォローなどお願いします m(_ _)m
- 投稿日:2020-03-29T13:05:57+09:00
【React】超カンタンなSPAサイトを一通り作成してみて
経緯
フロントエンドエンジニアとしてキャリアチェンジするにあたって、React.jsでのポートフォリオ作成に取り組む過程で、中でも一番興味があり、尚且つはじめに取り組むのに比較的カンタンなSPAサイトを作成してみました。
注意
あくまで、初学者が自分用のメモ、もしくは同じく同等のレベルの初学者方の参考に少しでもなればと思い投稿いたしますので、中級者異常の方にとってはあまり参考にならない記事となっておりますので、ご了承ください。
今回のディレクトリ構成
directory
ーApp.js
ーindex.js
ーcomponents
ーHeader
ーHeader.jsx
ーTop
ーTop.jsx
ーimg
ーreact.png
あくまで、わかりやすいように今回の記事で解説する部分のみ表示しています。個人的につまづいたポイント
- 各コンポーネント同士の連携
- 画像の読み込み
- .jsxはHTMLではなくてXML
- 階層構造
1. 各コンポーネント同士の連携
今回のポートフォリオを作成するにあたって、表示したいページごと単位だけではなくてHeaderやFooterもコンポーネントで作成してみました。
その際に、一般的なサイトと同様にHeader部分にナビゲーションを入れて、各ページに遷移することを試みました。
それにあたって、Header.jsxに非同期処理でのページ遷移を行うためのRouterタグ等を、以下のように予め用意しておきます。Header.jsximport React from 'react'; import { Route, Link } from "react-router-dom"; import Top from '../Top/Top'; function Header() { return ( <div> <Router> <nav> <ul> <li><Link to="/">top</Link></li> </ul> </nav> <Route path="/" exact component={Top} /> </Router> </div> ); } export default Header;そして、このHeaderコンポーネントを受けるApp.jsには以下のように読み込みを行います。
ただ、私が個人的に勘違いしていたポイントとして、このHeaderコンポーネントを受けるApp.jsにも以下のようなimportが必要と思い混んでいたため、、import { HashRouter as Router, Route, Link } from "react-router-dom";このようなエラーメッセージが出てしまいました。
既にHeader.jsxでimportされているため、重複してしまっていますよというメッセージですね。
App.jsではこのようにシンプルで問題ありません。
App.jsimport React from 'react'; import './App.css'; import Header from './components/Header/Header'; function App() { return ( <div> <Header /> </div> ); } export default App;2. 画像の読み込み
画像の読み込みなんて、そんなところでつまずくわけないだろ笑って言われちゃうかもしれませんが、ここで個人的にはだいぶ時間を取られてしまいました。
その要因としては画像もimportしなければいけないということ。
入力方法としては以下の通り.import Src from '../../img/react.png' . . . <img className="img" src={Src} alt="Src" />そして、import後にはimgタグでこのように入力すれば問題ありません。
3. .jsxはHTMLではなくてXML
ここが一番のカルチャーショックだったのですが、これまでHTMLでのコーディングしか経験がなかったため、XMLでのコーディングの作法とは若干異なる部分でやられました。
具体的にはbrやimgのような閉じタグのないようなものです。//HTMLでの書き方 <br> <img> //XMLでの書き方 <br/> <img/> //閉じタグがないとこんな感じのエラーが出てしまいすよ Parsing error: Expected corresponding JSX closing tag for <img>とまあ、こんなことかよ!
と思われるようなことでも、XMLのカルチャーがない人間にとっては全くの想定外なので気をつけたいですね。4. 階層構造
これはシンプルに考え方や感覚、経験の問題なのですが、意外と気がつかなかったりもするので気をつけましょう。
参考記事を添付いたします。
(http://w-d-l.net/html__course__high_level_link/)最後に
ここまで、超初歩的なミスをまとめた記事を作成しましたが、自分にとっての振り返りとしてのみではなく、これから勉強される方にとっても何かしらの参考になれば幸いでございます。
- 投稿日:2020-03-29T10:21:04+09:00
React触って1年たったが学び直した
Reactをおおよそ一年くらい触って複雑なUI組む時にjQueryで書くより楽だな。。みたいな感覚は理解できたし開発はできているものの例えばRailsで開発してる時に比べて
「こんな書き方あったのか。。」とか、
「ReactのコアAPIでこんな機能あったの。。」とか
「ここまでがReactの機能でここまでがNextでここまでがreact-domなのか。。」
とかの「あっ、知らなかった。。」って場面が多いとは感じていた。
調べるコストもかかってるし雰囲気でやっちゃってる部分も多かったので公式のドキュメント、チュートリアル、オンライン講座、技術ブログ等で教材を漁って学び直したのでやったことを書いてみる。
reactjs.org
reactで調べるとトップに出てくるサイト。以前もここでチュートリアルやったけど曖昧だった部分(hooksとかrefとか)を読み直した。
[https://ja.reactjs.org/docs/hello-world.html:embed:cite]
Next.jsのチュートリアル
[https://nextjs.org/learn/basics/getting-started:embed:cite]
NextがReactから追加でどの程度拡張されてるか曖昧だったのでチュートリアルやってみた。英語だがそんなに迷うところはない。
問題を解くたびにポイントが加算されたりなかなか凝ったサイトだった。ルーティングとかパフォーマンス最適化とかいちいちライブラリインストールしたり設定ファイル書き直さなくてもよくやっぱり楽なんだなと再確認。
Udemy
オンライン講座でReactの講座検索してもろもろやってみた。なんかこの辺りアフリエイトっぽくなってるけどそうではない。
「フロントエンドエンジニアのためのReact・Redux実践入門」
https://www.udemy.com/course/react-application-development/learn/
とりあえずReact、Reduxを学び直すため、これを受講「React Hooks 入門 - Hooksと Redux を組み合わせて最新のフロントエンド状態管理手法を習得しよう!」
今までクラスコンポーネントの開発ばかりなので最近話題のHooksを学ぶために受講した。これからはこれがスタンダードになるらしいのでキャッチアップしていかないと。
https://www.udemy.com/course/react-hooks-101/learn/
フロントエンドエンジニアのためのGraphQL with React 入門
別にGraphQLは仕事で使ってないんだけど興味がてら受講した。慣れたらJSONのAPIそのまま扱うよりも快適なんだろうなと。
https://www.udemy.com/course/graphql-with-react/
モジュールバンドラー webpack を1日で習得!フルスクラッチでインストールからカスタマイズまでの手順を理解…モジュールバンドラー webpack を1日で習得!フルスクラッチでインストールからカスタマイズまでの手順を理解する
webpackがjsやcssをまとめてパフォーマンスを改善してくれるもの。。とは知ってはいたものの設定ファイルの内容とかloader、pluginとか詳しいことは知らなかったので受講した。今まで曖昧だった部分を補強してくれて良い感じ。
https://www.udemy.com/course/webpack-crash-course/
「React Testing with Jest and Enzyme」
テストに関しては日本語でまとまった教材がなかったので英語で受講した。JestとEnzymeを使ったReactのテスト。
フロントエンドのテストは書き方だけではなくケースの作成とか考え方も曖昧なので引き続き学びたい。https://www.udemy.com/course/react-testing-with-jest-and-enzyme/
Qiita
QiitaでReactのタグをフォローした。
Podcast
React PodcastというReactのPodcastがあったので通勤時に聞いてみた。英語なので100%はわからないけど歩きながら情報収集できるのでもう少し粘って聞いてみようと思う。
書籍
教材としては書籍もいいものがないか調べたけど気のせいかVueに比べて充実していない気がした。Vueはなぜか日本人のコミュニティ強めって聞いたけどなんかそういうのあるんだろうか。
これから
引き続き情報収集しつつ何かアプリ作ったりとかReact関連のOSSのGithubを読んでいく予定