20190821のReactに関する記事は6件です。

Go Signinしたときにtokenを作成する

token作成やsessionの部分でつまずいたのでメモです。
SigninしたときにGo側でtokenを作成し、Javascriptのlocalstorageでtokenの情報を保持します。
データベースにaccountsテーブルがあり、email,password,nameなどその他諸々が入ってます。

サインイン画面

スクリーンショット 2019-08-21 13.01.04.png

Go

フレームワークとしてechoを使用しています。
トークンの中身はとりあえずaccount_idとトークンの有効期間を設定するやつです。

handler.go
type token struct {
    AccountID uint
    jwt.StandardClaims
}
signin.go
    // 画面からメールアドレスとパスワードが送られてくるため、リクエストを受け取る構造体を用意してあげる
    type loginParam struct {
    Email    string `json:"email"`
    Password string `json:"password"`
    }

    func (h *handler) loginUser(c echo.Context) error {
    var params loginParam
    err := c.Bind(&params)
    if err != nil {
        return err
    }

    // リクエストとデータベースのemailが一致するレコードをセットする
    var account models.Account
    err = h.DB.Where("email = ?", params.Email).First(&account).Error
    if err != nil {
        return err
    }

    // リクエストされたemailをもつレコードがデータベースに存在しない場合のエラー処理
    if account.ID == 0 {
        return echo.ErrUnauthorized
    }

    // リクエストされたpasswordとデータベースのpasswordが一致しない時のエラー処理
    if params.Password != account.Password {
        return echo.ErrUnauthorized
    }

    // トークン作成
    claims := &token{
        AccountID: account.ID,
        StandardClaims: jwt.StandardClaims{
            ExpiresAt: time.Now().Add(time.Hour * 72).Unix(),
        },
    }

    // 暗号化してtokenにしまう
    token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)

    // 生成された暗号を文字列としてsecretというキーをつけてtにしまう
    t, err := token.SignedString([]byte("secret"))
    if err != nil {
        return err
    }

    return c.JSON(http.StatusOK, map[string]string{"token": t})
}

React

作成したtokenはlocalstorageに保持します。
emailとpasswordだけを送るようにして、リクエストが通ったらlocalstorageにトークンをセットして、トップページに飛ばします。
通らなかった場合は、emailかpasswordが間違っているというエラー文を画面に表示します。

signin.js
handleSubmit() {
    return http
      .post('http://localhost:5000/login', {
        email: this.state.email,
        password: this.state.password
      })
      .then(response => {
        // 暗号化されたトークンをlocalStorageにabc_tokenというキーをつけてセット
        localStorage.setItem('abc_token', response.data.token);
        this.props.history.push(`/cust/toppage`)
        return response.data
      })
      .catch(error => {
        console.log(error)
        this.setState({
          errors: "メールアドレスもしくはパスワードに誤りがあります"
        })
      })
  }

リクエストを送るときに毎回headersにトークンを詰めなければいけないため、axiosを用いて、http.jsに切り出して呼び出す形にしています。

http.js
import axios from 'axios';

const API_HOST = process.env.REACT_APP_API_HOST || 'http://localhost:5000';
// localStorageからabc_tokenというキーでトークンをgetする
const http = axios.create({
  baseURL: API_HOST,
  headers: {
    Authorization: localStorage.getItem('abc_token') || ''
  }
});

export const defaultHttp = axios.create({
  headers: {
    Authorization: localStorage.getItem('abc_token') || ''
  }
});

export default http;

検証

abc_tokenというキーがついた、暗号化されたtokenが保持されているのが確認できます
スクリーンショット 2019-08-21 21.57.01.png

以上です。
今のところ、トークンを作成しただけなので、トークンに紐づいたアカウントの情報をGETする方法などをのちのち追加します。
理解が足りていなくて抜け落ちているところがあるかもしれないので、コメントで教えていただけると幸いです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

expressとreactで作るSPA(1) - reactでhello world!

概要

サーバサイドをexpress、フロントエンドをreactで構成するSPAのWebアプリケーションを作る流れを書いていきます。
Ubuntu 18.04 LTSで必要なものを一からinstallしながら進めていきます。

expressとreactで作るSPA(0) - 準備編

作業ディレクトリを作る

アプリケーションを作るディレクトリを作ります。ディレクトリ名をappとします。

mkdir ~/app
cd ~/app
git init

以下、作業はすべてappディレクトリで行うものとします。

appディレクトリの中はこうなりました。

.
└── .git/

.git にはGitの状態を管理するファイルが入っているので触らないように。

reactをインストール

Yarnを使ってreactをインストールします。

yarn add react react-dom

そうすると何やら動いてappが以下のようになります。

.
├── .git/
├── node_modules/
├── package.json
└── yarn.lock

package.json と yarn.lock はYarnがパッケージの状態を管理するためのテキスト情報が格納されていて、node_modulesは実際にダウンロードしたパッケージが格納されています。

node_modulesをGit管理対象外にする

作成されたファイル・ディレクトリはGitの管理対象になっています。

$ git status
ブランチ master

No commits yet

追跡されていないファイル:
  (use "git add <file>..." to include in what will be committed)

    node_modules/
    package.json
    yarn.lock

node_modulesは、今後もダウンロードしたパッケージがどんどん増えていくのでGitの管理対象になっているとリポジトリのサイズがどんどん大きくなって非常にウザいので、Gitの管理対象から外します。
Gitの管理対象外にするには、.gitignore というファイルを作ってそこに対象外にするファイルのパスを書きます。

