reactのhooksを利用した状態管理ライブラリeasy-peasyを使ってみた
最近reactのhooksが話題になっていますね。
そのhooksを利用した状態管理ライブラリを見つけました。
easy-peasyの意味は、「とっても簡単」というらしいです! 今回はこのライブラリを使用して簡単なgithubのリポジトリ検索機能を作成しようと思います。
完成したコードは以下になります。
GitHub - hayawata3626/easy-peasy-sample
以下からは、コード全てを説明するのではなく、主要な部分を中心に説明していきます。
環境構築
今回はcreate-react-appを使用しています。
プロジェクトの作成
create-react-app easy-peasy-sample
hooksは、React v16.7.0-alphaから使えるのでアップデートします。
yarn add -D react@16.7.0-alpha.0 react-dom@16.7.0-alpha.0
以下は今回使用するライブラリなのでインストールしてください。
これで環境構築は終了です。
このライブラリで使われるのが主にeffect、actionです。 effectは副作用のあるもの、非同期にデータを取得する時などに使用します。
フォルダ構成
フォルダ構成は以下のようになっています。(最低限必要な箇所を表示)
├── App.js ├── components │ ├── Item.js │ ├── Result.js │ └── Search.js ├── index.js ├── model.js
storeの作成
githubのREADME.mdにも説明されていますが、以下のようにstoreを作成します。
公式から引用したコードです。
const model = { todos: { items: [], } };
createStore
をimportします。
import { createStore } from 'easy-peasy'; const store = createStore(model);
これでreduxのstoreと同じようなものを作成できました。とても手軽ですね。
以下は実際にstoreを作成したコードです。
import React from "react"; import ReactDOM from "react-dom"; import * as serviceWorker from "./serviceWorker"; import { createStore, StoreProvider } from "easy-peasy"; import model from "./model"; import App from "./App"; import "./index.css" const store = createStore(model); function Root() { return ( <StoreProvider store={store}> <App /> </StoreProvider> ); } ReactDOM.render(<Root />, document.getElementById("root")); serviceWorker.unregister();
reduxとに似ていますね。
初期値の設定
必要なデータは以下になります。
export default { // data items: [], loading: false, }
items
にはapiから取得したデータが一つ一つ入ってきます。
loading
は検索中の時に表示したいので初期値はfalse
です。
Actionの作成
今回メインとなるactionは以下になります。
- 検索をする
- 検索中にプログレスを表示
export default { // data items: [], loading: false, // actions search: (state, payload) => { state.items = []; if (!payload.data.items.length) return; state.items = _.concat(state.items, payload.data.items); }, isProgress: (state, payload) => { state.loading = payload; }, }
キーにはaction名、値には関数を書きます。 actionは第一引数にstate、第二引数にpayloadを受け取ります。 payloadにはactionが発火した時のデータが入ります。
Effect Actionの作成
これからeffect actionを作成していきます。 今回のようにgithubからデータを取得するときなど、非同期で何かをしたい時に使用します。
以下のコードをみてください。(公式ドキュメントから引用しています。)
import { effect } from 'easy-peasy'; // 👈 import the helper const store = createStore({ todos: { items: [], // 👇 define an action surrounding it with the helper saveTodo: effect(async (dispatch, payload, getState) => { // 👆 // Notice that an effect will receive the actions allowing you to dispatch // other actions after you have performed your side effect. const saved = await todoService.save(payload); // 👇 Now we dispatch an action to add the saved item to our state dispatch.todos.todoSaved(saved); }), todoSaved: (state, payload) => { state.items.push(payload) } } });
見てわかるようにeffect action内では直接stateを変更していません。 代わりにdispatchを受け取り、actionにdispatchすることでstateを変更しています。 下記のコードを追加しました。
import { effect } from "easy-peasy"; // 追加 export default { ... 省略 // effects fetched: effect(async (dispatch, payload) => { const SEARCH = "https://api.github.com/search/repositories"; try { dispatch.isProgress(true); const result = await axios.get( `${SEARCH}?q=${payload}+in:name&sort=stars` ); dispatch.isProgress(false); dispatch.search(result); } catch (err) { dispatch.isProgress(false); } }) };
payloadには検索フォームで入力した文字列が入ってきます。 あとはプログレスを表示・非表示するだけです。
これで主要なロジックの部分が終わりです。
コンポーネントの作成
コンポーネントを作成していきます。
Saearhコンポーネント
import React, { useRef } from "react"; import Button from "@material-ui/core/Button"; import { useAction } from "easy-peasy"; import styled from "@emotion/styled"; const Wrapper = styled("div")` display: flex; justify-content: center; `; const Input = styled("input")` padding: 10px 20px; font-size: 20px; `; const SearchButton = styled(Button)` && { margin-left: 18px; } `; export default function Search() { const search = useAction(dispatch => dispatch.fetched); const inputEl = useRef(null); const handleSearchClick = () => { search(inputEl.current.value); }; return ( <Wrapper> <Input ref={inputEl} type="text" /> <SearchButton variant="contained" color="primary" onClick={handleSearchClick} > Search </SearchButton> </Wrapper> ); }
大事な箇所は以下の部分です。
const search = useAction(dispatch => dispatch.fetched);
useActionはコンポーネント側でstoreのactionがほしい時に使います。 上記ではactionを一つしか呼んでいませんが、もう少し複雑なアプリケーションになると複数のactionを一つのコンポーネントで使いたいケースが多いと思います。
そのような場合は以下のように使用します。 (公式ドキュメントから引用)
import { useState } from 'react'; import { useAction } from 'easy-peasy'; const EditTodo = ({ todo }) => { const [text, setText] = useState(todo.text); const { saveTodo, removeTodo } = useAction(dispatch => ({ saveTodo: dispatch.todos.save, removeTodo: dispatch.todo.toggle })); return ( <div> <input value={text} onChange={(e) => setText(e.target.value)} /> <button onClick={() => saveTodo(todo.id)}>Save</button> <button onClick={() => removeTodo(todo.id)}>Remove</button> </div> ); };
Resultコンポーネントの作成
import React from "react"; import { useStore } from "easy-peasy"; import Item from "./Item"; import Typography from "@material-ui/core/es/Typography/Typography"; export default function Result() { const items = useStore(state => state.items); return ( <> {items.length ? ( items.map((todo, index) => <Item key={index} todo={todo} />) ) : ( <Typography>検索結果は0です</Typography> )} </> ); }
ここでキーとなるのがuseStoreです。
コンポーネント側でstoreに格納されているstateにアクセスしたい時に使用します。
引数にはstate
を引数に持つ関数を渡します。
またactionの時と同じように複数のstateを利用したい場合は以下のようにします。
(公式ドキュメントから引用)
import { useStore } from 'easy-peasy'; const BasketTotal = () => { const totalPrice = useStore(state => state.basket.totalPrice); const netPrice = useStore(state => state.basket.netPrice); return ( <div> <div>Total: {totalPrice}</div> <div>Net: {netPrice}</div> </div> ); };
👇分割代入での方法
import { useStore } from 'easy-peasy'; const BasketTotal = () => { const { totalPrice, netPrice } = useStore(state => ({ totalPrice: state.basket.totalPrice, netPrice: state.basket.netPrice })); return ( <div> <div>Total: {totalPrice}</div> <div>Net: {netPrice}</div> </div> ); };
useStoreとuseActionはワード的にもわかりやすいですし、複数のactionやstateを取得したい時にも便利ですね。
補足
今回のコードには使用されていませんが、もう一つ紹介します。 reduxのreselectのような機能です。
公式サイトから引用
import { select } from 'easy-peasy'; // 👈 import then helper const store = createStore({ shoppingBasket: { products: [{ name: 'Shoes', price: 123 }, { name: 'Hat', price: 75 }], totalPrice: select(state => state.products.reduce((acc, cur) => acc + cur.price, 0) ) } }
stateを引数にもつ関数を引数にもちます。 便利なapiが提供されていますね。
react-nativeとも連携することができます。
https://github.com/ctrlplusb/easy-peasy#usage-with-react-native
tsの対応は以下のissueにのっています。
https://github.com/ctrlplusb/easy-peasy/issues/21
以上でhooksで作成されたライブラリeasy-peasyの紹介を終わります。 本当に簡単に実装することができました。