20200710のReactに関する記事は8件です。

【技術書まとめ】りあクト! TypeScriptで始めるつらくないReact開発

普段はRailsを使って開発しているが個人プロダクトでReactを使ってみたかったので本書を手にとった。
振り返った時に思い出せるように、簡単にまとめていく。

第1章 こんにちはReact

環境構築からyarnの説明までreactの概要。

ndenvが非推奨になっていたため、nodenvを入れる。
参考にさせてもらった記事。
https://qiita.com/silmisilon/items/a2cc637eb7e59fa13f2c

第2章 ナウでモダンな JavaScript

新しいJSの構文紹介。

  • varは使わない
    • ブロックをすり抜けるから不要なバグを生む

第3章 関数型プログラミングでいこう

非同期通信がほとんどなJSでは状態変化に人間の脳がついていけない。だから関数型が適する。関数型プログラミングの概要。

第4章 型のあるTypeScriptは強い味方

TypeScriptの概要。

  • ストリングリテラル型(文字列リテラル型)
    • let pet: 'cat' | 'dog' | 'rabbit' = 'dog';
    • enumようにも使える
  • constは再代入できないが要素の上書きや追加はできてしまう
    • そのためReadonlyArray<string>がある

第5章 拡張記法JSX

JSXの実際の使い方。ifコメントなど。

第6章 LintとPrettierでコードをクリーンに

・ eslint 
・ eslint-config-airbnb
・ eslint-config-prettier
・ eslint-plugin-import
・ eslint-plugin-jest
・ eslint-plugin-jsx-a11y
・ eslint-plugin-prettier
・ eslint-plugin-react
・ eslint-plugin-react-hooks
・ @typescript-eslint/eslint-plugin
・ @typescript-eslint/parser
・ stylelint
・ stylelint-config-prettier
・ stylelint-config-standard
・ stylelint-order
・ prettier

第7章 何はなくともコンポーネント

  • Reactの基本思想

    • 仮想DOM
    • コンポーネント指向
    • 単方向データフロー
  • LocalStateのthisの違い

    • アロー関数にすると実行時オブジェクトとなる
  • e: SyntheticEventという引数

    • e.preventDefault();で元々の挙動を抑えられる
  • updateされるのは2つの場合のみ

    • Propsの変更
    • LocalStateの変更
  • コンポーネントのライフサイクル

    • componentDidMount()
    • shouldComponentUpdate()
    • componentDidUpdate()
    • componentWillUnmount()
    • バージョン16.3から17にかけて大きく変更
  • クラスコンポーネントよりも関数コンポーネントで書く

    • クラス内のthisの挙動が難解
    • ライフサイクルメソッドの挙動が複雑
    • 導入予定の各種最適化のため
  • コンポーネントとコンテナ

    • 見た目のコンポーネント
    • 機能のコンテナ

第8章 Hooksで関数コンポーネントを強化する

Hooksでのライフサイクル相当のもの

useEffect(() => {
  doSomething();

  return clearSomething();
}, [watchVar]);
  • HooksのAPIで重要なもの
    • useState
    • useEffect

第9章 ルーティングでURLを制御する

  • SPAでは原則的にサーバーへリクエストが飛ぶことはない
  • React Router ではルーティング遷移時にスクロール 位置が変わらない
    • componentDidMount()window.scrollTo(0, 0) を仕込 んだコンポーネントを作る
    • DOM ツリーの上のほうでマウントさせる

第10章 Reduxでアプリの状態を管理する

  • MVCはインタラクティブなフロントエンドには適さない
    • データの方向を単方向にしたい
      • Fluxパターン
  • Reduxの三つの原則
    • Single source of truth (信頼できる唯一の情報源)
    • State is read-only (状態は読み取り専用)
    • Changes are mode with pure functions (変数は純粋関数で行なわれる)
      • Reducer
        • (prevState, action) => newState
        • Actionが同じなら新旧の差分も同じという保証
  • Reduxは導入すべきか
    • コンポーネントをまたいでデータを共有しない業務アプリはない
    • 一度導入すれば以降はStore Stateで管理できる
    • 初めからReduxに任せるべき
    • 背景や思想を知る
  • Flux Standard Action
    • 適宜使う

第11章 Reduxで非同期処理を扱う

非同期通信するための方法について

  • Redux Thunk
    • Action CreatorにAPI連携を書く
    • 「理解しやすく導入しやすいけど、放っておくとどこまでもコードが汚くなる」
  • Redux-Saga
    • 開始・成功・失敗を定義する
    • 「綺麗に書けるけど、学習コストが高くてコード量も増える」

読後まとめ

とてもいい本だった。React、Hooks、Redux、などの解説が丁寧なのに加えて、それらが開発された背景や変遷なども詳細に説明してくれるので、機能の理解だけでなく「腑に落ちる」という気持ちにさせてくれた。

商業ベースの本ではここまで突っ込んだ内容にできないだろうし、やはりフロントエンド界隈のような鮮度の高い情報を必要とする世界ではこういった形態の書籍がこれからも有用になってくるのかなと思った。

ぜひ著者の次の本も読みたい。

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

ゼロからのReact Native環境構築

はじめに

今回はExpo cliを使ってReact Nativeの環境構築を行っていきます。
環境構築のゴールはシミュレーターに"Open up App.js to start working on your app!"と出ることです。

目次

1.Node.jsをインストール
2.シュミレーターをインストール
3.Expoでプロジェクト作成
4.プロジェクトを起動

1. Node.jsをインストール

Node.jsのパッケージ管理システムであるnpmでExpo cliをインストールするので、ローカル環境にNode.jsが入っていない人はインストールしてください。
Node.js公式

2. シュミレーターをインストール

スマホアプリを作るにはシュミレーターもインストールする必要があります。
IOSアプリ開発の場合は、「Xcode」(Macユーザーのみ)
Androidアプリ開発の場合は、「Android Studio

Xcodeの設定

MacユーザーはApp StoreでXcodeをダウンロードし、Xcode->PreferenceからCommand Line Toolsを選択してください。
スクリーンショット 2020-07-09 17.40.04.png
そして、Componentsから一番最新のSimulatorをインストールしてください。
ここでXcodeの設定は完了です。
スクリーンショット 2020-07-09 17.41.47.png

Android Studioの設定

Android Studioを起動し、⚙ConfigureからSDK Managerを選択し、以下の写真のようにパッケージが選択してあれば大丈夫です。
スクリーンショット 2020-07-09 18.04.46.png
スクリーンショット 2020-07-09 18.05.25.png
Windowsユーザーはここで設定完了です。以下からはMacユーザーだけの追加設定をしていきます。

pathの設定(Macのみ)

ターミナルを開いて、以下のコマンド入力します。

cd ~/.bash_profile

次に、Android StudioのSDK ManagerのAndroid SDK Locationに書かれているpathを以下のようにコマンドする。

.bash_profile
export ANDROID_SDK=/Users/kajigaya/Library/Android/sdk
export PATH=/Users/kajigaya/Library/Android/sdk/platform-tools:$PATH

最後に、.bash_profileに追加したpathを.zshrcを開いて、コピペする。

Macユーザーのpath設定

AVD Managerの設定

Android studioの⚙ConfigureからAVD Managerを選択し、+Create Virtual Deviceを選択します。スクリーンショット 2020-07-10 10.19.35.png
使用したいハードウェアを選択し、Q Downloadでダウンロードを開始し、完了です。
(使用するときは、使いたいハードウェアの▶️ボタンを押します)
スクリーンショット 2020-07-10 10.21.38.png

3. expoでプロジェクト作成

今回は、ディスクトップにプロジェクトを作成したいので、ディスクトップに移動し、npmでexpoをインストールします。

cd ~/Desktop
Desktop
npm install -g expo-cli

expoをインストール完了したら、expoでプロジェクトを作成します。

Desktop
expo init プロジェクト名

インストール中、いろいろと選択する必要がありますが、基本的にEnterで構いません。

4.プロジェクトを起動

まず、作成したプロジェクトに移動します。そこでプロジェクトを立ち上げます。

プロジェクト名
cd ~/Desktop/プロジェクト名
npm start

ブラウザが立ち上がり、Run on iOS simulatorを選択します。
スクリーンショット 2020-07-10 12.41.40.png
このような画面が立ち上がれば環境構築完了です。スクリーンショット 2020-07-10 18.43.38.png
お疲れ様でした。

参考資料

React Native公式
Expo公式 MacユーザーのためAndroid Studioの設定
Android Studio公式 Android Studioのインストール方法

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

日本一わかりやすいReact-Redux講座 実践編 #3

はじめに

この記事は、Youtubeチャンネル『トラハックのエンジニア学習ゼミ【とらゼミ】』の『日本一わかりやすいReact-Redux講座 実践編』の学習備忘録です。

前回記事はこちら

