BETA

Reduxよりも多分使いやすいReact Custom Hooksを作りました !

投稿日:2019-07-26
最終更新:2019-07-28
※この記事は外部サイト(https://qiita.com/uttk/items/fdd5a1edee379...)からのクロス投稿です

作ったもの

非同期処理をより簡単に一か所にまとめて管理できるReact Custom Hooksです。

github



npm

https://www.npmjs.com/package/use-clutch

playground ( カウントアプリ )

https://codesandbox.io/embed/use-clutch-playground-wnmyx

作った経緯

前回の記事で、ReactHooksのみでWebアプリを作ったことを紹介しました。
その時に、Reduxを使わずにWebアプリを作っていたのですが、何かとState周りで手こずっていたので、そのState周りの問題を解決するために作りました。
なので、Webアプリの副産物のようなものです。

インストール&使い方

インストール

$> npm install use-clutch  

or

$> yarn add use-clutch  

使い方

Store管理したいコンポーネントで、useStateuseEffectなどと同じように使えます。

import React from "react";  
import { useClutch } from "use-clutch";  

const store = { value : "not update" };  

const reducer = async (state, action) => {  
  switch(action.type) {  
    case "action":  
      return { value : "updated!" };  

    default:  
      return state;  
  }  
};  

const App = () => {  
  const { state, dispatch } = useClutch(reducer, store);  
  const onClick = () => dispatch("request", { type : "action" });  

  return (  
    <button onClick={onClick}>{ state.value }</button>  
 );  
}  

第一引数にはPromiseを返すreducer関数、第二引数には初期値を渡します。
初期値は必ずObjectでなければなりませんので、注意してください。

特徴

1.非同期関数をまとめて管理できる !

reducer関数をasync関数として扱っています。なので、非同期処理をreducer関数に閉じ込めることができ、処理をより管理しやすくなります。

const reducer = async ( state, action ) => {  
  switch(action.type){  
    case "何らかのアクション":   
      const result = await 非同期処理( action.payload ); // reducer内で非同期関数を実行できる!  
      return result;  

    default:  
      return state;  
  }  
}  

actiondispatchする時は簡潔に書けます!

const App = () => {  
  const { dispatch } = useClutch(reducer, store);  
  const onClick = () => dispatch("何らかのアクション", { type: "何らかのアクション", payload: "通信に必要なデータなど"  });  

  return (  
    <div>  
      <button onClick={onClick}>非同期処理を実行する</button>  
    </div>  
  )  
}  

2.無駄な通信を排除できる !

ちょっとここでデータフローを確認してみましょう。

画像内に、通信中なら通信中のPromiseを返すという部分があります。これがまさに無駄な通信を排除できる部分になります。
具体的にどこで活躍するかというと、それはリスト表示する時など活躍します。

const ListItem = ({ item }) => {  
  const [user, setUser] = useState({ name : "" });  

  useEffect(() => {  
    (async () => {  
      const user = await getUser(item.userId); // getUser関数の定義は省略してます。  
      setUser(user);  
    })();  
  },[]);  

  return (  
    <li>  
      <p>{item.name} @ create by {user.name}</p>  
    </li>  
  );  
}  

const App = () => {  
  const items = [{ id: 0, name: "hello", userId: 1 }, { id: 1, name: "world", userId: 1 }];  

  return (  
    <ul>  
      {  
         items.map(item => (  
           <ListItem key={item.id} item={item} userName={users[item.userId]} />  
         ))  
      }  
    </ul>  
  );  
}  

上記の例では、items配列からListItemコンポーネントを作成しています。
また、ListItemコンポーネント初回マウント時にユーザーを取得する処理をしています。
この時itemsの要素(item)の中に同じuserIdを持つitemが複数あり、同じユーザーを取得する処理を走らせてしまいます。
これはItemsのサイズが大きくなればなるほど発生しやすくなり、表示が遅くなったり、サーバーにも余計な負荷がかかるなどいろいろな問題が発生します。

use-clutchを使えばこれらの問題が解決します。

const store = {   
  items: [{ id: 0, name: "hello", userId: 1 }, { id: 1, name: "world", userId: 1 }],  
  users: {}  
};  

const reducer = async (state, action) => {  
  switch(action.type){  
    case "get-user": {  
      const user = await getUser(action.payload).catch(e => null); // getUser関数の定義は省略してます。  

      if( user ){  
        return { ...state, users: { ...state.users, [user.id]:user } };  
      }  

      return state;  
    }  

    default:  
      return state;  
  }  
}  

const ListItem = ({ clutch, item, user }) => {  
  useEffect(() => {  
    // ここでuserを取得するためのアクションをdispatch  
    clutch.dispatch(`get-user-${item.userId}`, { type: "get-user", payload: item.userId });  
  },[]);  

  return (  
    <li>  
      <p>{item.name} @ create by { user ? user.name : "" }</p>  
    </li>  
  );  
}  

const App = () => {  
  const clutch = useClutch(reducer, store);  
  const { items, users } = clutch.state;  

  return (  
    <ul>  
      {  
         items.map(item => (  
           <ListItem key={item.id} item={item} user={users[item.userId]} />  
         ))  
      }  
    </ul>  
  );  
}  

ここで注意してほしいのは、dispatchをした段階ではまだ非同期処理を行っていないということです。
dispatchを実行するとuse-clutchは、dispatch関数第一引数の文字列(リクエスト文字列)を見て、同じリクエスト文字列の処理が実行中なのかを判定し、既に実行中なら、そのdispatchリクエストを破棄して通信中のPromiseを返します。

3.非同期処理をキャンセルできる !

意外と非同期処理をキャンセルできるモジュールなどが少なかったので、入れてみました。
実のところ、use-clutchは非同期処理をキャンセルしているのではなく、非同期処理を行っているPromiseインスタンスを破棄しているにすぎません。
よって、通信処理が発生した後にキャンセルしても通信処理が中止されるわけでは無いです。

しかし、描画処理はキャンセルされるので無駄な処理を省けることには変わりありません。
clutch.requestCancel("キャンセルするリクエスト文字列");  

リクエスト文字列は、dispatch関数,pipe関数,request関数の第一引数に渡す文字列の事です。

4.複数のActionを一つのActionとして実行できる !

複数のactionを一つのactionとして実行したい時などがあります。
これもuse-clutchを使えば簡単に実装できます。

clutch.pipe("request string",  
  state => ({ type : "ユーザーを取得する", payload:"user id"  }),  
  state => state.user ? ({ type : "記事を取得する", payload: state.user.id  }) : null  
);  

pipe関数で、actionを繋げています。
第二引数以降の引数には、ActionCreater関数を指定します。ActionCreater関数の第一引数には常に最新のstateの値が渡されます。
なので、前のActionの通信結果を受け取ることができます。また、nullを返すと何もしないActionとして扱われます。
なので、

state => state.user ? ({ type : "記事を取得する", payload: state.user.id  }) : null  

部分は、前のActionの結果を元に記事を取得するActionを実行するかを決めています。

描画更新は、pipe関数に渡されたActionがすべて完了した時に実行されます。

5.ソースコード量が少ない!

ソースコードは、全部で250行ぐらいです。
既にReact Hooksを理解している人ならすぐにソースコード全体を理解できると思います。

まとめ

ここまで読んでくれてありがとうございます。
ちょっとでも気になった人がいれば、サンプルを作っているので試していただくとよりわかると思います。

サンプル
https://codesandbox.io/embed/use-clutch-playground-wnmyx

reduxに不満がある人やState管理にお困りの方は、ぜひ使ってやってください。State周りの問題を解消できるかもしれません。

機能追加などの要件は、githubにissuepull-request投げてください。
すぐ対応します。

もし気になったことなどがありましたら、お気軽にコメントください !
Twitterでも大丈夫ですよ。

それではまた。

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

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

主にフロントエンドのことを書くブログです。

よく一緒に読まれる記事

1件のコメント

ブログ開設 or ログイン してコメントを送ってみよう
07/28 14:04

とても参考になるコメントが、はてなブックマークにあったので紹介します。

コメントへのリンク

この手法が一般的でないのは理由がありまして、Reducerが副作用を伴う処理を含んでしまうと、もともと意図されていたような純粋な関数でなくなってテストがとても書きにくくなってしまうんですよね。

確かにreducer関数が非同期処理(Promise)を持ってしまうとテストが書きにくくなるかもしれませんね。

とても参考になるコメントありがとうございました!

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