BETA

useReducerの非同期処理に立ち向かう

投稿日:2019-05-21
最終更新:2019-05-21

React Hooks、使っていますか? 私は鋭意学習中です。

Reaact Hooksで提供されているAPIの一つに、useReducerが存在します。簡単に言うと、変数を宣言するときに更新方法をあらかじめ設定しておくことができるものですね。
言葉で聞いてもわかりにくいと思いますので、簡単な例を。

import React, { useReducer } from 'react';  
import './App.css'  

interface action {type: 'increment' | 'decrement'}  

const reducer = (oldCount: number, action: action) => { // 値の更新方法を定義  
  switch(action.type){  
    case 'increment': return oldCount + 1;  
    case 'decrement': return oldCount - 1;  
    default: throw new TypeError(`Illegal type of action: ${action.type}`);  
  }  
}  

const App: React.FC = () => {  
  const [count, dispatch] = useReducer(reducer, 0); // 宣言と初期化  

  return (  
    <div className="App">  
      <h1>{count}</h1>  
      <button onClick={() => dispatch({type: 'increment'})}>INCREMENT</button>  
      <button onClick={() => dispatch({type: 'decrement'})}>DECREMENT</button>  
    </div>  
  );  
}  

export default App;  

ボタンを押すと値が増えたり減ったりするだけの簡単なアプリですね。
ところでuseReducerで宣言したcountですが、どこで型を定義しているのでしょうか。型定義ファイルを読んでもちんぷんかんぷんでしたが、おそらく「reducerの第一引数の型が戻り値の型をすべて含んでいる場合は第一引数の型、そうでない場合はnever」になるかと思います(違っていた場合はマサカリお願いします)。
今回の場合、reducerの第一引数をnumberと定義し、戻り値も同じくnumberにしている(例外は戻り値ではない)ので、countnumberに定まるわけです。

さて、ここに非同期処理を挟みたいと思った場合はどのようにすればいいでしょうか。試しにそのまま入れてみましょう。

import React, { useReducer } from 'react';  
import './App.css'  

interface action {type: 'increment' | 'decrement'}  

const reducer = async (oldCount: number, action: action) => {  
  await sleep(1000);  

  switch(action.type){  
    case 'increment': return oldCount + 1;  
    case 'decrement': return oldCount - 1;  
    default: throw new TypeError(`Illegal type of action: ${action.type}`);  
  }  
}  

const App: React.FC = () => {  
  const [count, dispatch] = useReducer(reducer, 0);  

  return (  
    <div className="App">  
      <h1>{count}</h1>  
      <button onClick={() => dispatch({type: 'increment'})}>INCREMENT</button>  
      <button onClick={() => dispatch({type: 'decrement'})}>DECREMENT</button>  
    </div>  
  );  
}  

const sleep = (time: number) => {  
  return new Promise((resolve, reject) => {  
    setTimeout(() => {  
      resolve();  
    }, time);  
  });  
}  

export default App;  

useReducerでエラーが出てしまいました。当然ですが、asyncを頭に付けた関数では戻り値が自動的にPromiseになります。numberPromiseでは型が一致しないため、countneverと判定されてしまったわけですね。
そこで非同期処理を切り出し、別個に実行してからdispatchを呼び出すことで、reducerへ非同期処理が侵食しないように修正してみます。

import React, { useReducer } from 'react';  
import './App.css'  

interface action {type: 'increment' | 'decrement'}  

const reducer = (oldCount: number, action: action) => {  
  switch(action.type){  
    case 'increment': return oldCount + 1;  
    case 'decrement': return oldCount - 1;  
    default: throw new TypeError(`Illegal type of action: ${action.type}`);  
  }  
}  

const App: React.FC = () => {  
  const [count, dispatch] = useReducer(reducer, 0);  

  const serveAction = async (action: action) => {  
    await sleep(1000);  
    dispatch(action);  
  }  

  return (  
    <div className="App">  
      <h1>{count}</h1>  
      <button onClick={() => serveAction({type: 'increment'})}>INCREMENT</button>  
      <button onClick={() => serveAction({type: 'decrement'})}>DECREMENT</button>  
    </div>  
  );  
}  

const sleep = (time: number) => {  
  return new Promise((resolve, reject) => {  
    setTimeout(() => {  
      resolve();  
    }, time);  
  });  
}  

export default App;  

実行してみると、期待した通りに動作するかと思います。

非同期処理で得た値を渡したい場合は、actionに属性を追加してあげればよさそうです。

import React, { useReducer } from 'react';  
import './App.css'  

interface action {type: 'increment' | 'decrement', amount: number}  
interface material {type: 'increment' | 'decrement', numericStr: string}  

const reducer = (oldCount: number, action: action) => {  
  switch(action.type){  
    case 'increment': return oldCount + action.amount;  
    case 'decrement': return oldCount - action.amount;  
    default: throw new TypeError(`Illegal type of action: ${action.type}`);  
  }  
}  

const sleep = (time: number) => {  
  return new Promise((resolve, reject) => {  
    setTimeout(() => {  
      resolve();  
    }, time);  
  });  
}  

const App: React.FC = () => {  
  const [count, dispatch] = useReducer(reducer, 0);  

  const serveAction = async (material: material) => {  
    const amount = Number(material.numericStr);  
    if(isNaN(amount))  
      throw new TypeError('[material.numericStr] is not number.');  

    await sleep(1000);  
    dispatch({type: material.type, amount: amount});  
  }  

  return (  
    <div className="App">  
      <h1>{count}</h1>  
      <button onClick={() => serveAction({type: 'increment', numericStr: '1'})}>INCREMENT</button>  
      <button onClick={() => serveAction({type: 'decrement', numericStr: '1'})}>DECREMENT</button>  
    </div>  
  );  
}  

export default App;  

まとめ

思ったよりも簡単に非同期処理が追加できましたね。
関数の宣言はuseReducerを利用しているコンポーネントの配下で行わなければならないため、こんがらがってしまわないように気を付けたいです。

参考にしたstack overflowのトピック

技術ブログをはじめよう Qrunch(クランチ)は、プログラマの技術アプトプットに特化したブログサービスです
駆け出しエンジニアからエキスパートまで全ての方々のアウトプットを歓迎しております!
or 外部アカウントで 登録 / ログイン する
クランチについてもっと詳しく

この記事が掲載されているブログ

だらだら更新していきたい技術ブログ

よく一緒に読まれる記事

0件のコメント

ブログ開設 or ログイン してコメントを送ってみよう
目次をみる
技術ブログをはじめよう Qrunch(クランチ)は、プログラマの技術アプトプットに特化したブログサービスです
or 外部アカウントではじめる
10秒で技術ブログが作れます!