20200329のReactに関する記事は16件です。

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、アプリ概要

その日その日にやったことをそれぞれカテゴライズしてカウントしていくというシンプルなアプリ
(ログイン機能・データベース連携あり)
<カテゴリ>
 ①自己投資した時間
 ②浪費した時間
 ③単純な癒しの時間

デモイメージ(画質が粗くすみません)
React-App-Google-Chrome-2020-03-29-17-13-05.gif

3、React/Reduxの大まかな流れ

React/Reduxは基本的に下図の流れでデータの受け渡しが進む。
①Reactで作ったComponentでデータの入力 →②ActionCreatorによって入力内容に応じたActionが発動→③Reducerが受け取ったaction Typeに応じてstoreのstateの値が更新される→④更新されたstateの値がComponentに表示される
※厳密にはこんな単純な説明ではないと思いますが、ざっくりこのように理解しています。
この際データベース(今回の場合はFirebase)とのやり取りは、ActionCreatorで行うようにする

image.png

4、アプリの構成

 ログイン画面、サインアップ画面、メイン画面(データ入力・表示)を作成
 React-routerにより画面に表示されるComponentを切り替えて遷移

App.js
function 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を埋め込み、カテゴリによってリストの表示を絞り込む機能を追加

image.png

Main.js
const Main = () => {
  return (
    <Box>
      <Counter />
      <Form />
      <ItemList />
    </Box>
  );
};

6、各Componentの構成 (1)Form Component

①:React Hooks を使ってstate(状態)の値を維持・更新できるようにする
 useStateのカッコ内は初期値となる