要約

  • 認証状態の永続化のため、認証のリッスン機能を作成する
  • 認証のリッスン機能を持たせた<Auth>コンポーネントで、リッスン対象をラッピングする
  • パスワードリセット機能は、firebase.authの機能で簡単に実装できる。

#3 Firebase Authで認証をリッスンしよう

認証状態の永続化

前回講座までで「サインアップ」「サインイン」の機能を実装しましたが、今の状態ではまだ不完全です。

例えば、サインイン直後ではユーザーの情報が state に正しく保存されているので、Home画面にはユーザーIDやユーザー名が表示されます。

しかし、この状態で画面をリロードしたり、ブラウザを立ち上げ直して再度アクセスすると、 state は初期状態に戻ってしまいます。

これは、React の state は、画面をリロードしたり、ブラウザを立ち上げ直したりすると初期化されてしまう性質を持っているからです。

これでは、 state の初期化が走るたびにサインインし直す必要があり、Webアプリとしては非常に使い勝手が悪くなってしまいます。

そのため、「ユーザー認証がすでに行われている状態であれば、適宜データベース側と通信してユーザー情報を state に保存する」という処理を入れ必要があります。

これを実現するのが認証のリッスンという処理なのですが、その前に一度、React+firebaseにおける認証の仕組みを簡単に整理しておきます。

React + firebase による認証の仕組み

そもそも firebase.Auth では、ユーザーの認証情報をどのように判断するのでしょうか?言い換えると、判断元となるユーザーの認証情報は、どこに保存されているのでしょうか?

こちらのteratail記事(『firebase.auth().onAuthStateChangedはどうやってログイン中であることを判定しているんでしょうか?』)によると、firebase.Auth は「サインアップまたはサインイン処理を行った時、ユーザーの認証情報をindexedDBという、ブラウザ内にあるデータベースに保存する」とのことです。

これは、「ブラウザ内にある情報を保存する場所」という意味ではlocalStorageに近い役割のものです。これらの違いについて詳しくは調べていませんが、ここで大切なのは「ブラウザ側に認証情報が保存される」ということです。

そのため、React+firebaseアプリにおいて、

  1. React の state に保存されているユーザー情報
  2. firebaseにより ブラウザのindexedDBへ保存された認証情報

がそれぞれ存在しているということになります。

ユーザー情報を画面描画に使用するためには、当然 React の state に値を入れる必要がありますが、これは画面リロードのたびに消えてしまいます。

対して、indexedDBに保存された情報は、画面リロードやブラウザ立ち上げなどでは削除されません。ただし、ここで保存している認証情報は、ユーザー名やメールアドレスなどのユーザー情報そのものではなく、「データベース(Cloud Firestore)から特定のユーザー情報を引き出すための鍵」のようなもので、そのまま使用することができないものです。

そのため、これらを組み合わせた以下のような流れで「認証状態の永続化」を実現します。

  1. 画面のリロードなどが行われた時、まずindexedDBを見て認証の有無を確認。
  2. 未認証なら initialState を state に入れて画面描画。認証済みであれば、indexedDBに保存された認証上を用いてCloud Firestoreと通信し、特定のユーザーの情報を取ってくる。
  3. 取ってきたユーザー情報を state に入れて画面描画する

このような流れを実装することで、サインインを行った特定のユーザーのサインイン状態が永続化し、何度もサインインをし直すという状態を防ぐことができます。

この今、ユーザーが認証しているかどうかを確認し、state に適切な値を入れる一連の作業のことを、認証のリッスンと呼びます。

認証のリッスン

Reactで認証のリッスンを行うためには、「認証のリッスン処理を行うコンポーネントを作成し、これで対象のコンポーネントをラッピングする」というやり方をとります。

認証のリッスン処理を行うためのコンポーネントは、一般的には<Auth>という名前で定義されます。

<Auth>コンポーネントをRouter.jsxにおいて呼び出し、「認証済みの状態でのみ描画したいコンポーネント」をラッピングすることで、上記処理を実現します。

src/Router.jsx
import React from 'react';
import {Route, Switch} from "react-router";
import {Home,SignIn,SignUp} from "./templates";
import Auth from "./Auth"

const Router = () => {
  return (
    <Switch>
      <Route exact path={"/signup"} component={SignUp} />
      <Route exact path={"/signin"} component={SignIn} />

      //追記
      <Auth>
        <Route exact path={"(/)?"} component={Home} />
      </Auth>
      //追記ここまで
    </Switch>
  );
};

export default Router

SignUp, SignIn画面は、認証のリッスンを行いません。なぜなら、ユーザー情報を state に保存する必要がない(=未認証のユーザーでもアクセス可能)なページにしたいからです。

一方、path={"(/)?"}へアクセスすると、<Auth>コンポーネントにより認証のリッスンが行われます。

「認証がされていればコンポーネントを描画」「未認証であれば別の処理(例えばSignInへリダイレクト)」という条件分岐を行うことで、認証状態でのみアクセス可能な画面を実装できます。

認証リッスン関数を作成

<Auth>コンポーネントを作る前に、認証リッスンを実行するlistenAuthState関数を作成します。

firebase authでは、現在認証中のユーザーを取得するためのメソッドとして、onAuthStateChanged()があります。これを使用することで認証のリッスンを簡単に実装することができます。

users stateに関わる関数のため、signUp関数や signIn関数と同様に、users/operations.js に定義していきます。

src/reducks/users/operations
import { signInAction } from "./actions";
import {push} from "connected-react-router";
import {auth, db, FirebaseTimestamp} from "../../firebase/index"

export const listenAuthState = () => {
  return async (dispatch) => {
    return auth.onAuthStateChanged(user => {
      if (user) {
        const uid = user.uid

        db.collection("users").doc(uid).get()
          .then(snapshot => {
            const data = snapshot.data()

            dispatch(signInAction({
              isSignedIn: true,
              role: data.role,
              uid: uid,
              username: data.username
            }))
          })
      } else {
        dispatch(push("/signin"))
      }
    })
  }
}
.
.
.

auth.onAuthStateChanged()というメソッドは、ユーザーの認証状態に応じて、返り値を変える(条件分岐して処理を変えることができる)メソッドです。

onAuthStateChanged()メソッドを実行すると、アプリはブラウザ内のindexedDBを見に行き、そこにユーザーに関する情報があれば取ってきて、userという返り値を返します。

if(user)、すなわちuserが存在しているのであれば、それはユーザーが認証済みということになるので、user.uidの情報をもとにCloud firestoreから該当するユーザーの情報を取得し、stateに保存します。(また、Cloud firestoreとの通信を行うため、この関数はreturn async (dispatch) ...という redux-thunk を利用した書き方を行い、非同期制御をする必要があります)

逆に、userが存在しないのであれば、ユーザーが未認証の状態ということになるので、dispatch(push("/signin"))でサインイン画面にリダイレクトをさせます。

Authコンポーネントの作成

先ほどのlistenAuthState()関数を利用して<Auth>コンポーネントを作成し、<Auth>タグで囲まれたコンポーネントには、認証リッスンが走るようにします

ひとつ事前準備として、<Auth>コンポーネントでは、ユーザーのサインイン情報を示すstateであるuser.isSignedInを使用するため、それを取得するためのselectorを定義します。

src/reducks/users/selectors.js
import { createSelector } from "reselect";

const usersSelector = (state) => state.users;

export const getSignedIn = createSelector(
  [usersSelector],
  state => state.isSignedIn
)

.
.
.

getSignedIn ()と記述することで、Store内のuser.isSignedInの値を外部からでも参照できるようになりました。これも用いて、<Auth>コンポーネントを作成します。

<Auth>コンポーネントを作成する場所は決められていませんが、今回は使用先のRouter.jsxと同階層であるsrcディレクトリ直下に作ります。

src/Auth.jsx
import React, { useEffect } from "react";
import { useSelector, useDispatch } from "react-redux";
import { getSignedIn } from "./reducks/users/selectors";
import { listenAuthState } from "./reducks/users/operations";

const Auth = ({children}) => {
  const dispatch = useDispatch();
  const selector = useSelector((state) => state);

  const isSignedIn = getSignedIn(selector);

  useEffect(() => {
    if (!isSignedIn) {
        dispatch(listenAuthState())
    }
  }, []);

  if (!isSignedIn) {
    return <></>
  } else {
    return children
  }
};

export default Auth;

childrenとは、「子要素全体」を意味する特別なpropsです。例えば、アクセスしているアドレスが(/)であれば、<Route exact path={"(/)?"} component={Home} />がここに丸ごと入っているイメージ。

<Auth>コンポーネントがマウントされるとき、まず最初にuseEffect内の処理が実行されます(第2引数に[]を与えているため、componentDidMount()と同じ役割)。先ほど定義したlistenAuthStateが実行され、ユーザー認証のリッスンが行われます。