.gitignore
node_modules

こうするとGitの管理対象から外れます。

$ git status
ブランチ master

No commits yet

追跡されていないファイル:
  (use "git add <file>..." to include in what will be committed)

    .gitignore
    package.json
    yarn.lock

ここまでで、一旦Gitにcommitします。

git add .
git commit

コーディング

ReactでHello, world!を出力するコードをコーディングしていきます。
脱create-react-app ~ 真面目に express × react 環境を構築する~ を参考にさせてもらいました。

src/client ディレクトリを作成して、その下に以下3つのファイルを作成します。

src/client/index.html
<html>
  <head>
    <meta charset="UTF-8">
    <title>Hello world!</title>
  </head>
  <body>
    <section id="index"></section>
  </body>
</html>
src/client/index.jsx
import React from 'react';
import ReactDOM from 'react-dom';
import Page from './page';

ReactDOM.render(<Page />, document.getElementById("index")); 
src/client/page.jsx
import React from 'react';
import ReactDOM from 'react-dom';

const Page = () => <div>Hello, World!</div>;
export default Page;

reactのコードを動かす

webでプログラムを動かすにはwebサーバが必要です。
最終的にはnode.jsで動かすわけなんですが、いきなりそこまでやると大変なので、webpack-dev-serverという開発・デバッグ用のserverを立てて動かします。

関連パッケージのinstall

yarn add -D webpack-cli webpack webpack-dev-server html-webpack-plugin 
yarn add -D @babel/core @babel/cli @babel/preset-env @babel/preset-react
yarn add -D babel-loader babel-plugin-module-resolver

webpackの設定ファイル

config/webpack.config.js
const HtmlWebPackPlugin = require("html-webpack-plugin");
const path = require('path');

const htmlWebpackPlugin = new HtmlWebPackPlugin({
  template: "./src/client/index.html",
  filename: "./index.html"
});

module.exports =  module.exports = {
  entry: "./src/client/index.jsx",
  output: {
    path: path.resolve('dist'),
    filename: '[name].js'
  },
  module: {
    rules: [
      {
        test: /(\.js$|\.jsx$)/,
        exclude: /node_modules/,
        use: {
          loader: "babel-loader"
        }
      }
    ]
  },
  plugins: [htmlWebpackPlugin],
  resolve: {
    extensions: ['.wasm', '.mjs', '.js', '.json', '.jsx'],
    modules: [path.resolve(__dirname, 'src'), 'node_modules']
  }
}; 

babelの設定ファイル

.babelrc
{
  "presets": [
    "@babel/preset-env", 
    "@babel/preset-react", 
  ],
  "plugins": [
    ["module-resolver", { "root": ["./src/client"] }]
  ]
}

以下のコマンドでwebpack-dev-serverを起動

yarn webpack-dev-server --config config/webpack.config.js

ブラウザで http://localhost:8080 にアクセス -> 「Hello, World!」 表示

お疲れ様でした!

参考

Webpack | Configration
Configure Babel
脱create-react-app ~ 真面目に express × react 環境を構築する~

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

vte.cxによるバックエンドを不要にする開発(その3)

前回=> vte.cxによるバックエンドを不要にする開発(その2)

今回はエントリにユーザ定義項目を追加してデータを表示するところまでをやってみましょう。

エントリに独自項目を追加する

前回の記事では、entryのtitleにHelloという文字列を入れるだけでした。
実はtitleはデフォルトで定義されている項目なのでスキーマ定義は必要ありませんでした。
デフォルト項目は基本的にATOMのメタ情報の項目です。それは以下のものがあります。

  • author、id、published、updatedなどは自動で値がセットされます
  • title、subtitle、summary等はユーザによって自由にセット可能です。
  • contentはコンテンツ(リソース本体)の格納場所として使われます。
  • linkは別名(alias)やキー(self)として使われます。
  • contributorはアクセス権限(ACL)を管理するために使われます。
  • rightsは設定情報などで使われます。この項目は暗号化されます。

詳しくは、ドキュメントの方を参照してください。

デフォルト項目以外でユーザが独自の項目を追加したい場合はスキーマ定義が必要になります。
スキーマ定義は、vte.cx管理画面のエントリスキーマ管理タブから行えます。
以下の画面は、userというユーザ定義項目を追加した様子です。

スクリーンショット 2019-08-21 16.51.35.png

項目名(英語)にuser、日本語名にユーザ、親項目選択に最上位を選択して追加ボタンを押すとuser項目が追加されます。

また、userの子項目である、nameとemailを追加することもできます。
例えば以下のような構造のJSONを登録するためのエントリスキーマを作成してみましょう。


{
   user: {
      name: "bar",
      email: "bar@vte.cx"
   }
}

項目名にname、日本語名に名前、親項目選択にuserを選択して追加ボタンを押します。

スクリーンショット 2019-08-21 17.02.22.png

また、項目名にemail、日本語名にメールアドレス、親項目選択にuserを選択して追加ボタンを押します。

スクリーンショット 2019-08-21 17.03.01.png

すると、エントリ項目一覧では以下のようになります。日本語名やコメントはいつでも更新可能です。

スクリーンショット 2019-08-21 17.03.21.png

次に、以下のコマンドをターミナルから実行してください。

npm run download:template

これにより、スキーマ情報がダウンロードされ、ローカルファイル(setup/settings/template.xml)が更新されます。中身を覗いてみましょう。