Form.jsx
const Form = props => {
  const [text, setText] = useState("");
  const [hour, setHour] = useState(0);
  const [status, setStatus] = useState("投資");

②:後に出てくる入力ボタンを押した際に発動する関数
 下で入力するstatus,text,hourの値をaddItem関数を使いprops経由で受け渡し、
 setXXXで画面上の値をリセットする

Form.jsx
  const onClickButton = () => {
    if ((text, hour, status)) {
      props.addItem(status, text, hour);

      setText("");
      setHour(0);
      setStatus("投資");
    }
  };

③:入力フォーム
 ここで入力・選択したstatus,text,hourの値が、ボタンを押した際にデータとして受け渡される

Form.jsx
  return (
    <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.jsx
const 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.jsx
    return (
      <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.jsx
  return (
    <Container maxWidth="sm">
      <div className="item-box">
        <h4>積み上げ履歴</h4>
        <Filter />
        <ul className="itemContainer">{itemLists}</ul>
      </div>
    </Container>
  );
};

④:Filter Component
 "全て"、"投資"、"浪費"、"癒し"のボタンを設定
 ボタンを押すと、それぞれshowAll,showInvest,showExpense,showHealing関数が実行される

Filter.jsx
const 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.jsx
const 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.js
export 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.js
export 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.js
export 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.js
export 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を使って一本化する

rootReducer
import { 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.js
import { 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アプリを作る
テストの方法を覚える
 ・テストコードを理解することで、より効率的な開発を行えるようになる

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

React Tutorial 中

node.js インストール後
image.png

node.jsがインストールされたら自動的にnpmも一緒にセットされていることを確認する
image.png

それと、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/32

npx セット確認
image.png

React自体がfacebookが作ったもの
aribnbはもうreactを利用しない(2019~
https://softwareengineeringdaily.com/2018/09/24/show-summary-react-native-at-airbnb/
React-> spotify/ Netflix

yarn... First Look at the New Package Manager for Javasciprt?

image.png

Happy Hacking!

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

【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になると逆になるのはなぜなのか・・・

今はそういうものと覚えておくべきですかね。

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

Concurrent Mode時代のReact設計論 (3) SuspenseやuseTransitionが何を解決するか

この記事は「Concurrent Mode時代のReact設計論」シリーズの3番目の記事です。

シリーズ一覧

SuspenseuseTransitionが何を解決するか

前回までは、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のデータをロード中の段階」は、PageAisLoadingステートがtrueになり、RootfetchUsers()の結果を待っている段階として現れます。そして、「ロードが終わって画面Bをレンダリングする段階」はRootsetStateでステートを変更して画面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>
  );
};

isLoadinguseStateで宣言するのをやめてuseTransitionを使うようになりました。画面Bへの遷移(goToPageB())をstartTransitionで囲むことで、遷移時にサスペンドが発生したらボタンにLoadinng...が表示されるという制御がされています。

目ざとい方は、この設計は微妙だと思ったかもしれません。というのも、startTransitionは中でステートを更新することで意味を発揮する関数なのに、goToPageBという関数は「画面Bに遷移する」という抽象化された意味を持たされており、中でステートの更新が行われることが明らかではありません。今回はgoToPageBの実態がsetState({ ... })なので偶々うまくいっていますが、startTransitionsetStageという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の恩恵をより受けるためにどのような設計がベストかについて考えていきます。

次の記事: 鋭意執筆中です。

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

Concurrent Mode時代のReact設計論 (2) useTransitionを活用する

この記事は「Concurrent Mode時代のReact設計論」シリーズの2番目の記事です。

シリーズ一覧

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に渡されたコールバック関数は即座に呼び出されます。

この実装では、ボタンを押すと以下のスクリーンショットのような挙動となります。

screenshots-2.png

これを理解するために。useTransitionの挙動を簡単に説明します。startTransitionの内部で行われたステートの更新がサスペンドを発生させた場合、変更後ではなく変更前のステートがレンダリングされます。ただし、このとき変更前のステートではuseTransitionが返すisLoadingtrueになっています。投げられたPromiseが解決された場合は変更後のステートで再レンダリングされます。

useTransitionを使わない場合との違いはサスペンド中に現れます。useTransitionを使わない場合はSuspenseによるフォールバックが表示されますが、useTransitionを使う場合はフォールバックは表示されず、代わりにステート更新前の状態が(isLoadingtrueで)レンダリングされるのです。

useTransitionにオプションとして渡したtimeoutMsは、この「isLoadingtrueの状態」の最大持続時間を表します。この時間が過ぎてもPromiseが解決されなかった場合、諦めて変更後のステートがレンダリングされます。ただし、まだPromiseが解決されていないのでSuspenseによりフォールバックが表示されます。

ボタンがクリックされてからの流れは次のようになります。

  1. 初期状態では、usersFetcher=undefined, isLoading=falseである。(上のスクリーンショットの左の状態)
  2. startTransition内でsetUsersFetcherが呼ばれ、usersFetcherステートが更新される。(このときnew Fetcherで作られたオブジェクトをFとする)
  3. useTransitionの効果ににより、まずusersFetcher=undefined, isLoading=trueの状態でContainerがレンダリングされ、DOMに反映される。(上のスクリーンショットの真ん中の状態)
  4. 次に、新しいステート(usersFetcher=F, isLoading=false)でContainerがレンダリングされる。これはUserListのレンダリングに繋がり、UserListのレンダリングはサスペンドする。useTransitionの効果により、この状態はDOMに反映されない。
  5. Fが持つPromiseが解決されると、新しいステート(usersFetcher=F, isLoading=false)でContainerが再レンダリングされる。今回はサスペンドが発生せずにレンダリングが完了し、この状態がDOMに反映される。(スクリーンショットの右の状態)

ポイントは、useTransition内でステートの更新を行なった場合、新しいステートよりも「元のステート+isLoading=true」のレンダリングが優先されるということです。これは、isLoading=trueの状態でユーザーへのフィードバックを表すことを意図しているためです。ユーザーへのフィードバックは最優先で画面に反映されるべきであるため、これが最初に処理されます。

ちなみに、startTransitionの中と外の両方でステートの更新を行うことができます。この場合、startTransitionの外で行なった更新は3の段階で反映されています(もちろん5の段階にも反映されます)。

また、timeoutMsで設定した時間を超えない限り、Suspensefallbackで指定した内容は表示されなくなります。useTransitionをきちんと使っている限りは、Suspensefallbackはいわば最終防衛ラインのような扱いになり、高頻度でユーザーが目にするものではなくなります。

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が何を解決するか

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

Concurrent Mode時代のReact設計論 (2) `useTransition`を活用する

この記事は「Concurrent Mode時代のReact設計論」シリーズの2番目の記事です。

シリーズ一覧

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に渡されたコールバック関数は即座に呼び出されます。

この実装では、ボタンを押すと以下のスクリーンショットのような挙動となります。

screenshots-2.png

これを理解するために。useTransitionの挙動を簡単に説明します。startTransitionの内部で行われたステートの更新がサスペンドを発生させた場合、変更後ではなく変更前のステートがレンダリングされます。ただし、このとき変更前のステートではuseTransitionが返すisLoadingtrueになっています。投げられたPromiseが解決された場合は変更後のステートで再レンダリングされます。

useTransitionを使わない場合との違いはサスペンド中に現れます。useTransitionを使わない場合はSuspenseによるフォールバックが表示されますが、useTransitionを使う場合はフォールバックは表示されず、代わりにステート更新前の状態が(isLoadingtrueで)レンダリングされるのです。

useTransitionにオプションとして渡したtimeoutMsは、この「isLoadingtrueの状態」の最大持続時間を表します。この時間が過ぎてもPromiseが解決されなかった場合、諦めて変更後のステートがレンダリングされます。ただし、まだPromiseが解決されていないのでSuspenseによりフォールバックが表示されます。

ボタンがクリックされてからの流れは次のようになります。

  1. 初期状態では、usersFetcher=undefined, isLoading=falseである。(上のスクリーンショットの左の状態)
  2. startTransition内でsetUsersFetcherが呼ばれ、usersFetcherステートが更新される。(このときnew Fetcherで作られたオブジェクトをFとする)
  3. useTransitionの効果ににより、まずusersFetcher=undefined, isLoading=trueの状態でContainerがレンダリングされ、DOMに反映される。(上のスクリーンショットの真ん中の状態)
  4. 次に、新しいステート(usersFetcher=F, isLoading=false)でContainerがレンダリングされる。これはUserListのレンダリングに繋がり、UserListのレンダリングはサスペンドする。useTransitionの効果により、この状態はDOMに反映されない。
  5. Fが持つPromiseが解決されると、新しいステート(usersFetcher=F, isLoading=false)でContainerが再レンダリングされる。今回はサスペンドが発生せずにレンダリングが完了し、この状態がDOMに反映される。(スクリーンショットの右の状態)

ポイントは、useTransition内でステートの更新を行なった場合、新しいステートよりも「元のステート+isLoading=true」のレンダリングが優先されるということです。これは、isLoading=trueの状態でユーザーへのフィードバックを表すことを意図しているためです。ユーザーへのフィードバックは最優先で画面に反映されるべきであるため、これが最初に処理されます。

ちなみに、startTransitionの中と外の両方でステートの更新を行うことができます。この場合、startTransitionの外で行なった更新は3の段階で反映されています(もちろん5の段階にも反映されます)。

また、timeoutMsで設定した時間を超えない限り、Suspensefallbackで指定した内容は表示されなくなります。useTransitionをきちんと使っている限りは、Suspensefallbackはいわば最終防衛ラインのような扱いになり、高頻度でユーザーが目にするものではなくなります。

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が何を解決するか

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

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を理解していなくても大丈夫です。(宣伝)

シリーズ一覧

現在は(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が解決されたら再度レンダリングが試みられます。まだ、サスペンドが発生したときに代替のビューを提供する機能が提供されます(SuspenseuseTransition)。

これらの機能を使うことで、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」ボタンを押すとusersFetcherFetcher<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の中身がレンダリングされます。

より具体的な流れとしては以下のことが発生しています。

  1. Container内でsetUsersFetcherが呼び出されることでusersFetcherステートにFetcherが入る。
  2. Containerが再レンダリングされてUserListがレンダリングされる。
  3. UserListがレンダリングされる(関数UserListが呼び出される)最中に、get()でPromiseがthrowされる(UserListがサスペンドする)。
  4. サスペンドが発生したので、Suspenseの中身として<p>Loading...</p>がレンダリングされる。
  5. しばらくしてusersFetcherが返したPromiseが解決される。
  6. ReactがPromiseの解決を検知し、以前サスペンドしたUserListが再レンダリングされる。
  7. 今回はget()がPromiseを投げない(解決済のため)のでUserListはサスペンドされずに描画される。

一応画面の動きを示しておくと、このようになります。

screenshots-1.png

従来の方式との比較

一応、従来の方式(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を活用する


  1. 実際に上述のアプリで使われているバージョンではさらにgetOrUndefinedというメソッド(解決前だったらthrowするのではなくundefinedを返す)があるのですが、これが本質的に必要なのかは悩んでいます。設計力の不足により必要になってしまっただけかもしれません。 

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

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>
  );
}

Mar-28-2020 23-43-33.gif

予想と違って、初回のレンダリングときだけ処理されるようだ。

子ノードにも持たせてみる

子のノードにも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>
  );
}

image.png

これも予想とは違い、子→親の順番で処理された。
これはの順番もあるかもだけど、レンダリング処理が終わった順番から処理されるのかもしれない。

useEffectと比べて

useEffectの処理との順番を見る
予想:子→親→useEffect

export 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>
  );
}