if (!isSignedIn) 、すなわちユーザーが未認証のときは、<Auth>コンポーネントは、空のHTMLタグ<></>を返すようにしています。なぜなら、ユーザーが未認証のときはlistenAuthStateが実行された時点でdispatch(push("/signin"))でサインイン画面にリダイレクトされることとなっているため、画面描画用のHTMLタグを用意する必要がないからです。

ユーザーが認証されていれば、<Auth>コンポーネントはchildren、すなわち子要素全体(アクセスしているアドレスが(/)であれば、<Route exact path={"(/)?"} component={Home} />)を描画する。

これでHomeコンポーネントが、ユーザー認証状態でしか描画できない((/)にアクセスできない)状態になったはずです。

SignOut の実装

認証のリッスンについて動作確認をする前に、SignOut 機能も実装してしまいます。SignOutを行うためには、

  1. firebase.auth でサインアウト処理を実行する(=ブラウザの indexedDB からユーザー情報を削除する)
  2. Redux Store内の users state を、initialState に戻す

の二つの処理を行う必要があります。

実装のイメージとして、何らかのテンプレート内に配置する「SIGN OUT」ボタンのonClickイベントとして、上記処理が走る関数(=signOut())を埋め込むことになるはずです。

その関数は user state に関連するものなので、reducks/users/operations.jsに、まずsignOut()を定義します。

signOut()の中で、「firebase.auth でサインアウト処理を実行」→「Redux Store内の users state を、initialState に戻すアクションを実行」という流れにすることで、上記二つの処理を同時に行うことができるはずです。

以上を踏まえると、編集ファイルは以下の通り。

1. src/reducks/users/operations.js // signOut()関数を定義
2. src/reducks/users/actions.js // 定義済みの signOutAction を一部編集
3. src/reducks/users/reducers.js // case Action.SIGN_OUT を追加
4. src/templates/Home.jsx // signOut()関数を発火するボタンを追加
src/reducks/users/operations.js
import { signInAction,signOutAction } from "./actions";
.
.
.
export const signOut = () => {
  return async (dispatch) => {
    auth.signOut()
    .then(() => {
      dispatch(signOutAction());
      dispatch(push('/signin'));
    })
  }
}

auth.signOut()は、firebase.authとしてのサインアウト処理を行うメソッドです。

その後、usersのアクションで定義するsignOutAction()を発火することで、アプリ側のサインアウトも行われます。

src/reducks/users/actions.js
.
.
.
export const SIGN_OUT = "SIGN_OUT";
export const signOutAction = () => {
  return {
    type: "SIGN_OUT",
    payload: {
      isSignedIn: false,
      role: "", // 追記
      uid: "",
      username: ""
    }
  }
}

payloadの中身は、initialState での定義と全く同じです。これにより、Redux Store内の users state が、initialState に戻るようにしています。

src/reducks/users/reducers.js
import * as Actions from './actions'
import initialState from '../store/initialState'

export const UsersReducer = (state = initialState.users, action) => {
  switch (action.type) {
    case Actions.SIGN_IN:
      return {
        ...state,
        ...action.payload
      };
    // 追記
    case Actions.SIGN_OUT:
      return {
        ...action.payload
      };
    // 追記ここまで
    default:
      return state
  }
}

users の initialState と全く同じ payload を、スプレッド構文で展開しています。これが Store に渡ることで、state の中身が書き換わります。

src/templates/Home.jsx
import React from 'react';
import {getUserId, getUserName} from '../reducks/users/selectors';
import {useSelector, useDispatch} from 'react-redux'
import {signOut} from "../reducks/users/operations" // 追記

const Home = () => {
  const dispatch = useDispatch() // 追記
  const selector = useSelector(state => state);
  const uid = getUserId(selector);
  const username = getUserName(selector);

  return (
    <div>
      <h2>Home</h2>
      <p>ユーザーID:{uid}</p>
      <p>ユーザー名:{username}</p>
      <button onClick={() => dispatch(signOut())}>SIGN OUT</button> // 追記
    </div>
  );
};

export default Home

「SIGN OUT」ボタンをクリックすることで、operationで定義したsignOut()が発火するようにしています。

ここまでくれば、実装は完了です!動作確認をしてみます。

新しいユーザーを一人新規作成するところから始めます。

localhost:3000/signup

image.png

「アカウントを登録する」をクリックすることで、ユーザーが登録され、画面がルートへリダイレクトされます。

localhost:3000/

image.png

先ほど配置した「SIGN OUT」ボタンがありますね。これをクリックすることで、サインアウト処理が行われ、かつ画面がサインイン画面にリダイレクトされます。

localhost:3000/signin

image.png

この状態でlocalhost:3000/にアクセスしてみても、強制的にlocalhost:3000/signin画面にリダイレクトされるはずです。

サインアウト処理が正常に行われており、かつ<Auth>コンポーネントのラッピングによる認証のリッスンが機能していることが確認できます。

これで、認証のリッスンおよびサインアウト処理の実装が完了しました!

パスワードリセットの作成

認証周りの最後の機能として、パスワードリセット機能を作ります。

firebase.authが提供するsendPasswordResetEmail()を利用することで、簡単にパスワードリセットを実装できます。

新規作成および編集ファイル
1. src/templates/Reset.jsx // パスワードリセット用のテンプレート
2. src/templates/index.js // Reset.jsxをエントリーポイントに追加
3. src/Router.jsx // Reset.jsxのルーティングを定義
4. src/reducks/users/operations.js // パスワードリセット用の関数を定義
5. src/templates/SignIn.jsx // SignUp, Resetへ遷移するリンクタグを追加
6. src/templates/SignUp.jsx // SignIp, Resetへ遷移するリンクタグを追加

5,6に関してはパスワードリセット機能の実装とは直接関係はないが、画面遷移がURL手打ちのままでも味気ないので、モックとしてそれっぽいリンクタグを配置しておく。

src/templates/Reset.jsx
import React, {useState,useCallback} from "react";
import {PrimaryButton,TextInput} from "../components/UIkit"
import {resetPassword} from "../reducks/users/operations"
import {useDispatch} from "react-redux"

const Reset = () => {
  const dispatch = useDispatch()

  const [email,setEmail] = useState()

    const inputEmail = useCallback((event) => {
      setEmail(event.target.value)
    },[setEmail])

  return(
    <div className="c-section-container">
      <h2 className="u-text__headline u-text-center">パスワードリセット</h2>
      <div className="module-spacer--medium" />

      <TextInput
        fullWidth={true} label={"メールアドレス"} multiline={false}
        required={true} rows={1} value={email} type={"email"} onChange={inputEmail}
      />

      <div className="module-spacer--medium" />

      <div className="center">
        <PrimaryButton
          label={"パスワードリセット"}
          onClick={() => dispatch(resetPassword(email))}
        />
      </div>
    </div>
  )
}

export default Reset
  • 基本構造はSignIn.jsxと同じ。パスワードリセットではメールアドレスしか使用しない点に注意。
  • 「パスワードリセット」ボタンをクリックすることで、この後実装するresetPassword()が実行され、パスワードリセットの処理が走ることになる。
src/templates/index.js
export {default as Home} from './Home'
export {default as Reset} from './Reset' //追記
export {default as SignIn} from './SignIn'
export {default as SignUp} from './SignUp'

  • テンプレートを追加したら、忘れずにエントリーポイントに追記をする。
src/Router.jsx
import React from 'react';
import {Route, Switch} from "react-router";
import {Home,Reset,SignIn,SignUp} from "./templates"; //追記
import Auth from "./Auth"

const Router = () => {
  return (
    <Switch>
      <Route exact path={"/signup"} component={SignUp} />
      <Route exact path={"/signin"} component={SignIn} />
      <Route exact path={"/signin/reset"} component={Reset} /> //追記

      <Auth>
        <Route exact path={"(/)?"} component={Home} />
      </Auth>
    </Switch>
  );
};

export default Router
  • Resetテンプレートに対して、(/signin/reset)というURLをルーティング。
  • Resetテンプレートはユーザー未認証状態でも表示させたいので、<Auth>ではラッピングしない。
src/reducks/users/operations.js
.
.
.
export const resetPassword = (email) => {
  return async (dispatch) => {
    if (email === "") {
      alert("必須項目が未入力です")
      return false
    } else {
      auth.sendPasswordResetEmail(email)
      .then(() => {
        alert('入力されたアドレスにパスワードリセット用のメールを送りました。')
        dispatch(push('/signin'))
      }).catch(() => {
        alert('パスワードリセットに失敗しました。通信環境を確認してください。')
      })
    }
  }
}
.
.
.
  • 「emailが空欄ではいけない」というバリデーションをかけている。
  • auth.sendPasswordResetEmail(email)メソッドが実行されると、firebase側より、パスワードリセット用のメールが届く仕組みになっている。
src/templates/SignIn.jsx
.
.
.
<PrimaryButton
  label={"サインイン"}
  onClick={() => dispatch(signIn(email,password))}
