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の紹介を終わります。 本当に簡単に実装することができました。
分割代入でオブジェクトにある特定の値を取得してくる
今回は分割代入でオブジェクトにある特定のキーの値を取得してくる方法を見ていきたいと思います。
経緯
reduxのコードを見ている時に分割代入をしているコードを見かけて挙動が気になった。
従来の方法と分割代入の比較
nameを取得する
const person = { name: "ジロリン丸", address: "Tokyo", } // 従来 const name = person.name console.log(name) // ジロリン丸 // 分割代入 const { name } = person console.log(name) // ジロリン丸
複数階層の時
この中のbrother
を取得する
const person = { name: "ジロリン丸", address: "Tokyo", family: { brother: 2, sister: 1 }, favoriteFoods: ["ramen", "ziro"] }; // 従来 const brother = person.family.brother; console.log(brother) // 2 // 分割代入 const { family: { brother } } = person; console.log(brother) // 2
以上になります。
まとめ
分割代入は便利ではありますが、読みやすさという点では最初理解しずらいところはあるかもしれません。それを踏まえても便利な点が多いので積極的に使っていこうと思います。
ReactのライフサイクルやPureComponentについて
今回はReactのライフサイクルやPureComponent辺りについて紹介したいと思います。
早速ですが、ライフサイクルはMounting、Updating、Unmounting、Error Handlingがあります。(今回の記事ではUnmountingとError Handlingについては紹介しません。)
Mounting
以下のメソッドは、コンポーネントのインスタンスを作成してDOMに追加される時に呼び出されます。呼び出される順番は以下の順番です。
- constructor()
- static getDerivedStateFromProps()
- render()
- componentDidMount()
Updating
以下のメソッドはコンポーネントが再レンダリングされる時に呼ばれます。
- static getDerivedStateFromProps()
- shouldComponentUpdate()
- render()
- getSnapshotBeforeUpdate()
- componentDidUpdate()
ここで重要なのはshouldComponentUpdate
です。
コンポーネントがrenderされる前に呼ばれます。
このメソッドは、nextProps
, nextState
を受け取り現在のpropsとstateを比較します。更新するときはtrue
を更新が必要ない場合はfalse
を返します。
注意したいのは、React.Component
の場合はshouldComponentUpdateはデフォルトでtrueを返すということです。
なのでReact.Component
の場合はshouldComponentUpdate
でロジックを書く必要があります。
簡単な例
type Props = { name: string; age: number; } class User extends React.Component<Props> { shouldComponentUpdate(nextProps:Props) { if(this.props.name === nextProps.name) { return true } else { return false } } /* 以下省略 */
例えば親のコンポーネントの状態だけが変更し、子のコンポーネントのpropsやstateに変化がない場合に子のコンポーネントも一緒にマウントされます。そうするとコンポーネントの描画速度が遅くなります。ですのでReact.Component
で書く場合は、ちゃんとshouldComponentUpdate
を使って描画パフォーマンスを最適化してあげる必要があります。
React.PureComponent
次に最初の方で挙げたReact.PureComponentについてです。
結論を先に言うとReact.PureComponent
はshouldComponentUpdate
の処理が実装済みとなっています。ですのでReact.PureComponent
を使うのが良いでしょう。ですがここで注意したいのは、React.PureComponentのshouldComponentUpdate
はshallow compareするので以下のケースの場合、常にマウントされる結果となります。
class App extends React.PureComponent { render() { return ( <div className="App"> <User families={{ brother: 2, sister: 1 }} name="sato" age={20} /> </div> ); } }
propsにはつねに新しいオブジェクトを受け取っています。
shallow compareなのでプロパティと値が正しくてもshouldComponentUpdate
の戻り値はtrue
を返してしまいます。今はオブジェクトの階層は深くありませんが、もっと階層がある場合などこのような場合などを考慮するとPureComponent
の受け取るpropsやstateなどはシンプルなものにすると良いでしょう。また深い階層でpropsやstateの値が変更されるとわかっている場合は、forceUpdate()
を使うのも一つの方法です。
Shallow Compareについて
Pure.Component
がリリースされる前は、Shallow Compareを使用していました。このアドオンがPure.Component
の役割を果たしていました。
以下のように使います。(公式サイトから引用)
// インポートする方法 import shallowCompare from 'react-addons-shallow-compare'; // ES6 var shallowCompare = require('react-addons-shallow-compare'); // ES5 with npm
shallowCompare
メソッドには同じようにnextProps
とnextState
を渡してあげます。そうすることでshallowCompareが比較してくれます。
export class SampleComponent extends React.Component { shouldComponentUpdate(nextProps, nextState) { return shallowCompare(this, nextProps, nextState); } render() { return <div className={this.props.className}>foo</div>; } }
まとめ
Reactのコンポーネントの描画する仕組みを知っておくことで、最適な描画パフォーマンスを実現できると思います。 今回紹介したもの以外にReact.memoやrecomposeのpureなどがあるので次はこれらについて紹介したいと思います。
JavaScriptの非同期処理とスコープの関係性について
今回非同期処理とスコープの関係性について紹介したいと思います。
唐突ですが以下のコードをご覧になってください。
function countdown() { let i; for (i = 5; i >= 0; i--) { setTimeout(function() { console.log(i === 0 ? "GO!" : i); }, (5 - i) * 500); } } countdown();
出力結果はどうなると思いますか?
結果は以下になります。
このコードのポイントとしては、変数i
がfor文の外にあるということです。setTimeoutが実行される時にはi
の値が-1
になっているのでsetTimeoutが参照する値が2行目のi
つまり-1になります。
では以下のコードではどうなるのでしょう?
function countdown() { for (let i = 5; i >= 0; i--) { setTimeout(function() { console.log(i === 0 ? "GO!" : i); }, (5 - i) * 500); } } countdown();
結果は以下となります。
こちらは意図としたように動きます。
先ほどのコードとの違いは、変数i
をforループの制御文の箇所で利用しています。
JavaScriptの処理系はループの各ステップで新しい独立した変数iのコピーを作成します。setTimeoutに渡された関数が実行されるときに、自分の独自のスコープになる変数から値を受け取ることになります。
言葉ではなかなかわかりづらいので下記のコードをみてみてください。
// 1回目 { let i = 5 setTimeout(function() { console.log(i === 0 ? "GO!" : 5); // 5 }, (5 - 5) * 500); } // 2回目 { let i = 4 setTimeout(function() { console.log(i === 0 ? "GO!" : 4); // 4 }, (5 - 4) * 500); } // 3回目 { let i = 3 setTimeout(function() { console.log(i === 0 ? "GO!" : 3); // 3 }, (5 - 3) * 500); } // 4回目 { let i = 2 setTimeout(function() { console.log(i === 0 ? "GO!" : 2); // 2 }, (5 - 2) * 500); } // 5回目 { let i = 1 setTimeout(function() { console.log(i === 0 ? "GO!" : 1); // 1 }, (5 - 1) * 500); } // 6回目 { let i = 0 setTimeout(function() { console.log(i === 0 ? "GO!" : 0); // "GO!" }, (5 - 0) * 500); }
setTimeout関数が実行される時には上記のように各ステップで新しくコピーされた変数i
を参照していることになります。
まとめ
どうしてこのような結果になるのか最初理解するのに時間が掛かりました。JavaScriptにおける非同期処理とスコープについてはちゃんと理解しておく必要があります。 間違いがあれがご指摘ください!
参考記事・書籍
TypeScriptの型の互換性について
今回はTypeScriptの型の互換性について取り上げます。
型の互換性・・・??
簡単に言うと例えば、ある型の変数であるHogeに、別の型であるFugaが代入できれば、型の互換性があると言えます。逆に代入できなければ型の互換性がないとなります。
この型の互換性がどのように決まるかは言語によって異なります。TypeScriptの場合は、構造的部分型(Structural Subtyping)が採用されています。
言葉だけで捉えるとなんだか難しそうですね・・・ 実際にコードを見て確認します。
interface Tree { age: number } class Person { age: number } const tanaka: Tree = new Person(); tanaka.age = 20; console.log(tanaka.age); // 20
上記のようにPersonクラスがTreeを実装していなくてもメンバーが同じなのでコンパイルエラーにはならないのです。 これを構造的部分型(Structural Subtyping)というのです。
一方でC#やJavaの場合は、Person
クラスがTree
インターフェースを実装していないといけないので上記のコードではエラーになります。(自分はC#やJavaを触ったことがないので別の機会でお試しください。。。)
このようなものを名目上の型付けと呼びます。
また以下の例はどうでしょうか?
interface User { name: string; } let x: User; let y = { name:"hoge", address: "Tokyo" } x = y;
こちらは正常にコンパイルされます。
変数y
が変数x
のプロパティ(ここでいうとname)をもっているので割り当てが可能になります。
逆にy
にx
を代入した時はどうでしょうか?
interface User { name: string; } let x: User; let y = { name:"hoge", address: "Tokyo" } y = x;
お察しかもしれませんが、y
に対応するプロパティをx
はname
しか持っていないのでコンパイルエラーになります。
関数の場合はどのように型を評価しているのでしょうか?
以下のような関数があります。
let hoge = (a: number) => 0; let fuga = (b: number, c: string) => 0; fuga = hoge;
結果としては、fugaにhogeを割り当て可能です。注意する点としては、引数の名前が異なっていても型のみが考慮されるのです。
hogeの関数の引数名はa
ですが、fugaの関数の引数名はb
とc
です。
上記の例でみたオブジェクトの例では、プロパティ名が異なっていると正しく割り当てされません。
またfuga = hoge
のように引数が切り捨てられるのが許される理由としては、関数の引数が無視されることがJavaScriptでよくみられるからだそうです。
例えば、以下の例です。
const names = ["田中", "佐藤", "中村"]; const result = names.some(name => name === "田中"); // true
someメソッドのコールバック関数のパラメーターは以下の3つを受け取ります。
- 配列の中の値
- 各要素のインデックス
- 要素を格納している配列
このパラメーターを全て使う時もあるとはありますが、使わないケースも多いです。このことから引数の切り捨てられるのが許されるようです。
まとめ
このようにTypeScriptには構造的部分型が取り入れられています。オブジェクトや関数などによって型の評価の仕方が異なるのです。 次回はジェネリクスのことについて取り上げたいと思います。
JavaScriptにおけるfor文のまとめ
今回は言語において基本中の基本であるfor文について取り上げます。
for
forというのはfor three days
など期間を表す意味として使われます。
このようにfor文は決められた回数のなかで何か繰り返し処理を書きたい時に使います。これとは対照的なものとしてwhileなどが挙げられます。
(例)
const names = ["香川", "南野", "中島", "堂安", "柴崎"]; for (let i = 0; i < names.length; i++) { console.log(`サッカー日本代表選手${i}`); } // 実行結果 /* 香川 南野 中島 堂安 柴崎 */
for in
オブジェクトの中から何かを取り出したい時 (例)
const obj = { name: "太郎", age: 23, address: "Tokyo" } for(let item in obj){ console.log(item); } // 実行結果 /* name age address */
for of
配列の中から何か値を取得したい時
(例)
const fruits = ["apple", "orange", "grape", "banana"] for(fruit of fruits){ console.log(fruit); } // 実行結果 /* apple orange grape banana */
forEach
配列の一つ一つのデータに対してコールバック関数を適用することができます。
const numbers = [0, 1, 2, 3, 4, 5]; numbers.forEach((value, index, array) => { ...処理内容 })
コールバック関数の引数は最大で3つ受け取れます。 第一引数は、配列の値、第二引数はインデックス番号、第三引数はforEachの対象となっている配列になります。
注意する点としては、コールバックの関数内でbreakやcontinueは記述することができないということです。
まとめ
for of
やforEach
は配列に対して行うものですが、forEachの場合コールバック関数を使うことで配列内の
値を何かしたいといった時に便利です。一つ一つの構文がどのような役割をもっているかを理解しておくことは大切です。
Javascriptで簡単な画像プレビュー機能を作成してみる
プロフィール画像をアップロードした時などプレビューできるできると便利ですよね? 今日は簡易的なものですが、作っていきます。
こちらアップロードした後の画面です。
準備
必要なものはブラウザとエディタです。それだけ。
見た目を整える
まずは、簡単な見た目の部分から作ります。
<!DOCTYPE html> <html lang='en'> <head> <meta charset='UTF-8'> <meta name='viewport' content='width=1, initial-scale=1.0'> <meta http-equiv='X-UA-Compatible' content='ie=edge'> <title>ファイルプレビュー機能について</title> <link href="https://use.fontawesome.com/releases/v5.0.6/css/all.css" rel="stylesheet"> <link rel='stylesheet' href='style.css'> </head> <body> <div class='wrap'> <div> <label class="file_photo_wrap" for="file_photo"> <i class="fas fa-camera fa-4x"></i> <input type='file' id="file_photo"> </label> <div class="preview_img" id="preview"> <img src='' class="preview_img_content" id="preview_img_data" alt=''> <span class="updated_text">アップロード済み</span> </div> </div> <script src="main.js"></script> </body> </html>
アイコンを使いたかったのでFontawesomeを読み込んでいます。
.wrap { justify-content: center; display: flex; } .file_photo_wrap { display: block; width: 90px; height: 90px; padding: 50px; margin: 100px auto 0 auto; border-radius: 50%; background: #4f4fe9; display: flex; align-items: center; justify-content: center; opacity: 1; transition: opacity .5s; } .file_photo_wrap:hover { opacity: .6; } input[type="file"] { display: none; } .fa-camera { color: #fff; cursor: pointer; } .preview_img { display: none; border: 1px solid #ccc; width: 160px; height: 100px; padding: 10px; margin-top: 20px; } .preview_img.is-active { display: flex; align-items: center; justify-content: center; position: relative; } .preview_img_content { height: 100%; } .updated_text { position: absolute; bottom: -30px; }
プレビュー機能を作成
const target_el = document.getElementById("file_photo"); target_el.addEventListener("change", (e) => { const files = target_el.files; if (e.target.files.length) { const reader = new FileReader(); reader.readAsDataURL(files[0]); reader.onload = () => { document.getElementById("preview").classList.add("is-active"); document.getElementById("preview_img_data").setAttribute("src", reader.result); } } })
このコードではまず2行目でinputタグのchange
イベントを取得します。
target_el.addEventListener("change", (e) => {}
その後3行目のfilesでファイル情報を取得しています。
const files = target_el.files;
ファイルの数が1つでもあればif文の中を通ります。
5行目のFileReader
オブジェクトを使用するとユーザのコンピュータ内にあるファイルを非同期的に読み込むことが出来ます。
以下参考にしてみてください。
FileReader.readAsDataURL()
は
指定されたBlobオブジェクトを読み込みます。
BlobはBinary Large Object の略になります。Blob は文字列や巨大な画像、音声ファイル、動画ファイルなどを扱うことができます。
FileReader.onload
はファイルの読み込みが成功した時に呼ばれるイベントです。
プレビュー画像のimgタグのsrc属性にreader.result
という値が入ります。
このresultというプロパティには、ファイルのデータを示すデータURLが入ります。
とても簡単なものではありますが、これで完成です。
プレビュー機能があるだけでUXの向上に繋がると思います。