image.png

これは予想通り、最後にuseEffectの処理が来た。

ここまででわかったこと

最初に述べたように、書くrefのノードがマウントされたあとにだけ処理されるようだ。

今まで、ノードに対して直接処理をしたい場合はuseEffectに書いていたんだが、これを使えば、useEffectを書かずとも、そのノードのrefに書いてあげればうまく動いてくれることがわかった。

使いみち

そして使いみちだけど、正直なにも思いつかないw
ライブラリ作るときとか、ちょっとこったUI作ったりするときは使うのかもしれないが、ちょっと考えつかない。。。

もしこんなので使ってるよみたいなのがあったら教えてほしいです。

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

インベントリー管理系のElasticsearchフロントアプリを1日で作る

はじめに

Elasticsearchを使って、インベントリーデータをささっと検索して必要な情報を取り出すようなアプリをゼロから1日で作る手順です。所持品やナレッジを分類データ化したものをパソコンやスマホから検索できるようにします。当然データ自体は自分で用意する必要があります。

作成するもの

バックエンドはAmazon Elasticsearch Serviceを用意し、データはそこに蓄積します。
フロントエンドはブラウザで動作するReactアプリで、Reactivesearchというコンポーネントを活用して、Elasticsearch Serviceのデータを検索できるようにします。ブラウザ(のJavaScript)から直接Elasticsearch Serviceにアクセスします。
また、Reactアプリを自動でビルドしデプロイする環境もCodepipelineで作ります。