/>
<div className="module-spacer--medium" />
<p onClick={() => dispatch(push('/signup'))}>アカウントをお持ちでない方はこちら</p> //追記
<p onClick={() => dispatch(push('/signin/reset'))}>パスワードをお忘れの方はこちら</p> //追記
.
.
.
  • クリックすると('/signup'), ('/signin/reset')それぞれに遷移するリンクを、「サインイン」ボタン直下に配置
  • <div className="module-spacer--medium" />はタグ同士の空欄を表現するタグ(styles.cssで定義)
src/templates/SignUp.jsx
.
.
.
<PrimaryButton
  label={"アカウントを登録する"}
  onClick={() => dispatch(signUp(username,email,password,confirmPassword))}
/>
<div className="module-spacer--medium" />
<p onClick={() => dispatch(push('/signin'))}>アカウントをお持ちの方はこちら</p> //追記
.
.
.
  • クリックすると('/signup')に遷移するリンクを、「アカウントを登録する」ボタン直下に配置

これで、実装は一通り完了です。動作確認します。

実際にメールを受け取ることができるアドレスで、ユーザー登録をしてみます。

localhost:3000/signup

image.png

新規登録が成功すれば、ルートにリダイレクトします。

localhost:3000/

image.png

いったんサインアウトします。

localhost:3000/signin

image.png

「パスワードをお忘れの方はこちら」より、パスワードリセット画面へ飛びます。

localhost:3000/signin/reset

image.png

メールアドレスを入力して、「パスワードリセット」ボタンを押すと、

image.png

先ほど定義した通りのalert文が出ていますね。「OK」を押すと、サインイン画面にリダイレクトされます。

実際にメールボックスを確認すると、firebase より、パスワードリセット用のメールが届いているはずです。

image.png

URLをクリックすると、firebaseが用意しているパスワードリセット用の画面に飛びます。
image.png

新しいパスワードをSAVEします。

image.png

再度、Reactアプリのサインイン画面から、新しいパスワードを用いてサインインしてみます。

localhost:3000/signin

image.png

localhost:3000/

image.png

新しいパスワードでのサインインができました!

おわり

要点をまとめると、

  • 認証状態の永続化のため、認証のリッスン機能を作成する
  • 認証のリッスン機能を持たせた<Auth>コンポーネントで、リッスン対象をラッピングする
  • パスワードリセット機能は、firebase.authの機能で簡単に実装できる。

今回はここまで!講座一回あたりの内容がボリューミーになってきたので、基本1講座1記事の単位で投稿していくことになりそうです。

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

日本一わかりやすいReact-Redux講座 実践編 #3 学習備忘録

はじめに

この記事は、Youtubeチャンネル『トラハックのエンジニア学習ゼミ【とらゼミ】』の『日本一わかりやすいReact-Redux講座 実践編』の学習備忘録です。

前回記事はこちら

要約

  • 認証状態の永続化のため、認証のリッスン機能を作成する
  • 認証のリッスン機能を持たせた<Auth>コンポーネントで、リッスン対象をラッピングする
  • パスワードリセット機能は、firebase.authの機能で簡単に実装できる。

#3 Firebase Authで認証をリッスンしよう

認証状態の永続化

前回講座までで「サインアップ」「サインイン」の機能を実装しましたが、今の状態ではまだ不完全です。

例えば、サインイン直後ではユーザーの情報が state に正しく保存されているので、Home画面にはユーザーIDやユーザー名が表示されます。

しかし、この状態で画面をリロードしたり、ブラウザを立ち上げ直して再度アクセスすると、 state は初期状態に戻ってしまいます。

これは、React の state は、画面をリロードしたり、ブラウザを立ち上げ直したりすると初期化されてしまう性質を持っているからです。

これでは、 state の初期化が走るたびにサインインし直す必要があり、Webアプリとしては非常に使い勝手が悪くなってしまいます。

そのため、「ユーザー認証がすでに行われている状態であれば、適宜データベース側と通信してユーザー情報を state に保存する」という処理を入れ必要があります。

これを実現するのが認証のリッスンという処理なのですが、その前に一度、React+firebaseにおける認証の仕組みを簡単に整理しておきます。

React + firebase による認証の仕組み

そもそも firebase.Auth では、ユーザーの認証情報をどのように判断するのでしょうか?言い換えると、判断元となるユーザーの認証情報は、どこに保存されているのでしょうか?

こちらのteratail記事(『firebase.auth().onAuthStateChangedはどうやってログイン中であることを判定しているんでしょうか?』)によると、firebase.Auth は「サインアップまたはサインイン処理を行った時、ユーザーの認証情報をindexedDBという、ブラウザ内にあるデータベースに保存する」とのことです。

これは、「ブラウザ内にある情報を保存する場所」という意味ではlocalStorageに近い役割のものです。これらの違いについて詳しくは調べていませんが、ここで大切なのは「ブラウザ側に認証情報が保存される」ということです。

そのため、React+firebaseアプリにおいて、

  1. React の state に保存されているユーザー情報
  2. firebaseにより ブラウザのindexedDBへ保存された認証情報

がそれぞれ存在しているということになります。

ユーザー情報を画面描画に使用するためには、当然 React の state に値を入れる必要がありますが、これは画面リロードのたびに消えてしまいます。

対して、indexedDBに保存された情報は、画面リロードやブラウザ立ち上げなどでは削除されません。ただし、ここで保存している認証情報は、ユーザー名やメールアドレスなどのユーザー情報そのものではなく、「データベース(Cloud Firestore)から特定のユーザー情報を引き出すための鍵」のようなもので、そのまま使用することができないものです。

そのため、これらを組み合わせた以下のような流れで「認証状態の永続化」を実現します。

  1. 画面のリロードなどが行われた時、まずindexedDBを見て認証の有無を確認。
  2. 未認証なら initialState を state に入れて画面描画。認証済みであれば、indexedDBに保存された認証上を用いてCloud Firestoreと通信し、特定のユーザーの情報を取ってくる。
  3. 取ってきたユーザー情報を state に入れて画面描画する

このような流れを実装することで、サインインを行った特定のユーザーのサインイン状態が永続化し、何度もサインインをし直すという状態を防ぐことができます。

この今、ユーザーが認証しているかどうかを確認し、state に適切な値を入れる一連の作業のことを、認証のリッスンと呼びます。

認証のリッスン

Reactで認証のリッスンを行うためには、「認証のリッスン処理を行うコンポーネントを作成し、これで対象のコンポーネントをラッピングする」というやり方をとります。

認証のリッスン処理を行うためのコンポーネントは、一般的には<Auth>という名前で定義されます。

<Auth>コンポーネントをRouter.jsxにおいて呼び出し、「認証済みの状態でのみ描画したいコンポーネント」をラッピングすることで、上記処理を実現します。

src/Router.jsx
import React from 'react';
import {Route, Switch} from "react-router";
import {Home,SignIn,SignUp} from "./templates";
import Auth from "./Auth"

const Router = () => {
  return (
    <Switch>
      <Route exact path={"/signup"} component={SignUp} />
      <Route exact path={"/signin"} component={SignIn} />

      //追記
      <Auth>
        <Route exact path={"(/)?"} component={Home} />
      </Auth>
      //追記ここまで
    </Switch>
  );
};

export default Router

SignUp, SignIn画面は、認証のリッスンを行いません。なぜなら、ユーザー情報を state に保存する必要がない(=未認証のユーザーでもアクセス可能)なページにしたいからです。

一方、path={"(/)?"}へアクセスすると、<Auth>コンポーネントにより認証のリッスンが行われます。

「認証がされていればコンポーネントを描画」「未認証であれば別の処理(例えばSignInへリダイレクト)」という条件分岐を行うことで、認証状態でのみアクセス可能な画面を実装できます。

認証リッスン関数を作成

<Auth>コンポーネントを作る前に、認証リッスンを実行するlistenAuthState関数を作成します。

firebase authでは、現在認証中のユーザーを取得するためのメソッドとして、onAuthStateChanged()があります。これを使用することで認証のリッスンを簡単に実装することができます。

users stateに関わる関数のため、signUp関数や signIn関数と同様に、users/operations.js に定義していきます。

src/reducks/users/operations
import { signInAction } from "./actions";
import {push} from "connected-react-router";
import {auth, db, FirebaseTimestamp} from "../../firebase/index"

export const listenAuthState = () => {
  return async (dispatch) => {
    return auth.onAuthStateChanged(user => {
      if (user) {
        const uid = user.uid

        db.collection("users").doc(uid).get()
          .then(snapshot => {
            const data = snapshot.data()

            dispatch(signInAction({
              isSignedIn: true,
              role: data.role,
              uid: uid,
              username: data.username
            }))
          })
      } else {
        dispatch(push("/signin"))
      }
    })
  }
}
.
.
.

auth.onAuthStateChanged()というメソッドは、ユーザーの認証状態に応じて、返り値を変える(条件分岐して処理を変えることができる)メソッドです。