template.xml
<?xml version="1.0" encoding="UTF-8" ?>
<feed>
<entry>
  <content>user
 name
 email</content>
  <link href="/_settings/template" rel="self"/>
</entry>
<entry>
  <link href="/_settings/template_property" rel="self"/>
</entry>
  <entry>
    <link href="/_settings/template_property/user" rel="self"/>
    <title>ユーザ</title>
  </entry>
  <entry>
    <link href="/_settings/template_property/user.email" rel="self"/>
    <title>メールアドレス</title>
  </entry>
  <entry>
    <link href="/_settings/template_property/user.name" rel="self"/>
    <title>名前</title>
  </entry>
</feed>

最初のentryのcontentの中で以下のようなスキーマ情報が格納されています。userの下の行に一つスペースを空けてname、その下にemailがあります。一つスペースを空けることで子要素であることを意味します。

user
 name
 email

 これを手修正して更新することもできます。修正したら必ず、npm run upload:templateを実行してサーバを更新してください。ちなみに、サービスを止めることなくスキーマ更新は可能であり登録済のデータが壊れることはありません。ただし、項目名の変更や追加は可能ですが、削除はできません。

データをアップロードする

以下のようなJSONデータを/dataフォルダ上に作成し、npm run upload:dataを実行してください。キーであるlink.___href/foo/2になっていますので、/d/foo/2に登録されるはずです。

sample2.json
[{
  "user": {
    "name": "bar",
    "email": "bar@vte.cx"
 },
  "link": [
      {
          "___href": "/foo/2",
          "___rel": "self"
      }
  ]
}]

登録されたかブラウザで確認してみましょう。http://{サービス名}.vte.cx/d/foo?x&fをブラウザで開いてみてください。以下のように表示されたらアップロード成功です。

スクリーンショット 2019-08-21 17.41.46.png

もし、表示されない場合は、/d/fooフォルダが正しく作成されているか確認してください。
管理画面のエンドポイント管理タブのエンドポイント一覧で以下が表示されていれば作成されています。作成されていなければ新規エンドポイント作成を行ってください。(詳しくは、前回の記事を参照)

スクリーンショット 2019-08-21 17.45.07.png

プログラムからデータを取得して表示する

前回のコードを修正して、登録したデータをプログラムから表示させてみましょう。

index.tsx
import * as React from 'react'
import * as ReactDOM from 'react-dom'
import { useState,useEffect } from 'react'
import axios from 'axios'

const App = () => {
  const [x, f] = useState(0)

  const getdata = async () => {
    try {
      axios.defaults.headers['X-Requested-With'] = 'XMLHttpRequest'
      const res = await axios.get('/d/foo/2?e')
      alert(`res= ${res.data.user.name} `);
    } catch (e) {
      alert('error')
      console.log(e)
    }
  }

  useEffect(() => {
    getdata()
  })

  return (
    <div>
      <button onClick={() => { f(x+1) }}>
       {x} times
      </button>
    </div>
  )

}

ReactDOM.render(<App/>, document.getElementById('container'))

npm run serve:indexを実行するとブラウザが起動し、res.data.user.nameの中身が表示されますので、以下のようにres=barが表示されれば成功です。(errorが表示される場合は、npm run serve:loginを実行してログインしてください)

スクリーンショット 2019-08-21 17.57.30.png

型を定義する

最後に、TypeScriptの型を利用した安全なコードの作成について説明します。

まず、npm run download:typingsで型定義ファイルをダウンロードしてください。vte.cxではダウンロードの際、エントリスキーマの情報を元にTypeScriptの型定義ファイルを自動生成します。
ダウンロードすると、/typingsフォルダの下に、index.d.tsファイルが作成されますので、それを開いてみてください。以下のように、ATOM項目とユーザ定義項目が定義されているのがわかります。

export = VtecxApp
export as namespace VtecxApp

declare namespace VtecxApp {
    interface Request {
        feed: Feed
    }
    interface Feed {
        entry: Entry[]
    }
    interface Entry {
        id?: string,
        title?: string,
        subtitle?: string,
        rights?: string,
        summary?: string,
        content?: Content[],
        link?: Link[],
        contributor?: Contributor[],
        user?:User
    }
    interface Content {
        ______text: string
    }
    interface Link {
        ___href: string,
        ___rel: string
    }
    interface Contributor {
        uri?: string,
        email?: string
    }
    interface User {
        name?:string,
        email?:string
    }
}

次に、先程のソースを編集して型を追加してください。
const entry: VtecxApp.Entry = res.dataが該当の箇所です。
これにより、entry.user.nameがエラーになるので、空チェックを行うif文を追加してください。if (entry.user&&entry.user.name)
これで、より堅牢なコードになりました。

index.tsx
import * as React from 'react'
import * as ReactDOM from 'react-dom'
import { useState,useEffect } from 'react'
import axios from 'axios'

const App = () => {
  const [x, f] = useState(0)

  const getdata = async () => {
    try {
      axios.defaults.headers['X-Requested-With'] = 'XMLHttpRequest'
      const res = await axios.get('/d/foo/2?e')
      const entry: VtecxApp.Entry = res.data
      if (entry.user&&entry.user.name) {
        alert(`res= ${entry.user.name} `);
      }
    } catch (e) {
      alert('error')
      console.log(e)
    }
  }

  useEffect(() => {
    getdata()
  })

  return (
    <div>
      <button onClick={() => { f(x+1) }}>
       {x} times
      </button>
    </div>
  )

}