アプリの画面イメージはこちら。
(この画面はインベントリー管理になっていませんが流用はできると思います)
beautifiedcityrank.png

インフラの概要図はこちら。
RactivesearchAP.png

手順

詳細手順を記述した記事をこの順番でやっていけば作れます。右端の時間は、順調に進められたときの目安の時間です。

  1. Amazon Elasticsearch Serviceで検索できる状態まで最速で立ち上げる(30分)
  2. React版Reactivesearch v3を使ってゼロから最速でElasticsearchフロントアプリを作る(1時間)
  3. React版ReactivesearchアプリをiPhone縦でも見やすくする(15分)
  4. AWS S3 + CloudFrontでReactアプリをHTTPS公開するための正しい構成(1時間)
  5. AWS Codepipelineを使ってReactアプリのCI環境をゼロから作る(1時間)
  6. Bracketsエディタからgit pushボタンで自動でデプロイされるCI環境を作る(15分)

番外編

今回は対応していませんが、派生での参考記事です。

Amplifyを使えば今回と同等の環境はもっと簡単に作れます。ただし、制約はあります。
AWS Amplify Consoleを使ってReactアプリのCICD環境を10分で作る

Elasticsearch Serviceに認証を付けたいところですが、今回は制約から見送りました。
Amazon Elasticsearch ServiceのKibana Cognito認証設定をゼロから最小限の設定で実現する

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

インベントリー/ナレッジ管理系のElasticsearchフロントアプリを1日で作る

はじめに

