basyura's blog

あしたになったらほんきだす。

React - Hooks を使って ToDo アプリ

GW 中は React の勉強するかと思い立ったもののまだまだ道半ば (ゴールが見えない)。その自分用の記録。

構成を考える

Web 層は Go + echo でキーワード検索した結果を JSON で返すだけ。CL 層は React で受け取った JSON の内容を表示する単純構成。Web はすぐに完成。CL 層もボタンを押したタイミングで一覧を出せるところまでは React のチュートリアルを見ながらで到達。

ここからが長かった。

Flux

React で状態管理をしたくなるが、複数 Component で構築する際に状態をどこに持って管理して、どう伝搬させたらいいのか分からなくてすぐに詰まる。Facebook 推奨の Flux を使えばいいらしいことが分かるので入門記事を漁る。

pub/sub 方式により Action でイベントを発行して Dispatcher が振り分ける。
Store を更新して View が反映する。

連携 クラス図
f:id:basyura:20200505143037p:plain f:id:basyura:20200505143217p:plain

TodoStore.js

class TodoStore extends EventEmitter {
    createTodo(text) {
        // ・・・ 略 ・・・ //
        this.emit("change")
    }
  
  handleActions(action) {  switch(action.type) {
      case "CREATE_TODO": {
        this.createTodo(action.text);
      }
  }
  
    // ・・・ 略 ・・・ //
}

const todoStore = new TodoStore();
dispatcher.register(todoStore.handleActions.bind(todoStore));

Todos.js

TodoStore.on("change", () => {
    this.setState({
        todos: TodoStore.getAll()
    })
})

f:id:basyura:20200505144448p:plain:w600

dispatcher と emitter の二つが出てくる。使い分けはなんとなく感じるけど漠然としてる。
これでもいいのだけど状態管理するのに分かりづらいので他の方法が無いかを探し始める。
とくに状態を伝搬させていくのが辛い。

Redux

すぐに Redux がヒットして、説明を読む分にはこれがいいのではないかと入門を漁る。

Redux の Store の特徴として、flux とは異なり、一つの Store のみ存在することです。すべての状態データを一箇所で管理することになるのでシンプル性を維持することができます。画面及びアプリケーションの初期化は Store から始まります。

  • reducer を作成する
  • store を作成する
  • action をdispatch する
  • 単一のreducer もしくは複数のreducer が単一のstore に対して処理を行う

middleware を使うと reducer の組み合わせで柔軟な処理を実現できるっぽい。

f:id:basyura:20200505145039p:plain:w500

ここまできて、大げさすぎるしよく分からんくなってきたのでもっと簡単にできないかと探し始める。

React Hooks

“接続する (hook into)” ための関数です。 フックは React をクラスなしに使うための機能ですので、クラス内では機能しません

useEffect

レンダリングの後に処理を動作させる

  • React は、副作用が実行される時点では DOM が正しく更新され終わっていることを保証します。 ( https://ja.reactjs.org/docs/hooks-effect.html )
  • 関数を返すと cleanup 関数とみなされて、useEffect 実行前に呼ばれる
const [count, setCount] = useState(0)

useEffect(() => {
  console.log('hello useEffect')
})

return (
  <>
    <p>You clicked {count} times</p>
    <button onClick={() => setCount(count + 1)}>
      Click me
    </button>
  </>
);

useContext

prop drilling 問題を解決する一案。

const Context = createContext()

const Mago = () => {
  const { money } = useContext(Context)
  return <p>{money}円</p>
}

const Kodomo = () => <Mago />

const Oya = () => {
  return (
    <Context.Provider value={{ money: 10000 }}>
      <Kodomo />
    </Context.Provider>
  )
}

useRef

const afterRef = useRef();

// useEffect の cleanup で参照する場合はコピーしないとだめ? (ワーニングが出る)
useEffect(() => {
  document.title = `You clicked ${count} times`;
  const copyRef = afterRef;
  return () => {
    copyRef.current.innerText = count;
  };
});

return (
  <span ref={afterRef}>{count}</span>
)

Hooksを使う上で絶対に守ること

コンポーネントの中で呼び出されるHooksはいつなんどきでも必ず同じ順番で同じ回数呼び出されること!

簡単にいうとつまり if や for の中に Hooks を入れて「場合によって Hooks の順番や実行回数が変わる」ことを禁止しています。また、早期 return による実行回数のズレにも注意です。 基本的には関数コンポーネントのトップレベルかつ最上部に Hooks を書き並べておけば大丈夫でしょう。

Reducer と useContext でオレオレ状態管理

Redux のあたりで出てきた Reducer と createContext を使えば手軽にできるじゃんと思い立つ。

  • Store (状態管理のオブジェクト) をトップレベルで作って Context で伝搬させる
  • 状態の変更は Reducer を通して通知する
function App() {
  const [store, dispatch] = useReducer(reducers, Store);

  return (
    <div className="App">
      <AppContext.Provider value={{ store, dispatch }}>
        <Header />
        <Content />
      </AppContext.Provider>
    </div>
  );
}
const ToDoPane = () => {
  const { store, dispatch } = useContext(AppContext);

  return (
    <div>
      {store.toDoList.map((todo) => (
        <ToDoItem
          key={todo.key}
          title={todo.title}
          description={todo.description}
          onClick={() => dispatch({ type: "remove", todo: todo })}
        />
      ))}
    </div>
  );
};
export default function reducer(store, action) {
    switch (action.type) {
    case "add":
      return store.add(action.todo);
    case "remove":
      return store.remove(action.todo);
    default:
  }
  return store;
}

いい感じにできた気がしたのだけど挫折

  • reducer が邪魔くさく感じる
  • App() が何回も呼ばれる
  • 描画ループに陥る

unstated-next

もっと簡単にやりたいので探し始める。オレオレでやりたかったことができるじゃんで飛びつく。

200 bytes to never think about React state management libraries ever again

  • React Hooks use them for all your state management.
  • ~200 bytes min+gz.
  • Familiar API just use React as intended.
  • Minimal API it takes 5 minutes to learn.
  • Written in TypeScript and will make it easier for you to type your React code.
ページ構成 クラス図
f:id:basyura:20200505150838p:plain f:id:basyura:20200505150925p:plain

react-modal でモーダル表示もしてみた。

f:id:basyura:20200505151026p:plain:w400

こねこねやった結果がこのあたり。
最初の描画でデータを取得して反映する方法がよくわからず (描画のループになるとか、描画中に反映できないエラーとか) Initializer Component をかますようにしてみた。正当な方法はどうやるんだろか。

その他 - オブジェクトの更新方法

hooks を使うときは新しいオブジェクトを返すお決まりなのでやり方。

配列に追加

[...state.values, newValue]

配列のある要素の変更

[
 ...state.slice(0, action.index),
 modifiedValue,
 ...state.slice(action.index + 1)
]

オブジェクトの変更

Object.assign({}, 
  state, 
  {
    completed: true,
    name: "new name"
  });

まとめ

パズルみたいでアレをやるためにはどうしたらいいのかを結構考えないといけなくて、ちょっとズレてると大失敗する印象。
いまどきのフロントエンド開発は大変だ。

本命のツールは作れずで今に至る。疲れた。 そもそも React である必要はなくて単純なページ更新方式でいいのだけど。