ReactDOM.render(<App/>, document.getElementById('container'))

今回はこれで以上です。お疲れ様でした。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

React Hooks でページネーションを実装する

やりたいこと

Hooks がリリースされてから、コンポーネントのシンプルな状態管理に関してはuseStateなどで済ませられるようになったが、やや複雑なコンポーネントの状態管理+アルファを Hooks でやってみたかった。

つくったもの

今回はuseReducerを使って市町村名をリストで表示するページネーションを実装してみた。
サンプルコード:https://codesandbox.io/s/paginator-demo-mnxvc
UIフレームワークは Material UI でサクッと用意。

useReducer を使った例としては公式のリファレンスでもカウンターなどがありますが、もう少し実用的で、複雑な状態管理と副作用をうまく利用することが要求されるコンポーネントを作って試してみたいと思っていたので今回それをやりました。

ページネーションを題材とした理由は、主に

  • コンポーネントの操作で非同期にデータをGETする必要性がある
  • ページの始点・終点・中間点等でコンポーネント(「進む」「戻る」などのボタン、ページ位置の表示)の制御を切り替える必要がある
  • 実際のWebアプリケーションでも使用される機会が多い

という点。その他に1ページあたりの表示件数とか、データの表示スタイルの切り替えといったアレンジも加えやすいので、思いつく限り今回は機能を盛り込んでみました。
※今回はサンプルコードを全て公開している都合上、公開APIを叩いて非同期でデータをGETするかわりに静的なデータ(市町村名とIDの一覧)を用意し擬似的に何件かずつarray.sliceで切り出して表示するという実装に変えてあります。

ソースコードとコンポーネントについて

詳しくは上記の CodeSandbox の中身をみていただくとして、今回はページネーションのコンポーネントとReducerの動きについて軽く説明します。

<Paginator />コンポーネント

初期Stateは以下のように設定しています。

