reactのhooksを利用した状態管理ライブラリeasy-peasyを使ってみた

最近reactのhooksが話題になっていますね。

そのhooksを利用した状態管理ライブラリを見つけました。

github.com

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

以下は今回使用するライブラリなのでインストールしてください。

これで環境構築は終了です。

このライブラリで使われるのが主にeffectactionです。 effectは副作用のあるもの、非同期にデータを取得する時などに使用します。

フォルダ構成

フォルダ構成は以下のようになっています。(最低限必要な箇所を表示)

├── App.js
├── components
│   ├── Item.js
│   ├── Result.js
│   └── Search.js
├── index.js
├── model.js

storeの作成

githubREADME.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>
  );
};

useStoreuseActionはワード的にもわかりやすいですし、複数の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辺りについて紹介したいと思います。

早速ですが、ライフサイクルはMountingUpdatingUnmountingError Handlingがあります。(今回の記事ではUnmountingとError Handlingについては紹介しません。)

Mounting

reactjs.org

以下のメソッドは、コンポーネントインスタンスを作成してDOMに追加される時に呼び出されます。呼び出される順番は以下の順番です。

  • constructor()
  • static getDerivedStateFromProps()
  • render()
  • componentDidMount()

Updating

reactjs.org

以下のメソッドはコンポーネントが再レンダリングされる時に呼ばれます。

  • 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についてです。

reactjs.org

結論を先に言うとReact.PureComponentshouldComponentUpdateの処理が実装済みとなっています。ですので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について

reactjs.org

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メソッドには同じようにnextPropsnextStateを渡してあげます。そうすることで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.memorecomposeのpureなどがあるので次はこれらについて紹介したいと思います。

JavaScriptの非同期処理とスコープの関係性について

今回非同期処理スコープの関係性について紹介したいと思います。

唐突ですが以下のコードをご覧になってください。

function countdown() {
  let i;
  for (i = 5; i >= 0; i--) {
    setTimeout(function() {
      console.log(i === 0 ? "GO!" : i);
    }, (5 - i) * 500);
  }
}
countdown();

出力結果はどうなると思いますか?

結果は以下になります。

f:id:top_men:20181103122242p:plain

このコードのポイントとしては、変数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();

結果は以下となります。

f:id:top_men:20181103163708p:plain

こちらは意図としたように動きます。 先ほどのコードとの違いは、変数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における非同期処理スコープについてはちゃんと理解しておく必要があります。 間違いがあれがご指摘ください!

参考記事・書籍

js-next.hatenablog.com

www.oreilly.co.jp

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)をもっているので割り当てが可能になります。

逆にyxを代入した時はどうでしょうか?

interface User {
  name: string;
}

let x: User;
let y = { name:"hoge", address: "Tokyo" }
y = x;

お察しかもしれませんが、yに対応するプロパティをxnameしか持っていないのでコンパイルエラーになります。

関数の場合はどのように型を評価しているのでしょうか?

以下のような関数があります。

let hoge = (a: number) => 0;
let fuga = (b: number, c: string) => 0;
fuga = hoge;

結果としては、fugaにhogeを割り当て可能です。注意する点としては、引数の名前が異なっていても型のみが考慮されるのです。 hogeの関数の引数名はaですが、fugaの関数の引数名はbcです。 上記の例でみたオブジェクトの例では、プロパティ名が異なっていると正しく割り当てされません。

また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 offorEachは配列に対して行うものですが、forEachの場合コールバック関数を使うことで配列内の 値を何かしたいといった時に便利です。一つ一つの構文がどのような役割をもっているかを理解しておくことは大切です。

Javascriptで簡単な画像プレビュー機能を作成してみる

プロフィール画像をアップロードした時などプレビューできるできると便利ですよね? 今日は簡易的なものですが、作っていきます。

こちらアップロードした後の画面です。

f:id:top_men:20181010193215p:plain

準備

必要なものはブラウザとエディタです。それだけ。

見た目を整える

まずは、簡単な見た目の部分から作ります。

<!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 オブジェクトを使用するとユーザのコンピュータ内にあるファイルを非同期的に読み込むことが出来ます。

以下参考にしてみてください。

developer.mozilla.org

FileReader.readAsDataURL()は 指定されたBlobオブジェクトを読み込みます。 BlobはBinary Large Object の略になります。Blob は文字列や巨大な画像、音声ファイル、動画ファイルなどを扱うことができます。

FileReader.onloadファイルの読み込みが成功した時に呼ばれるイベントです。

プレビュー画像のimgタグのsrc属性にreader.resultという値が入ります。 このresultというプロパティには、ファイルのデータを示すデータURLが入ります。

とても簡単なものではありますが、これで完成です。

プレビュー機能があるだけでUXの向上に繋がると思います。