Elasticsearchを使って、インベントリーデータをささっと検索して必要な情報を取り出すようなアプリをゼロから1日で作る手順です。所持品やナレッジを分類データ化したものをパソコンやスマホから検索できるようにします。当然データ自体は自分で用意する必要があります。

作成するもの

バックエンドはAmazon Elasticsearch Serviceを用意し、データはそこに蓄積します。
フロントエンドはブラウザで動作するReactアプリで、Reactivesearchというコンポーネントを活用して、Elasticsearch Serviceのデータを検索できるようにします。ブラウザ(のJavaScript)から直接Elasticsearch Serviceにアクセスします。
また、Reactアプリを自動でビルドしデプロイする環境もCodepipelineで作ります。

アプリの画面イメージ

(この画面はインベントリー管理になっていませんが流用はできると思います)
beautifiedcityrank.png

インフラの概要図

RactivesearchAP.png

環境

Elasticsearch v7.4
Node.js v13.10.1
React v16.13.0
Reactivesearch v3.5.0

手順

詳細手順を記述した記事をこの順番でやっていけば作れます。右端の時間は、順調に進められたときの目安の時間です。

  1. Amazon Elasticsearch Serviceで検索できる状態まで最速で立ち上げる(30分)
  2. React版Reactivesearch v3を使ってゼロから最速でElasticsearchフロントアプリを作る(1時間)
  3. React版ReactivesearchアプリをiPhone縦でも見やすくする(15分)
  4. AWS S3 + CloudFrontでReactアプリをHTTPS公開するための正しい構成(1時間)
  5. AWS Codepipelineを使ってReactアプリのCI環境をゼロから作る(1時間)
  6. Bracketsエディタからgit pushボタンで自動でデプロイされるCI環境を作る(15分)

番外編

今回は対応していませんが、派生での参考記事です。

Amplifyを使えば今回と同等の環境はもっと簡単に作れます。ただし、制約はあります。
AWS Amplify Consoleを使ってReactアプリのCICD環境を10分で作る

Elasticsearch Serviceに認証を付けたいところですが、今回は制約から見送りました。
Amazon Elasticsearch ServiceのKibana Cognito認証設定をゼロから最小限の設定で実現する

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

今更ながら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.jsx
import 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.jsx
import 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.jsx
import 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.jsx
import 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.jsx
import 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.jsx
    const [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.jsx
    const 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を踏まえて処理したい場合に有効
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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
・プロジェクトのインジェクトを行う

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

フロントエンド知らないマンが始める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. ジェネリクス

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()
- etc

6-5. Routing

  • ルーティングの適応単位はコンポーネント
  • React Routerがデファクト

7. Redux

  • Reactはコンポーネントを組み合わせてアプリケーションを作成するが、コンポーネントは状態を持つものと持たないものがある
  • 実際のアプリケーションでは状態を保持したままコンポーネントを跨ぎたい場合がある
    • ログイン状態など

7-1. Flux

  • Reactで状態を保持するための考え方
  • この考えをもとに、Reactを使ったフレームワークがいろいろでてる

Screen Shot 2020-03-28 at 23.33.14.png

7-2 Redux

  • Fluxのフレームワーク競争の中で勝ち残って今やデファクト

Screen Shot 2020-03-28 at 23.38.34.png

  • 以下の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-actionsTypescript FSなどの外部ライブラリが使われることも
    • 一方、ライブラリのメンテが止まってたりもするので、どこまで使うかは要検討
  • Flux Standard Actionの思想を取り入れつつ、ベストエフォートでやるのが良さそう

8. Redux-Saga

  • 外部とのAPI通信を行うことで副作用を持つ非同期処理を扱う必要がある
  • 対処としては2つある
    • Reactのコンポーネント内で処理する方法
    • コンポーネントと外部への通信処理 が密結合になりロジックが複雑になる
    • それにより可読性やてスタビリティが下がる
    • Reduxのミドルウェアで処理する方法
    • こちらを使うのが一般的
    • thunksagaなどがある

8-1. Redux ThunkとRedux Saga

8-1-1. Redux Thunk

Screen Shot 2020-03-29 at 13.00.18.png

長所
  • 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のようなもの)を持つ

Screen Shot 2020-03-29 at 13.08.50.png