index.js
function App() {
  return (
    <div className="App">
      <Container fixed>
        <Paginator
          sum={cities.length} //データの総件数
          per={10} //1ページあたりの表示件数
          initialData={cities.slice(0, 10)} //読み込み時に表示するデータ
          component={ListComponent} //データを渡すPresentaionコンポーネント
        />
      </Container>
    </div>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

<Button> 前へ・次へ

ページの操作を行います。
「前へ」ボタンがクリックされたら前n件のデータとともにviewPreviewという関数を、「次へ」ボタンがクリックされたらviewNextという関数を発行します。

Paginator.js
const reducer = (state, action) => {
  switch (action.type) {
    //[前へ]ボタンをクリック時に発行。
    //ページ数のデクリメント
    case "viewPreview":
      return {
        ...state,
        currentPage: state.currentPage - 1,
        resourceData: action.data
      };
    //[次へ]ボタンをクリック時に発行。
    //ページ数のデクリメント
    case "viewNext":
      return {
        ...state,
        currentPage: state.currentPage + 1,
        resourceData: action.data
      };
    .
    .
    .
  }
};

reducer は発行される直前のstatedispatch()の中身(≒変更するstate)をactionとして引数にとります。変更を加えないstateは...stateとスプレッド演算子を使って return しないと上手くコンポーネントが機能しないため忘れずに。

<Select>1ページあたりの表示件数の切り替え

Selectボックスを変更した場合、setPerAmountという関数を発行します。

Paginator.js
//reducer
    //表示件数の切り替え時に発行。
    //現在のページを1ページ目にリセット。
    case "setPerAmount":
      return {
        ...state,
        currentPage: 1,
        per: action.per,
        pageAmount: Math.ceil(state.sum / action.per),
        resourceData: action.data
      };

表示件数を切り替えた場合は一番先頭のデータからまた表示し直すことが良い?気がするのでactionに入っているデータは先頭からn件の内容です。

<Switch>表示形式(コンポーネントのスタイル)を切り替え

初期状態では市町村名とIDが2段組になったリスト形式で表示していますが、スイッチを切り替えることでシンプルな1行のコンポーネントで現在のページの内容を表示し直します。
スイッチの切り替え時にデータを1行ずつ表示するための子コンポーネントとswitchComponentという関数を発行します。

Paginator.js
//reducer
    //表示形式の切り替え時に発行。
    case "switchComponent":
      return {
        ...state,
        component: action.component
      };

最後に

公式リファレンスを読むと使い方がなんとなくわかった気になりますが、実務ではカウンターやTodoリスト以上に複雑な機能を扱うことの方が多いためどういう場面で Hooks の力を最大限利用できるのかはまだ手探り中です。今回はuseContext抜きで実装しましたが、扱うコンポーネントの数が増えてきたら避けては通れなくなるかも。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

redux-thunk入門!thunkとは?なぜ必要?新人が書く簡単まとめ

はじめに

(ちょっと分かりやすくように再編しました)
昔ReactとReduxを触ったこと全然ないので、インターンはじめて以来、ただ「こういう風に書くもの」、「このように書くのは正しい」としか認識していない状況でした。そもそもどの部分が redux-thunk なのかも全然分かってません。
なので、今回は redux-thunk 公式に推奨されてる文章を読んで、自分なりに理解した後、記録してみます。
全文を翻訳するわけではないので、詳細はそちらに参照していただければと思います。
やはり、ライブラリーは何故生まれたのか、どういう問題を解決したのか、それを理解するのは大事なことだと思います。

Thunkとは

一般的にいうと、functional programmingのテックニックの一つで、そのまま関数Aを利用するではなく、まず函数Bに変数を提供して、関数Bはそれを使って関数Aの中身を完成させる。最後は完成した関数Aを返す、必要の時でそれを呼び出すみたいな感じ。

コードだとこういう感じ。

function yell (text) {
  console.log(text + '!')
}

yell('bonjour') // 'bonjour!'

function thunkedYell (text) {
  return function thunk () {
    console.log(text + '!')
  }
}

const thunk = thunkedYell('bonjour') // まだ実行されてない

thunk() // 'bonjour!'

で、React / Redux はどういう風にThunkの仕組みを利用しているのかというと、主にはactionsaction creatorscomponentsが「直接的に」side effectsを起こさせないようにしている。
それらの処理は全部Thunkに包んで、そのあとmiddlewareがThunk呼ぶ時に実行される。
このようの仕組みだと、少なくともMiddlewareレベル以外のところは比較的にpureになるので、メンテナンス、テスト、読みやすさでは役に立つ。

基礎&作られた原因

原生のreduxだけ利用すると遭う問題

Reduxのdispatchはaction objectを引数にしてる。

const LOGIN = 'LOGIN'
store.dispatch({ type: LOGIN, user: {name: 'Lady GaGa'} })

だが、Asyncのリクエストが組み込まれた場合はそれをできなくなる。(Axiosを例としている)

const asyncLogin = () =>
  axios.get('/api/auth/me')
  .then(res => res.data)
  .then(user => {
    // このuserはどうやって利用する?
  })

// componentのどこかで:
store.dispatch(asyncLogin()) // こういう風にはできない; `asyncLogin()` は promise、actionではない

解決法

一見では、async handlerの中で store.dispatch 呼ぶことで解決できるが:

import store from '../store'

const simpleLogin = user => ({ type: LOGIN, user })

const asyncLogin = () =>
  axios.get('/api/auth/me')
  .then(res => res.data)
  .then(user => {
    //直接Storeに送るか!
    store.dispatch(simpleLogin(user))
  })

// componentのどこかで:
asyncLogin()

さて、これだといくつかの問題に遭う。

問題1:Inconsistent API

このように書くと、componentsの中の処理は二種類に別れられる。
一つはstore.dispatch(syncActionCreator())を呼ぶ処理と、
doSomeAsyncThing()の処理。
それでは一致性を失うことになる。

後者の処理ではdispatch関連の部分がどう処理されてるのかのを理解しにくい。
それに、もし処理をsyncからasyncに変更する場合(逆も然り)は、componentの中の関連記述も修正しなければならないので、メンテナンスでは非常に面倒い。

問題2:Impurity

前述したasyncLoginは明らかにpure functionではない、それ自体はまあ仕方がないが、componentが直接にside effectsを起こすと、一見では悟りにくいので、メンテナンス上でも、unit testing上(例えばaxiosをmockする)でもよしとされない。

問題3:Tight Coupling

asyncLogin(action creator)の利用は一つ特定のstoreに限定されている。リユースできない。

Thunkでの解決法を試みる

Thunkを使って、network関連の処理は「直ちに」実行させない、
その代わりにThunkの中に包んで、Thunkを返す。

import store from '../store'   // 今はバインドしているのでTight Coupling

const simpleLogin = user => ({ type: LOGIN, user })

const thunkedLogin = () =>    
  () =>                        
    axios.get('/api/auth/me')
    .then(res => res.data)
    .then(user => {
      store.dispatch(simpleLogin(user))
    })

// componentのどこかで:
store.dispatch(thunkedLogin()) // thunk自体をStoreに送…れるのか?

これでは、問題1のdoSomeAsyncThing()は存在しなくなる、thunkedLoginも比較的にpureになる(実行される時にThunkを返すだけなので)。

でも待って、action creatorが返すのはaction objectではないので、Reduxは理解できないはず。それと問題3も放置されたままでは?

確かに、なので、これからはredux-thunkの出番だ。

Redux-Thunk Middleware

redux-thunkがインストールされると、Dispatchはこのように変えられる:

actionOrThunk =>
  typeof actionOrThunk === 'function'
    ? actionOrThunk(dispatch, getState)
    : passAlong(actionOrThunk);

ようするに:
- Dispatchされたのは一般のaction objectだと、そのまま通す。
- Dispatchされたのは関数(すなわちthunk)になると、その関数にstoredispatchgetStateを渡して実行させる。

これだと、Reduxがもらったものは確かaction objectになる!

あとは、Tight Couplingの問題が残っているが、
それもredux-thunkがdependency injectionを通じて解決できた。

どういうことかというと、
前の例では、storeがcomponentで指定されて、変えられない状態になっている。
で、redux-thunkはThunkにdispatch(とあるstoreのdispatch関数)、 getState渡しているので、違うdispatchを渡すともちろん違うstoreに行く。

ちなみに普通のThunkは引数を取らないが、redux-thunkではその型を破った。

これで、今まであった問題はすべて解決しました!

終わりに

何回も読みながら情報整理した後、ようやくredux-thunkの基礎を理解した…気がしなくてもない。
本来の文章では他の内容ものってるので、よければそちらも参照してみてはいかがでしょうか。
あとは日本語で書くのが時間かかるので疲れてました…

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Reactでメモアプリを作る(React基礎講座9)

はじめに

今回は、Reactを使って、メモを追加・削除ができる簡単なメモアプリを作成していきます。

挙動は以下のような感じです。

116d349e1defb23e1458f5359e3a85c6.gif

2a9d4baca00d88f7bf4ef1ab914e5438.gif

シリーズ

本記事はReact基礎講座のための連載になっています。気になる方のために、前の章は以下です。

React開発で見かける配列処理系のメソッド map , filter について(React基礎講座8) - Qiita

最初の記事は、以下です。

Reactを使ってJSXの内容をレンダリングする(create-react-app)(React基礎講座1) - Qiita

メモアプリとは

仕様は以下のようなシンプルなReactアプリケーションです。

  • テキストエリアにテキストを入力して、入力ボタンを押したらその内容が一覧で表示される
  • メモの一覧にはそれぞれ、削除ボタンがあり、それを押したらそのアプリが消える
  • メモはDBには格納されない

ファイル構成

root/
 ├ public/
 └ src/
   └ components/
   |  └ Form.js
   |  └ List.js
   └ memoApp.js
   └ index.js

  • index.js
    • React.ComponentであるmemoAppをimportする
  • MemoApp.js
    • stateとして以下の情報を初期設定します
      • memos: 具体的なメモの情報
      • nextId: 次に追加するメモのidの情報
        • stateを持たせるので、クラスコンポーネントで実装します
    • メモを保存する機能 addMemo と、削除する機能 deleteMemoを作る
      • これら2つの機能がstateを変更させます
    • コンポーネント Form.jsList.js をimportする
      • これらも今回はクラスコンポーネントで実装しましょう
  • /components/Form.js
    • メモのフォーム部分の見た目を作る
  • /components/List.js
    • メモ一覧の見た目を作る

コンポーネントの配置

まずは、ファイル構成どおりにコンポーネントを配置していきましょう。

まずは、index.js

index.js
import React from "react";
import { render } from "react-dom";

import MemoApp from "./MemoApp";

render(<MemoApp />, document.getElementById("root"));

次に、MemoApp.js

このコンポーネントから、<Form /><List />をimportして呼び出していることが分かります。

MemoApp.js
import React from "react";
import { render } from "react-dom";

import Form from "./components/Form";
import List from "./components/List";

class MemoApp extends React.Component {
  constructor(props) {
    super(props);
  }

  render() {
    return (
      <div>
        <h2>MemoApp</h2>
        <Form />
        <List />
      </div>
    );
  }
}

export default MemoApp;

続いて、FormListコンポーネントを作成していきます。

Form.js
import React from "react";
import { render } from "react-dom";

class Form extends React.Component {
  constructor(props) {
    super(props);
  }

  render() {
    return <h3>Form</h3>;
  }
}

export default Form;
List.js
import React from "react";
import { render } from "react-dom";

class List extends React.Component {
  constructor(props) {
    super(props);
  }

  render() {
    return <h3>List</h3>;
  }
}

export default List;

スクリーンショット 2019-08-19 00.05.46.png

すると、こんな感じで作成できましたかね?これで、下準備というか、コンポーネントの配置は完了です。

MemoAppにStateを定義してListで表示

MemoAppにStateを定義していきます。

前出しましたが、定義するStateは2種類です。

  • memos: 具体的なメモの情報
    • 配列の中に連想配列を持たせましょう(keyは、idcontentにしましょう)
    • これ、最終的にはテキストボックスからcontentを生成しますが、今回はコンポーネントの見た目確認のため、Stateに定義します
  • nextId: 次に追加するメモのidの情報

MemoAppでは、<List />コンポーネント部分にstateのmemosを渡して、List.jsで表示させるようにしましょう。

List.jsでは、propsで、MemoAppにStateとして定義したmemosを要素全て表示させましょう。
その時に、使用するメソッドは....mapでしたね。

それでは、実装していきましょう。まずは、MemoApp.jsから。

MemoApp.js
import React from "react";
import { render } from "react-dom";

import Form from "./components/Form";
import List from "./components/List";

class MemoApp extends React.Component {
  constructor(props) {
    super(props);

    this.state = {
      memos: [
        { id: 1, content: "one" },
        { id: 2, content: "two" },
        { id: 3, content: "three" },
        { id: 4, content: "four" },
        { id: 5, content: "five" }
      ],
      nextId: 0
    };
  }

  render() {
    return (
      <div>
        <h2>MemoApp</h2>
        <Form />
        <List memos={this.state.memos} />
      </div>
    );
  }
}

export default MemoApp;

こちらは、Stateを定義して、それをStateとして<List />コンポーネントにmemosとして渡します。

List.jsでは、MemoApp.jsからStateに乗って渡ってきたmemosmapで要素ごとに処理した結果を変数listコンポーネントに入れて、その下のrenderメソッドで{list}として受け取ります。

List.js
import React from "react";
import { render } from "react-dom";

class List extends React.Component {
  constructor(props) {
    super(props);
  }

  render() {
    const list = this.props.memos.map(memo => {
      return (
        <li>
          #{memo.id} - {memo.content}
        </li>
      );
    });
    return (
      <div>
        <h2>List</h2>
        {list}
      </div>
    );
  }
}

export default List;

こんな感じですね。見た目は、こんな感じになります。

スクリーンショット 2019-08-19 22.56.31.png

Form.jsを作成する

今度は、メモを作成するFormのコンポーネントを作成します。

Form.js
import React from "react";
import { render } from "react-dom";

class Form extends React.Component {
  constructor(props) {
    super(props);

    this.state = { content: "content" };
  }

  render() {
    return (
      <div>
        <h2>Form</h2>
        <input value={this.state.content} />
        <input type="submit" value="Add Memo" />
      </div>
    );
  }
}

export default Form;

ただ、このままだと、テキストエリアの値がいつまでも変わらないので、テキストエリア内の値が更新された場合、その変更後の値をテキストエリアに表示させます。テキストエリア内の値のstateが変更されたことを検知するイベントハンドラといえば、onChangeですね。これは、フォーム内の要素の内容が変更された時に起こるイベントハンドラです。

これをハンドラにして、メソッドhandleChangeを呼び出すような処理を書きましょう。handleChange関数の引数にeventをようして、関数内でevent.target.valueと記述すると、イベントの結果更新された値を取得することができます。それを変数contentとして格納する。

その変数(content)を使って、setStateしてフォーム内の値(state)を更新します。つまり、handleChange関数を使って自分自身の値を更新させていることがわかります。

それでは、実装していきましょう。

Form.js
import React from "react";
import { render } from "react-dom";

class Form extends React.Component {
  constructor(props) {
    super(props);

    this.state = { content: "content" };
  }

  render() {
    return (
      <div>
        <h2>Form</h2>
        <input value={this.state.content} onChange={this.handleChange} />
        <input type="submit" value="Add Memo" />
      </div>
    );
  }

  handleChange = event => {
    const content = event.target.value;
    this.setState({ content: content });
  };
}

export default Form;

次に、submitした際の挙動を書いていきましょう。inputタグをformタグで囲って、<input type="submit" value="Add Memo" />をクリックした際の処理(関数を)formの開始タグに記述します。その際のハンドラは、onSubmitですね。onSubmitの詳細は、以下の記事を見て見てください。

参考

https://www.sejuku.net/blog/28720

onSubmitで検知した際に、処理させる関数名はhandleSubmitとでもしましょう。今回も引数eventを受け取るようにしましょう。

処理内容は、一旦以下のようにします。

  • まずは、submitをクリックした際のデフォルトの挙動をしないようにする
  • 次に、アラートにて、現状のthis.state.contentの値を表示させるようにする
  • アラートで、現状のcontentを表示させたら、現状のテキストエリアの値は何も無いことにします

こんな感じです。では、実装していきましょう。

Form.js
import React from "react";
import { render } from "react-dom";

class Form extends React.Component {
  constructor(props) {
    super(props);

    this.state = { content: "content" };
  }

  render() {
    return (
      <div>
        <h2>Form</h2>
        <form onSubmit={this.hamdleSubmit}>
          <input value={this.state.content} onChange={this.handleChange} />
          <input type="submit" value="Add Memo" />
        </form>
      </div>
    );
  }

  handleChange = event => {
    const content = event.target.value;
    this.setState({ content: content });
  };

  hamdleSubmit = event => {
    event.preventDefault();
    alert(this.state.content);
    this.setState({ content: "" });
  };
}

export default Form;

こんな感じですね。挙動は、こんな感じになります。

b044a7af7e18e4f869ff43618be34f3e.gif

ここまでできれば一旦OKです。

なので、Form.jsのコンストラク内のstateのcontentで定義している文字列は空白でもいいかもですね。

次は、いよいよ、アラートを出すのではなく、実際に、stateを変更して、Listコンポーネントを変更するような機能を作成していきます。

stateを変更する機能を作成する

今度は、テキストフォームにメモの内容を記述したら、Listコンポーネントにその値が反映されるようにしましょう。

手順は以下のような感じです。

  • (色々なやり方がありますが今回は)MemoApp.jsでテキストを入力してボタンを押したら
    • 関数(addMemo)が走るようにしましょう
      • 関数(addMemo)自体は、
        • setStateが走るようにして
        • その中でstate(memosの配列にSpread operatorで既存のものを残しつつ、新たな要素を追加します)
        • contentの内容はForm.js内のhamdleSubmit関数の中で引数として渡します
        • 宣言元のaddMemoは引数としてhamdleSubmit関数から渡ってきたメモ内容(this.state.content)を引数(content)として受け取って、Spread operator(3点)の後にカンマして、あらたなオブジェクトに渡す
        • そして、idもthis.state.nextIdもセットして、その後のsetState内で 1たす項目を設けます
  • 既存のstate(memos)にsubmitした際、要素が加われば、コンストラクタで定義した既存のstate(memos)は消しましょう

JSでは、Spread operator(スプレッド構文)は頻出シンタックスなのでぜひ覚えてください。

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Operators/Spread_syntax

また、今回実装したstateを変更するメソッド(addMemo)を親(MemoApp.js)に持たせて、それを子(Form.js)に渡して実行する方法はよくあるので難しいですが手に馴染ませてください。

子に渡っているのはhamdleSubmit関数の中のthis.props.addMemo(this.state.content); ← この部分ですね。

それでは、実装していきましょう。

MemoApp.js
import React from "react";
import { render } from "react-dom";

import Form from "./components/Form";
import List from "./components/List";

class MemoApp extends React.Component {
  constructor(props) {
    super(props);

    this.state = {
      memos: [],
      nextId: 0
    };
  }

  addMemo = content => {
    this.setState({
      memos: [...this.state.memos, { id: this.state.nextId, content: content }],
      nextId: this.state.nextId + 1
    });
  };

  render() {
    return (
      <div>
        <h2>MemoApp</h2>
        <Form addMemo={this.addMemo} />
        <List memos={this.state.memos} />
      </div>
    );
  }
}

export default MemoApp;
Form.js
import React from "react";
import { render } from "react-dom";

class Form extends React.Component {
  constructor(props) {
    super(props);

    this.state = { content: "" };
  }

  render() {
    return (
      <div>
        <h2>Form</h2>
        <form onSubmit={this.hamdleSubmit}>
          <input value={this.state.content} onChange={this.handleChange} />
          <input type="submit" value="Add Memo" />
        </form>
      </div>
    );
  }

  handleChange = event => {
    const content = event.target.value;
    this.setState({ content: content });
  };

  hamdleSubmit = event => {
    event.preventDefault();
    this.props.addMemo(this.state.content);
    this.setState({ content: "" });
  };
}

export default Form;

こんな感じですね。挙動は、こんな感じになります。

116d349e1defb23e1458f5359e3a85c6.gif

stateを削除する機能を作成する

次は、個々のメモを削除するような機能を作成していきます。

手順は以下のような感じです。

  • List.jsのコンポーネントにタグを設置
  • 機能自体はMemoApp.jsaddMemoの下にdeleteMemoメソッドを作成していきましょう
    • deleteMemoメソッドは引数にmemosのidを取れるようにしてください
  • では、上記の呼び出しをList.jsに定義したbuttonタグにハンドラはonClickとして、アロー関数をdeleteMemoメソッドのコールバックとして実行してください
  • コール先の関数の中の処理ですが
    • 配列(memos)をfilterで処理して、引数として受け取ったid以外をTRUEとして返すような条件でにして、結果を変数filteredArrayに格納し、それをmemosのsetStateとして渡す

それでは、実装していきましょう。

MemoApp.js
import React from "react";
import { render } from "react-dom";

import Form from "./components/Form";
import List from "./components/List";

class MemoApp extends React.Component {
  constructor(props) {
    super(props);

    this.state = {
      memos: [],
      nextId: 0
    };
  }

  addMemo = content => {
    this.setState({
      memos: [...this.state.memos, { id: this.state.nextId, content: content }],
      nextId: this.state.nextId + 1
    });
  };

  deleteMemo = id => {
    const filteredArray = this.state.memos.filter(memo => {
      return memo.id !== id;
    });
    this.setState({ memos: filteredArray });
  };

  render() {
    return (
      <div>
        <h2>MemoApp</h2>
        <Form addMemo={this.addMemo} />
        <List memos={this.state.memos} deleteMemo={this.deleteMemo} />
      </div>
    );
  }
}

export default MemoApp;
react.List.js
import React from "react";
import { render } from "react-dom";

class List extends React.Component {
  render() {
    const list = this.props.memos.map(memo => {
      return (
        <li>
          #{memo.id} - {memo.content}{" "}
          <button onClick={() => this.props.deleteMemo(memo.id)}>delete</button>
        </li>
      );
    });
    return (
      <div>
        <h2>List</h2>
        <ul>{list}</ul>
      </div>
    );
  }
}

export default List;

こんな感じですね。挙動は、こんな感じになります。

2a9d4baca00d88f7bf4ef1ab914e5438.gif

冗長な部分をリファクタ

冗長な書き方になってしまっている部分をconst { ... } = this.state;propsでも同様)と定義してリファクタしてあげましょう。

また、この記述import { render } from "react-dom";が不要なファイルはあるので、不要であれば消します。

そして、

Warning: Each child in a list should have a unique "key" prop.

というエラー対応をします。具体的には、Listコンポーネント’にある

タグにidを追加します。
<li key={memo.id}>

こんな感じですね。

それでは、実装していきましょう。

MemoApp.js
import React from "react";

import Form from "./components/Form";
import List from "./components/List";

class MemoApp extends React.Component {
  constructor(props) {
    super(props);

    this.state = {
      memos: [],
      nextId: 0
    };
  }

  addMemo = content => {
    const { memos, nextId } = this.state;
    this.setState({
      memos: [...memos, { id: nextId, content: content }],
      nextId: this.state.nextId + 1
    });
  };

  deleteMemo = id => {
    const { memos } = this.state;
    const filteredArray = memos.filter(memo => {
      return memo.id !== id;
    });
    this.setState({ memos: filteredArray });
  };

  render() {
    const { memos } = this.state;
    return (
      <div>
        <h2>MemoApp</h2>
        <Form addMemo={this.addMemo} />
        <List memos={memos} deleteMemo={this.deleteMemo} />
      </div>
    );
  }
}

export default MemoApp;
List.js
import React from "react";

class List extends React.Component {
  render() {
    const { memos, deleteMemo } = this.props;
    const list = memos.map(memo => {
      return (
        <li key={memo.id}>
          #{memo.id} - {memo.content}{" "}
          <button onClick={() => deleteMemo(memo.id)}>delete</button>
        </li>
      );
    });
    return (
      <div>
        <h2>List</h2>
        <ul>{list}</ul>
      </div>
    );
  }
}

export default List;

長くなりましたが、Reactを使ったメモアプリ作成は以上です。参考にしてみてください。

参考

  • 改訂新版JavaScript本格入門 ~モダンスタイルによる基礎から現場での応用まで | 山田 祥寛
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む