onAuthStateChanged()メソッドを実行すると、アプリはブラウザ内のindexedDBを見に行き、そこにユーザーに関する情報があれば取ってきて、userという返り値を返します。

if(user)、すなわちuserが存在しているのであれば、それはユーザーが認証済みということになるので、user.uidの情報をもとにCloud firestoreから該当するユーザーの情報を取得し、stateに保存します。(また、Cloud firestoreとの通信を行うため、この関数はreturn async (dispatch) ...という redux-thunk を利用した書き方を行い、非同期制御をする必要があります)

逆に、userが存在しないのであれば、ユーザーが未認証の状態ということになるので、dispatch(push("/signin"))でサインイン画面にリダイレクトをさせます。

Authコンポーネントの作成

先ほどのlistenAuthState()関数を利用して<Auth>コンポーネントを作成し、<Auth>タグで囲まれたコンポーネントには、認証リッスンが走るようにします

ひとつ事前準備として、<Auth>コンポーネントでは、ユーザーのサインイン情報を示すstateであるuser.isSignedInを使用するため、それを取得するためのselectorを定義します。

src/reducks/users/selectors.js
import { createSelector } from "reselect";

const usersSelector = (state) => state.users;

export const getSignedIn = createSelector(
  [usersSelector],
  state => state.isSignedIn
)

.
.
.

getSignedIn ()と記述することで、Store内のuser.isSignedInの値を外部からでも参照できるようになりました。これも用いて、<Auth>コンポーネントを作成します。

<Auth>コンポーネントを作成する場所は決められていませんが、今回は使用先のRouter.jsxと同階層であるsrcディレクトリ直下に作ります。

src/Auth.jsx
import React, { useEffect } from "react";
import { useSelector, useDispatch } from "react-redux";
import { getSignedIn } from "./reducks/users/selectors";
import { listenAuthState } from "./reducks/users/operations";

const Auth = ({children}) => {
  const dispatch = useDispatch();
  const selector = useSelector((state) => state);

  const isSignedIn = getSignedIn(selector);

  useEffect(() => {
    if (!isSignedIn) {
        dispatch(listenAuthState())
    }
  }, []);

  if (!isSignedIn) {
    return <></>
  } else {
    return children
  }
};

export default Auth;

childrenとは、「子要素全体」を意味する特別なpropsです。例えば、アクセスしているアドレスが(/)であれば、<Route exact path={"(/)?"} component={Home} />がここに丸ごと入っているイメージ。

<Auth>コンポーネントがマウントされるとき、まず最初にuseEffect内の処理が実行されます(第2引数に[]を与えているため、componentDidMount()と同じ役割)。先ほど定義したlistenAuthStateが実行され、ユーザー認証のリッスンが行われます。

if (!isSignedIn) 、すなわちユーザーが未認証のときは、<Auth>コンポーネントは、空のHTMLタグ<></>を返すようにしています。なぜなら、ユーザーが未認証のときはlistenAuthStateが実行された時点でdispatch(push("/signin"))でサインイン画面にリダイレクトされることとなっているため、画面描画用のHTMLタグを用意する必要がないからです。

ユーザーが認証されていれば、<Auth>コンポーネントはchildren、すなわち子要素全体(アクセスしているアドレスが(/)であれば、<Route exact path={"(/)?"} component={Home} />)を描画する。

これでHomeコンポーネントが、ユーザー認証状態でしか描画できない((/)にアクセスできない)状態になったはずです。

SignOut の実装

認証のリッスンについて動作確認をする前に、SignOut 機能も実装してしまいます。SignOutを行うためには、

  1. firebase.auth でサインアウト処理を実行する(=ブラウザの indexedDB からユーザー情報を削除する)
  2. Redux Store内の users state を、initialState に戻す

の二つの処理を行う必要があります。

実装のイメージとして、何らかのテンプレート内に配置する「SIGN OUT」ボタンのonClickイベントとして、上記処理が走る関数(=signOut())を埋め込むことになるはずです。

その関数は user state に関連するものなので、reducks/users/operations.jsに、まずsignOut()を定義します。

signOut()の中で、「firebase.auth でサインアウト処理を実行」→「Redux Store内の users state を、initialState に戻すアクションを実行」という流れにすることで、上記二つの処理を同時に行うことができるはずです。

以上を踏まえると、編集ファイルは以下の通り。

1. src/reducks/users/operations.js // signOut()関数を定義
2. src/reducks/users/actions.js // 定義済みの signOutAction を一部編集
3. src/reducks/users/reducers.js // case Action.SIGN_OUT を追加
4. src/templates/Home.jsx // signOut()関数を発火するボタンを追加
src/reducks/users/operations.js
import { signInAction,signOutAction } from "./actions";
.
.
.
export const signOut = () => {
  return async (dispatch) => {
    auth.signOut()
    .then(() => {
      dispatch(signOutAction());
      dispatch(push('/signin'));
    })
  }
}

auth.signOut()は、firebase.authとしてのサインアウト処理を行うメソッドです。

その後、usersのアクションで定義するsignOutAction()を発火することで、アプリ側のサインアウトも行われます。

src/reducks/users/actions.js
.
.
.
export const SIGN_OUT = "SIGN_OUT";
export const signOutAction = () => {
  return {
    type: "SIGN_OUT",
    payload: {
      isSignedIn: false,
      role: "", // 追記
      uid: "",
      username: ""
    }
  }
}

payloadの中身は、initialState での定義と全く同じです。これにより、Redux Store内の users state が、initialState に戻るようにしています。

src/reducks/users/reducers.js
import * as Actions from './actions'
import initialState from '../store/initialState'

export const UsersReducer = (state = initialState.users, action) => {
  switch (action.type) {
    case Actions.SIGN_IN:
      return {
        ...state,
        ...action.payload
      };
    // 追記
    case Actions.SIGN_OUT:
      return {
        ...action.payload
      };
    // 追記ここまで
    default:
      return state
  }
}

users の initialState と全く同じ payload を、スプレッド構文で展開しています。これが Store に渡ることで、state の中身が書き換わります。

src/templates/Home.jsx
import React from 'react';
import {getUserId, getUserName} from '../reducks/users/selectors';
import {useSelector, useDispatch} from 'react-redux'
import {signOut} from "../reducks/users/operations" // 追記

const Home = () => {
  const dispatch = useDispatch() // 追記
  const selector = useSelector(state => state);
  const uid = getUserId(selector);
  const username = getUserName(selector);

  return (
    <div>
      <h2>Home</h2>
      <p>ユーザーID:{uid}</p>
      <p>ユーザー名:{username}</p>
      <button onClick={() => dispatch(signOut())}>SIGN OUT</button> // 追記
    </div>
  );
};

export default Home

「SIGN OUT」ボタンをクリックすることで、operationで定義したsignOut()が発火するようにしています。

ここまでくれば、実装は完了です!動作確認をしてみます。

新しいユーザーを一人新規作成するところから始めます。

localhost:3000/signup

image.png

「アカウントを登録する」をクリックすることで、ユーザーが登録され、画面がルートへリダイレクトされます。

localhost:3000/

image.png

先ほど配置した「SIGN OUT」ボタンがありますね。これをクリックすることで、サインアウト処理が行われ、かつ画面がサインイン画面にリダイレクトされます。

localhost:3000/signin

image.png

この状態でlocalhost:3000/にアクセスしてみても、強制的にlocalhost:3000/signin画面にリダイレクトされるはずです。

サインアウト処理が正常に行われており、かつ<Auth>コンポーネントのラッピングによる認証のリッスンが機能していることが確認できます。

これで、認証のリッスンおよびサインアウト処理の実装が完了しました!

パスワードリセットの作成

認証周りの最後の機能として、パスワードリセット機能を作ります。

firebase.authが提供するsendPasswordResetEmail()を利用することで、簡単にパスワードリセットを実装できます。

新規作成および編集ファイル
1. src/templates/Reset.jsx // パスワードリセット用のテンプレート
2. src/templates/index.js // Reset.jsxをエントリーポイントに追加
3. src/Router.jsx // Reset.jsxのルーティングを定義
4. src/reducks/users/operations.js // パスワードリセット用の関数を定義
5. src/templates/SignIn.jsx // SignUp, Resetへ遷移するリンクタグを追加
6. src/templates/SignUp.jsx // SignIp, Resetへ遷移するリンクタグを追加

5,6に関してはパスワードリセット機能の実装とは直接関係はないが、画面遷移がURL手打ちのままでも味気ないので、モックとしてそれっぽいリンクタグを配置しておく。

src/templates/Reset.jsx
import React, {useState,useCallback} from "react";
import {PrimaryButton,TextInput} from "../components/UIkit"
import {resetPassword} from "../reducks/users/operations"
import {useDispatch} from "react-redux"