長所
  • 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. 参考

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

DeepLearning を使った手書き文字採点の Web サービスをリリースした

ブラウザ上で文字を手書きすると字の読みやすさを AI が自動採点してくれる、という web サービスを趣味で個人開発し、 2 週間ほど前にリリースしました:relaxed:


Letters - AI による手書き文字の採点アプリ

コア技術は、DeepLearning の画像認識と Preact によるフロントエンド実装です。
この記事では、開発したアプリに関して、サービス概要・技術詳細・所感を記載いたします。

サービス概要

機能紹介

ページ数が少なく軽量なため、機能に関してはアプリを見ていただいたほうがおそらく早いのですが、使用した技術の概要も含めた機能紹介をスクリーンショットと共にご説明します。

トップページからの手書き文字採点

letters_demo_01.gif

  • トップページでは文字が書ける枠が表示され、文字を書くことができます。
    • HTML5 の Canvas で文字を書けるようにしています。
  • 文字を書いて、「採点ボタン」をクリックすると候補が表示されます。
    • Canvas に書かれたものを PNG ファイルに画像化し、サーバ上に送信
    • サーバ上では受信した画像に対して、機械学習の推論を実行
    • 推論結果の、書かれた確率が高いものをクライアントに返信
  • 書いた文字を選択すると、採点結果(100点満点)が表示されます。
    • 推論結果の確率の値を元に、直感的な点数になるような計算式を適用し点数を算出
  • 丁寧で読みやすい字を書いたほうが高得点が出る可能性が高いです。
  • 書く線の太さを変えることもできます(線の太さによって採点結果が大いに変わることもあります)
これまで書いた文字の採点実績表示

letters_demo_02_2.gif

  • これまで書いた文字(採点した文字)の一覧を見ることができます。
    • 書いた文字一覧はサーバ上にログとして保管している
    • cookie をキーに書いた文字一覧を取得
    • 別の端末・ブラウザを使用した場合や、シークレットブラウザを使っている場合は、cookie が変わるため書いた文字の履歴は残りません
  • 他のユーザには書いた文字自体・書いた文字の一覧は一切見えないようになっています。
高得点の例の表示

letters_demo_03.gif

  • なかなか高い得点がだせない場合、どういう文字を書くと高得点が出るのかという例を表示することができます。
  • 実際にサーバ上で実行される推論処理によって高得点が出る例を、学習データ、テストデータから抽出し、推論された際の点数と共に画像を表示しています。

開発の背景・目的

今回の開発の目的は 2 つあります。

  1. 手書き文字を採点してくれるアプリがあったら楽しそう
  2. DeepLearning を応用した Web アプリケーションを作りたかった

開発してみた結果としては、とりあえずどっちも満たしたので満足です。
今回開発したものと同等の機能を提供しているようなサービスは現時点ではまだ存在しないのでは…と思っています。(いや、そこまで他のサービスのことは調査していません…)

こだわりポイント

軽快な動作

基本的にスマホやタブレットで使用されることを前提としているため、初期表示などをサクサクと動くように頑張りました。
具体的には、なるべくフロントエンドのファイル群が軽量になるようにする等の対応をしています。

採点可能文字の種類数の担保

採点対象は、日本語において多く用いられる、ひらがな・カタカナ・漢字・ローマ字・数字の合計 3175 文字で、日本で日常的に使われている文字をほぼ網羅するようにしました1

数字だけであれば MNIST のデータセットを使えれば容易に用意できるし、英数字だけでも様々なデータセットがありますが、ひらがな・カタカナ・漢字を含んだ上で英数字も混ぜ合わせ、さらに英数字の大文字/小文字も含めたデータセットは若干作るのが面倒でした。

技術詳細

アーキテクチャ概要

Letters_architecture (1).png

使用した技術・言語・ツール

フロントエンド
  • 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 コンテナをリリース単位として、下記の流れで実行します。

  1. 開発環境でフロントエンドをビルド
  2. ビルド済み・実行可能なコードを含んだコンテナイメージを作成
  3. 作成したコンテナイメージを、プライベートのコンテナレジストリにプッシュ
  4. プッシュしたコンテナイメージをベースにし、インスタンステンプレートを作成
  5. インスタンステンプレートを元に、インスタンスグループを更新

カタカナが多い…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 上には置いていないため、実行環境をそのまま開発環境として再現させることはできません。

所感

アプリで遊んでいて感じたこと

さて、突然関係のない話題ですが、次の文章の意味を解読できるでしょうか。
(※投稿時からちょっとだけ変更しました)

「卜口 卜 力二 力工夕」

mushimegane_boy.png

※再度表示するので、じっくり見て読んでいただくと分かるかもしれません。
「卜口 卜 力二 力工夕」

sashimi_maguro_ootoro.png
kani_ashi.png

すぐに正解が見えないように、無駄な画像を貼り申し訳ございません:sweat:

そして画像は完全なる引掛けで「とろ と かに かえた」のカタカナ表記だと思われた方は一文字もあっていません…
正解は下記の通りで、実は文章として全く成り立たない文字の羅列に過ぎません。

卜:水アナの「卜」
口:内炎、角の「口」
力:士、チカラ「力」
二:子玉川、項分布の「二」
工:事中、場、斎藤さんの「工」
夕:方、刊、食の「夕」
(コピペして Google 検索にかけると全て漢字であることが分かります。)

おそらく全ての文字を正しく認識できた方は多くないのではないかと思います。
もしそうだとすると「ちゃんとした活字ならば、どんな文字でも人間には認識可能で読み分けることができる」というのは真ではない、ということがわかります4
例えば「工」という表示に対して読みあげて下さい、と言われた際に、何もヒントがなければ、「工事の『工』かカタカナの「エ」(え)のどちらか」としか答えられないかと思います5。つまり、文字単体での判別はとても難しい場合がある、ということです。

また、本節の冒頭の「次の文章の意味を解読できるでしょうか」が「次の文字列の各文字はそれぞれ何が書いてあるでしょうか」 という問いであればまた見方がまた違った方も多いと思います。

よく言われることではありますが、機械学習、特に深層学習分野を勉強したり試したりしていると、人間の認識は文脈への依存が多分にあるということを強く感じることが多々あります。今回の文字認識はその代表的な例で、我々は文字を読む時、その字単体の図形のみを認識して判別しているわけではなく、「周辺にはどのような文字が書かれているのか」「どういった単語や形態素の要素となるか」「そこにはどういった単語が書かれていることが自然か」という「文脈と呼べる情報」に大いに依存している、ということが分かります。

今回開発したアプリでは、文脈情報を全く使わずに画像認識をして採点しているため、高得点が出にくい文字が少なからず存在します。その代表例が上記の5文字ですが、その他にも 0 (数字のゼロ)と O (ラテンアルファベットのオー)などのように、文字単体での認識が難しいために高得点が出にくい文字はそれなりの数があります。

機械学習関連の開発では「人間はどうやって認識しているのか」ということを考えつつ取り組むことが多いのですが、それを純粋に掘り下げることはとても興味深く、人間の脳の出来の良さに驚くことがしばしばあります。また「現時点での AI には何が出来て何が出来ないのか」というのも、そういった側面から考えるとヒントのようなものもたくさんあると思います。

なお個人的には、単語のみではない文脈情報も自然言語処理では解析可能なレベルになってきていますし、マルチモーダルの深層学習なども今後数年のうちに大きく発展するのではないかと思いますので、文脈を読んだ上での精度の高い推論も AI には自然にできるようになっていくと考えています。それらの進歩により、いわゆる「気が利く AI」も遠からぬ未来に実現されていくと思っています。

個人開発におけるリリースの難しさ

個人開発において「どこまで作ったらリリースするべきか」という判断は、業務のように契約等で納期が決まっている場合と異なり、とても難しいものです。
業務にせよ趣味にせよ、ソフトウェア開発をがっつりやったことのある方なら分かるかと思いますが、開発途中では、直さなければいけないバグ以外にも、改良点やあった方がいいだろうなと思う機能が無数に出てきます。今回の個人開発でも、追加した機能や改良した方がよい点がいまだにたくさんあります…