const Reset = () => {
  const dispatch = useDispatch()

  const [email,setEmail] = useState()

    const inputEmail = useCallback((event) => {
      setEmail(event.target.value)
    },[setEmail])

  return(
    <div className="c-section-container">
      <h2 className="u-text__headline u-text-center">パスワードリセット</h2>
      <div className="module-spacer--medium" />

      <TextInput
        fullWidth={true} label={"メールアドレス"} multiline={false}
        required={true} rows={1} value={email} type={"email"} onChange={inputEmail}
      />

      <div className="module-spacer--medium" />

      <div className="center">
        <PrimaryButton
          label={"パスワードリセット"}
          onClick={() => dispatch(resetPassword(email))}
        />
      </div>
    </div>
  )
}

export default Reset
  • 基本構造はSignIn.jsxと同じ。パスワードリセットではメールアドレスしか使用しない点に注意。
  • 「パスワードリセット」ボタンをクリックすることで、この後実装するresetPassword()が実行され、パスワードリセットの処理が走ることになる。
src/templates/index.js
export {default as Home} from './Home'
export {default as Reset} from './Reset' //追記
export {default as SignIn} from './SignIn'
export {default as SignUp} from './SignUp'

  • テンプレートを追加したら、忘れずにエントリーポイントに追記をする。
src/Router.jsx
import React from 'react';
import {Route, Switch} from "react-router";
import {Home,Reset,SignIn,SignUp} from "./templates"; //追記
import Auth from "./Auth"

const Router = () => {
  return (
    <Switch>
      <Route exact path={"/signup"} component={SignUp} />
      <Route exact path={"/signin"} component={SignIn} />
      <Route exact path={"/signin/reset"} component={Reset} /> //追記

      <Auth>
        <Route exact path={"(/)?"} component={Home} />
      </Auth>
    </Switch>
  );
};

export default Router
  • Resetテンプレートに対して、(/signin/reset)というURLをルーティング。
  • Resetテンプレートはユーザー未認証状態でも表示させたいので、<Auth>ではラッピングしない。
src/reducks/users/operations.js
.
.
.
export const resetPassword = (email) => {
  return async (dispatch) => {
    if (email === "") {
      alert("必須項目が未入力です")
      return false
    } else {
      auth.sendPasswordResetEmail(email)
      .then(() => {
        alert('入力されたアドレスにパスワードリセット用のメールを送りました。')
        dispatch(push('/signin'))
      }).catch(() => {
        alert('パスワードリセットに失敗しました。通信環境を確認してください。')
      })
    }
  }
}
.
.
.
  • 「emailが空欄ではいけない」というバリデーションをかけている。
  • auth.sendPasswordResetEmail(email)メソッドが実行されると、firebase側より、パスワードリセット用のメールが届く仕組みになっている。
src/templates/SignIn.jsx
.
.
.
<PrimaryButton
  label={"サインイン"}
  onClick={() => dispatch(signIn(email,password))}
/>
<div className="module-spacer--medium" />
<p onClick={() => dispatch(push('/signup'))}>アカウントをお持ちでない方はこちら</p> //追記
<p onClick={() => dispatch(push('/signin/reset'))}>パスワードをお忘れの方はこちら</p> //追記
.
.
.
  • クリックすると('/signup'), ('/signin/reset')それぞれに遷移するリンクを、「サインイン」ボタン直下に配置
  • <div className="module-spacer--medium" />はタグ同士の空欄を表現するタグ(styles.cssで定義)
src/templates/SignUp.jsx
.
.
.
<PrimaryButton
  label={"アカウントを登録する"}
  onClick={() => dispatch(signUp(username,email,password,confirmPassword))}
/>
<div className="module-spacer--medium" />
<p onClick={() => dispatch(push('/signin'))}>アカウントをお持ちの方はこちら</p> //追記
.
.
.
  • クリックすると('/signup')に遷移するリンクを、「アカウントを登録する」ボタン直下に配置

これで、実装は一通り完了です。動作確認します。

実際にメールを受け取ることができるアドレスで、ユーザー登録をしてみます。

localhost:3000/signup

image.png

新規登録が成功すれば、ルートにリダイレクトします。

localhost:3000/

image.png

いったんサインアウトします。

localhost:3000/signin

image.png

「パスワードをお忘れの方はこちら」より、パスワードリセット画面へ飛びます。

localhost:3000/signin/reset

image.png

メールアドレスを入力して、「パスワードリセット」ボタンを押すと、

image.png

先ほど定義した通りのalert文が出ていますね。「OK」を押すと、サインイン画面にリダイレクトされます。

実際にメールボックスを確認すると、firebase より、パスワードリセット用のメールが届いているはずです。

image.png

URLをクリックすると、firebaseが用意しているパスワードリセット用の画面に飛びます。
image.png

新しいパスワードをSAVEします。

image.png

再度、Reactアプリのサインイン画面から、新しいパスワードを用いてサインインしてみます。

localhost:3000/signin

image.png

localhost:3000/

image.png

新しいパスワードでのサインインができました!

おわり

要点をまとめると、

  • 認証状態の永続化のため、認証のリッスン機能を作成する
  • 認証のリッスン機能を持たせた<Auth>コンポーネントで、リッスン対象をラッピングする
  • パスワードリセット機能は、firebase.authの機能で簡単に実装できる。

今回はここまで!講座一回あたりの内容がボリューミーになってきたので、基本1講座1記事の単位で投稿していくことになりそうです。

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

React ハンズオン

今日作成するアプリ

chapter0.gif

1.Reactの簡単な説明

  • データの変更を検知したら、関連する部分だけを効率的に更新、描画する
  • 仮想 DOM(インメモリに保持されたUI表現)による高速な描画
  • JSXを使う(JavaScriptのソースコードにHTML的なものを埋め込む)
  • 単一のWebページでアプリケーション(Single Page Application)を作れる

Reactの採用事例

主な採用事例

  • Netflix
  • Slack
  • Uber
  • Airbnb
  • Paypal

ES2015(ES6)について

  • ECMASCriptの6th Editionのこと
  • letとconstで変数を宣言できる
  • アロー関数 : console.log(materials.map(material => material.length));
  • Class構文
  • extendsでクラスの継承
  • 全てのブラウザで対応しているわけではないため、Babelというトランスパイラを利用する
  • ReactはES6かES7で書く場合が多い

2.新しいシングルページアプリケーションを作成する

create-react-appという新規のReactプロジェクトを作るCLIツールを使います。
まずは、create-react-appをインストールします。
(Node >= 8.10 及び npm >= 5.6 の環境が必要です)

npm install -g create-react-app

todoプロジェクトを作成します。

npx create-react-app todo

3.ディレクトリ構成についての説明

.
├── README.md
├── package.json
├── public
│   ├── favicon.ico
│   └── index.html Reactアプリケーションがのるページ
├── src
│   ├── App.css App.jsで使用されるcss
│   ├── App.js index.jsから呼ばれるReactコンポーネント
│   ├── App.test.js
│   ├── index.css index.jsで使われるcss
│   ├── index.js Reactアプリケーションで最初に走るスクリプト(ルート DOM ノードにレンダリングする処理が書かれている)
│   └── logo.svg
└── yarn.lock

4.実行する

次のコマンドを実行します。

npm start

ブラウザに次のように表示されたら成功です。(http://localhost:3000のURLでブラウザが起動します)

chapter4.png

5.ソースを読んでみる

src/index.jsを開きます

以下の処理はpublic/index.htmlの<div id="root"></div>にAppコンポーネントをレンダリングしています。

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

src/App.jsを開きます

AppクラスがReact.Componentを継承しReact コンポーネントになっています。React.Component サブクラスで必ず定義しなければならない唯一のメソッドは render() です。render() メソッドは変更が起こるたびに呼び出されます。

returnで返しているのはJSXです。classはclassNameと書かないといけないことに注意してください。

export default AppでこのファイルのデフォルトとしてAppをexportしています。

import React, { Component } from 'react';
import logo from './logo.svg';
import './App.css';

class App extends Component {
  render() {
    return (
      <div className="App">
        <div className="App-header">
          <img src={logo} className="App-logo" alt="logo" />
          <h2>Welcome to React</h2>
        </div>
        <p className="App-intro">
          To get started, edit <code>src/App.js</code> and save to reload.
        </p>
      </div>
    );
  }
}

export default App;

6.コンポーネントを作る

目的:コンポーネントの作り方と使い方

1) コンポーネントを入れるためのディレクトリを作ります。

mkdir -p src/components

2) componentsディレクトリにList.jsを作ります。

List.js

import React, { Component } from 'react';

class List extends Component {
  render() {
    return (
      <table>
        <tbody>
          <tr><td style={{border: 1, borderColor: 'gray', borderStyle: 'solid', cursor: 'pointer'}}>掃除する</td></tr>
          <tr><td style={{border: 1, borderColor: 'gray', borderStyle: 'solid', cursor: 'pointer'}}>買い物</td></tr>
          <tr><td style={{border: 1, borderColor: 'gray', borderStyle: 'solid', cursor: 'pointer'}}>洗濯する</td></tr>
        </tbody>
      </table>
    );
  }
}