そのような中で「どこまでできた時点でリリースするか」というのはとても決めがたいものです。そのような状況で最も大切なのは「絶対に世に出す」という強い意志ではないかと思います。

Facebook 社内の標語として使われるらしい "Done is better than perfect" は、「ソフトウェアが完璧な状態になることなんてない。だからこそスピード感を持って世に出し、少しずつずっと改良し続けていくことが大切だ」という意図が背景にあるそうです6
この言葉に代表されるように、リリースの際にはある程度の見切りが必要です。
が、その一方、一度離れたユーザは戻ってくるまでに長い時間がかかるとよく言われますし、品質が低いものを気軽に世に出すというのも、技術者としてのプライドなども邪魔してなかなか難しいものです。

それらを踏まえると結局のところ、個人開発においても リリースは日付を決めてしまう(あらかじめリリース日を決めて、なにがなんでもそこにリリースするようにするようにするのがベストなのではないかと近頃は考えています。(それ以外にもよい方法があるかもしれません…もし知っていたら本当に知りたいので教えていただければと思います…!)

この「リリースタイミングの難しさ」に関しては他にももっと色々考えている・思うこともあるので、そのうち別途ポエムとして記事にまとめたいなーと思っています。

最後に

東京では外出の自粛要請の上に大雪と、家からなかなか出にくい環境のさなかですし、もしよければちょっとだけでも遊んで頂けると幸いです。
まだまだ不完全な点も多々あり機能的にも不十分かもしれませんが、ぜひぜひ楽しんでいただければと思います!

なお最近 Twitter も細々とやり始めていますので、もしよければTwitterアカウントのフォローなどお願いします m(_ _)m


  1. 内訳は、ひらがな 75 文字、カタカナ 71 文字、漢字 2967 文字、ローマ字が 52 文字、数字が 10 文字です。 

  2. gzip したもので 3KB ほど。驚異的。 

  3. 関数型コンポーネント・ hook API も Preact では普通に使えますし、Preact 用の router もあります。 

  4. 単なる読み間違えでないのであれば… 

  5. フォントによっては、漢字とカタカナの見分けがつくようになっているフォント/人も存在するかもしれません 

  6. The Hacker Way — Facebook に原文があります。とても素敵な文章です。 

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

【React】超カンタンなSPAサイトを一通り作成してみて

経緯

フロントエンドエンジニアとしてキャリアチェンジするにあたって、React.jsでのポートフォリオ作成に取り組む過程で、中でも一番興味があり、尚且つはじめに取り組むのに比較的カンタンなSPAサイトを作成してみました。

注意

あくまで、初学者が自分用のメモ、もしくは同じく同等のレベルの初学者方の参考に少しでもなればと思い投稿いたしますので、中級者異常の方にとってはあまり参考にならない記事となっておりますので、ご了承ください。

今回のディレクトリ構成

directory
 ーApp.js
 ーindex.js
 ーcomponents
  ーHeader
   ーHeader.jsx
  ーTop
   ーTop.jsx
  ーimg
   ーreact.png
あくまで、わかりやすいように今回の記事で解説する部分のみ表示しています。

個人的につまづいたポイント

  1. 各コンポーネント同士の連携
  2. 画像の読み込み
  3. .jsxはHTMLではなくてXML
  4. 階層構造

1. 各コンポーネント同士の連携

今回のポートフォリオを作成するにあたって、表示したいページごと単位だけではなくてHeaderやFooterもコンポーネントで作成してみました。
その際に、一般的なサイトと同様にHeader部分にナビゲーションを入れて、各ページに遷移することを試みました。
それにあたって、Header.jsxに非同期処理でのページ遷移を行うためのRouterタグ等を、以下のように予め用意しておきます。

Header.jsx
import 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されているため、重複してしまっていますよというメッセージですね。
スクリーンショット 2020-03-29 7.54.09.png

App.jsではこのようにシンプルで問題ありません。

App.js
import 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/)

最後に

ここまで、超初歩的なミスをまとめた記事を作成しましたが、自分にとっての振り返りとしてのみではなく、これから勉強される方にとっても何かしらの参考になれば幸いでございます。

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

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を読んでいく予定

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