export default List;

3) App.jsに組み込み

import React, { Component } from 'react';
import List from './components/List';

class App extends Component {
  render() {
    return (
      <List />
    );
  }
}

export default App;

次のように表示されます。
chapter6.png

7.stateを使う

目的:stateの使い方

stateはコンポーネントの内部で制御されるオブジェクトです。更新はsetStateにより非同期に更新されます。stateが更新されると再描画されます。

List.jsを次のように書き換えます。

import React, { Component } from 'react';

class List extends Component {

  constructor(props) {
      super(props);
      this.state = {
        todos: [
          '掃除する',
          '買い物',
          '洗濯する'
        ]
      };
  }

  onClickAdd() {
    const newTodo = window.prompt("やることを入力してください", "");
    const todos = this.state.todos;
    todos.push(newTodo);
    this.setState({todos});
  }

  render() {
    return (
      <div>
        <div><button onClick={() => this.onClickAdd()}>追加</button></div>
        <table>
          <tbody>
          {this.state.todos.map((todo) => (
            <tr key={todo}><td style={{border: 1, borderColor: 'gray', borderStyle: 'solid', cursor: 'pointer'}}>{todo}</td></tr>
          ))}
          </tbody>
        </table>
      </div>

    );
  }
}

export default List;

追加でリストが増えると成功です。

8.propsを使う

目的:propsの使い方

propsはコンポーネントに属性として設定し値を渡すことができます。値だけでなく関数も渡すことができます。

1) componentsディレクトリにItem.jsを作ります。

import React, { Component } from 'react';

class Item extends Component {

  render() {
    const {todo, onClickItem} = this.props;
    return (
      <tr onClick={() => onClickItem(todo)}><td style={{border: 1, borderColor: 'gray', borderStyle: 'solid', cursor: 'pointer'}}>{todo}</td></tr>
    );
  }
}

export default Item;

2) List.jsのrenderを次のように書き換えます。

Itemをimportしてください。

import Item from './Item';
  render() {
    return (
      <div>
        <div><button onClick={() => this.onClickAdd()}>追加</button></div>
        <table>
          <tbody>
          {this.state.todos.map((todo) => (
            <Item key={todo} todo={todo} onClickItem={(todo) => alert(todo)} />
          ))}
          </tbody>
        </table>
      </div>
    );
  }

9.詳細内容表示用のコンポーネントの追加

1) src/componentsディレクトリにContent.jsを追加します。

import React, { Component } from 'react';

class Content extends Component {

  render() {
    return (
      <div style={{marginLeft: 50}}>
        <span>掃除をする</span>
      </div>
    );
  }

}

export default Content;

2) App.jsを次のように書き換えます。

import React, { Component } from 'react';
import List from './components/List';
import Content from './components/Content';

class App extends Component {
  render() {
    return (
      <div style={{display:'flex'}}>
        <List />
        <Content />
      </div>
    );
  }
}

export default App;

Listコンポーネントで選択した内容をContentコンポーネントに表示させたいですが、このままではうまく行きません。

10.reduxとredux-sagaの導入

目的:redux・redux-sagaの導入方法と使い方

Reduxは、Reactのstate(状態)を管理をするためのフレームワークです。
redux-sagaとは

redux-saga は、アプリケーションの副作用(つまり、データフェッチのような非同期のものや、ブラウザキャッシュへのアクセスのような不純なも> > の)を管理しやすく、実行効率が高く、テストが簡単で、障害処理を改善することを目的としたライブラリです。

redux-sagaの構成
chapter10.png

1) ライブラリをインストールします

npm install redux react-redux redux-saga
npm install

2) actionの作成

mkdir -p src/actions

src/actions/index.js

/**
 * Redux Actions
 */
export * from './TodoAppActions';

src/actions/TodoAppActions.js

/**
 * Todo App Actions
 */
import {
    GET_TODOS,
    GET_TODOS_SUCCESS,
    GET_TODOS_FAILURE,
    ADD_TODO,
    ADD_TODO_SUCCESS,
    SELECT_TODO,
    SELECT_TODO_SUCCESS,
} from './types';

export const getTodos = () => ({
    type: GET_TODOS
});

export const getTodosSuccess = (response) => ({
    type: GET_TODOS_SUCCESS,
    payload: response
});

export const getTodosFailure = (error) => ({
    type: GET_TODOS_FAILURE,
    payload: error
});

export const addTodo = (todo) => ({
    type: ADD_TODO,
    payload: todo
});

export const addTodoSuccess = (todo) => ({
    type: ADD_TODO_SUCCESS,
    payload: todo
});

export const selectTodo = (todo) => ({
    type: SELECT_TODO,
    payload: todo,
});

export const selectTodoSuccess = (todo) => ({
    type: SELECT_TODO_SUCCESS,
    payload: todo,
});

src/actions/types.js

export const GET_TODOS = 'GET_TODOS';
export const GET_TODOS_SUCCESS = 'GET_TODOS_SUCCESS';
export const GET_TODOS_FAILURE = 'GET_TODOS_FAILURE';
export const ADD_TODO = 'ADD_TODO';
export const ADD_TODO_SUCCESS = 'ADD_TODO_SUCCESS';
export const SELECT_TODO = 'SELECT_TODO';
export const SELECT_TODO_SUCCESS = 'SELECT_TODO_SUCCESS';

3) sagasの作成

src/sagas/index.js

/**
 * Root Sagas
 */
import { all } from 'redux-saga/effects';

// sagas
import todoSagas from './Todo';

export default function* rootSaga(getState) {
    yield all([
        todoSagas(),
    ]);
}

src/sagas/Todo.js

import { all, call, fork, put, takeEvery } from 'redux-saga/effects';

import {
    GET_TODOS,
    SELECT_TODO,
    ADD_TODO,
} from '../actions/types';

import {
    getTodosSuccess,
    getTodosFailure,
    selectTodoSuccess,
    addTodoSuccess,
} from '../actions';

const getTodosRequest = () => new Promise((resolve, reject) => {
  const todos = ['部屋の掃除', '買い物', '洗濯'];
  resolve(todos);
});

function* getTodosFromServer() {
    try {
        const response = yield call(getTodosRequest);
        yield put(getTodosSuccess(response));
    } catch (error) {
        yield put(getTodosFailure(error));
    }
}

function* addTodoToServer(action) {
  yield put(addTodoSuccess(action.payload));
}

function* selectTodoFromServer(action) {
  yield put(selectTodoSuccess(action.payload));
}

export function* getTodos() {
    yield takeEvery(GET_TODOS, getTodosFromServer);
}

export function* selectTodo() {
    yield takeEvery(SELECT_TODO, selectTodoFromServer);
}

export function* addTodo() {
    yield takeEvery(ADD_TODO, addTodoToServer);
}

export default function* rootSaga() {
    yield all([
        fork(getTodos),
        fork(selectTodo),
        fork(addTodo),
    ]);
}

function*についてはこちら
yieldについてはこちら

takeEvery: Actionがdispatchされるたびに起動させたいタスクを指定します
put: Actionをdispatchします

4) reducersの作成

src/reducers/index.js

/**
 * App Reducers
 */
import { combineReducers } from 'redux';
import todoAppReducer from './TodoAppReducer';

const reducers = combineReducers({
   todoApp: todoAppReducer,
});

export default reducers;

src/reducers/TodoAppReducer.js

/**
 * Todo App Reducer
 */

// action types
import {
    GET_TODOS,
    GET_TODOS_SUCCESS,
    GET_TODOS_FAILURE,
    ADD_TODO_SUCCESS,
    SELECT_TODO_SUCCESS,
} from '../actions/types';

// initial state
const INIT_STATE = {
    todos: [],
    selectedTodo: '',
};

export default (state = INIT_STATE, action) => {
    switch (action.type) {

        case GET_TODOS:
            return { ...state, todos: [] };

        case GET_TODOS_SUCCESS:
            return { ...state, todos: action.payload };

        case GET_TODOS_FAILURE:
            return {}

        case ADD_TODO_SUCCESS:
            const newTodos = [];
            state.todos.forEach((todo) => newTodos.push(todo));
            newTodos.push(action.payload);
            return { ...state, todos: newTodos };

        case SELECT_TODO_SUCCESS:
            return { ...state, selectedTodo: action.payload };

        default: return { ...state };

    }
}

5) storeの追加

src/index.js

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import './index.css';

import { Provider } from "react-redux";
import createSagaMiddleware from 'redux-saga';
import { createStore, applyMiddleware } from "redux";

import reducers from './reducers';
import RootSaga from "./sagas";
const sagaMiddleware = createSagaMiddleware();

const store = createStore(
  reducers,
  applyMiddleware(sagaMiddleware)
);
sagaMiddleware.run(RootSaga);

ReactDOM.render(
  <Provider store={store}>
     <App />
  </Provider>
,
  document.getElementById('root')
);

6) コンポーネントの修正

src/components/Content.js

import React, { Component } from 'react';
import { connect } from 'react-redux';

class Content extends Component {

  render() {
    const {selectedTodo} = this.props;
    return (
      <div style={{marginLeft: 50}}>
        <span>{selectedTodo}</span>
      </div>
    );
  }

}

const mapStateToProps = ({ todoApp }) => {
    const { selectedTodo } = todoApp;
    return { selectedTodo };
}

export default connect(mapStateToProps, null)(Content);

src/components/Item.js

import React, { Component } from 'react';

import { connect } from 'react-redux';
import { selectTodo } from '../actions';

class Item extends Component {

  render() {
    const {todo, selectTodo} = this.props;
    return (
      <tr onClick={() => selectTodo(todo)}><td style={{border: 1, borderColor: 'gray', borderStyle: 'solid', cursor: 'pointer'}}>{todo}</td></tr>
    );
  }
}

export default connect(null, { selectTodo })(Item);

src/components/List.js

import React, { Component } from 'react';
import Item from './Item';

import { connect } from 'react-redux';
import { getTodos, addTodo } from '../actions';

class List extends Component {

  componentDidMount() {
    const { getTodos } = this.props;
    getTodos();
  }

  onClickAdd() {
    const { addTodo } = this.props;
    const newTodo = window.prompt("やることを入力してください", "");
    addTodo(newTodo);
  }

  render() {
    const {todos} = this.props;
    return (
      <div>
        <div><button onClick={() => this.onClickAdd()}>追加</button></div>
        <table>
          <tbody>
          {todos.map((todo) => (
            <Item key={todo} todo={todo} />
          ))}
          </tbody>
        </table>
      </div>

    );
  }
}

const mapStateToProps = ({ todoApp }) => {
    const { todos } = todoApp;
    return { todos };
}

export default connect(mapStateToProps, { getTodos, addTodo })(List);

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

Reactのレンダリング視覚的なガイド

Reactはどのタイミングで、なぜ再レンダリングするかを説明する視覚的なガイド。
もともとの記事は文字が多く、例も編集できないため、わかりやすいバージョンを作った。

https://will-it-render.vercel.app/

もともとの記事:https://blog.isquaredsoftware.com/2020/05/blogged-answers-a-mostly-complete-guide-to-react-rendering-behavior/
翻訳:https://qiita.com/hellokenta/items/6b795501a0a8921bb6b5

中身

ガイドにはほとんど以下のような画像と編集できるCodesandboxの例がいくつか入っている。

u95az2s88db5h5uhnae5.png

一つの独立したページとして作っているので、上記でリンクをシェア。

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

Reactのレンダリング:視覚的なガイド

Reactはどのタイミングで、なぜ再レンダリングするかを説明する視覚的なガイド。
もともとの記事は文字が多く、例も編集できないため、わかりやすいバージョンを作った。

https://will-it-render.vercel.app/

もともとの記事:https://blog.isquaredsoftware.com/2020/05/blogged-answers-a-mostly-complete-guide-to-react-rendering-behavior/
翻訳:https://qiita.com/hellokenta/items/6b795501a0a8921bb6b5

中身

ガイドにはほとんど以下のような画像と編集できるCodesandboxの例がいくつか入っている。

u95az2s88db5h5uhnae5.png

一つの独立したページとして作っているので、上記でリンクをシェア。

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

atomic designでコンポーネント指向な開発を進めよう

atomic designとは?

アトミックデザインは、ビューコンポーネントライブラリの粒度の定義で、アメリカのWebデザイナーBrad Frost氏が考案・提唱しました。
http://atomicdesign.bradfrost.com/
VueやReactでコンポーネント設計でよく使われる手法です。スクリーンショット 2020-07-09 0.28.33.png
この図はコンポーネントの粒度を表しています。
上記のように5分割してコンポーネントを構成していきます。

Atoms

atoms-form-elements.png

ラベル、インプット、ボタンなどこれ以上分解できない最小の単位のものがAtomsになります。

Molecules

molecule-search-form.png

Atomsを組み合わせたものがMoleculesです。
上の図だとひとつ前のAtomsの図で3つのAtomsを全て組み合わせて検索フォームのMoleculesを作成しています。

Organisms

organism-header.png

Moleculesを組み合わせたものがOrganismsです。図はロゴとメニューと先ほどの検索フォームを合わせたヘッダーになっています。

Templates

template.png

Moleculesを組み合わせて、ページの構造を作成したものがTemplatesです。ワイヤーフレームの役割で、ページ内容についてはまだこの階層では仮のものとなります。

Pages

page.png

配置されたUIの外観を決めるテンプレートの文章や画像などといった内容を持つのがPagesとなります。

一連のコンポーネントの粒度を理解した上で、instagramで表したこの図を見るのが一番理解が深まりそうです。(わかりやすい!!)
instagram-atomic.png

出典: https://atomicdesign.bradfrost.com/chapter-2/

ディレクトリ構造の一例(Vue.js)

 components/
 ├── atoms
 │   ├── Link
 │   │   ├── NormalLink.vue
 │   │   └── BoldLink.vue
 │   ├── Button
 │   │   ├── NormalButton.vue
 │   │   └── BoldButton.js
 │   └── Table
 │       ├── HeaderCell.vue
 │       └── ContentCell.vue
 ├── molecules
 │   ├── Article
 │   │   ├── MainArticle.vue
 │   │   └── SubArticle.vue
 │   └── Table
 │       ├── TableHeader.vue
 │       └── TableContent.vue
 ├── organisms
 │   ├── Header
 │   │   ├── MainHeader.vue
 │   │   └── SubHeader.vue
 │   └── Table
 │       ├── MainTable.vue
 │       └── SubTable.vue
 └── templates
     └── Main
         ├── DashboadTemplate.vue
         └── MainTemplate.vue

こんな感じでatomic designの名称のファイルの配下にさらにLinkやButtonなどのディレクトリを作ってあげれば整理できて見易いかな思います。
atomsなどの直下にどんどん突っ込んでいくパターンでもありかとは思います。

悩むポイント

どの粒度で分類する?

これがmoleculesなのか、organismsなのかと迷う場面は出てくるかもしれませんが基本的には上記にも書いた通り、atomsの組み合わせはmolecules、moleculesの組み合わせはorganismっていうシンプルな思考でざっくりやってくのが良いかと思います。

organismsとorganismsを合わせたコンポーネントはどうなる?

パーツとしてのコンポーネントが3階層なので、このようなことが起きるのはどうしてもしょうがないです。
organisms内ではネストしてしまって良いルールで運用するのが1番簡単そうかなと思います。

データの変更はどこで行うの?

基本的にデータはPagesに持たせて、変更はきちんとPagesで行うのが良いです。
理由としてはdataを持つ場所を決めず、複数箇所に持たせてしまうといちいちどこで階層でデータを操作しているのか探さなければいけなくなるのでデータの流れが複雑化しスパゲティ化します...
書くのは楽でも後のことを考えて、きちんとクリックイベントなどはemitさせて親にイベントを渡して変更するのが良いです。

どんなコンポーネントがあったかわからなくならない?

StoryBookの導入してカタログ化すると良さそうです。
デザイナーとの認識合わせもこれでスムーズに行えそうです。
参考: https://qiita.com/masaakikunsan/items/dad8d84807918f3a43cb

実は、SmartHRさんはStoryBookとコンポーネントのソースコード公開してたりします。(Reactですが)
https://smarthr.invisionapp.com/share/ADUDJ8BW74C#/screens/403704083

導入メリット

デザインが崩れにくくなる

コンポーネントを使いまわしながらデザインをしていくようになるため、デザインが分からない人でもコンポーネントを当て込むだけで画面を構成することができるので、デザインに一貫性が生まれ、崩れにくくなります。
当たり前ですが、パーツごとのCSSの変更箇所も1箇所に集約されるため修正忘れのリスクが大きく減ります。

コンポーネントの作成方法を共有認識化できる

指針としてアトミックデザインを導入すれば、コンポーネント作成の粒度を上記に合わせるだけなので、認識を合わせやすくなります。個人開発であればある程度好きにやればよさそうですが、大勢が好き勝手にコンポーネントを作成するとどこに保存するコンポーネントなのか不明瞭になりがちです。

実装量の削減

コンポーネントの再利用が簡単にできるため開発効率が上がります。最初は大量のファイルを作るので少し大変かもしれませんが、その後のコストは大きく削減できるはずです。

参考文献

https://qiita.com/japboy/items/9576301456b2bffb1fac
https://qiita.com/kei-tamiya/items/cd34c120a860f20622dd
https://tech.connehito.com/entry/learn-and-failure-atomic-design

時間がある方は、この動画が死ぬ程わかりやすいのでおすすめです!!
【プログラミング】チームリーディング フロントエンドコンポーネントの指針 音ズレ修正Ver.
https://www.youtube.com/watch?v=oYdSLEixVFo&t=385s

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