- 投稿日:2020-07-10T21:15:43+09:00
【技術書まとめ】りあクト! 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、などの解説が丁寧なのに加えて、それらが開発された背景や変遷なども詳細に説明してくれるので、機能の理解だけでなく「腑に落ちる」という気持ちにさせてくれた。
商業ベースの本ではここまで突っ込んだ内容にできないだろうし、やはりフロントエンド界隈のような鮮度の高い情報を必要とする世界ではこういった形態の書籍がこれからも有用になってくるのかなと思った。
ぜひ著者の次の本も読みたい。
- 投稿日:2020-07-10T18:51:26+09:00
ゼロからの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を選択してください。
そして、Componentsから一番最新のSimulatorをインストールしてください。
ここでXcodeの設定は完了です。
Android Studioの設定
Android Studioを起動し、⚙ConfigureからSDK Managerを選択し、以下の写真のようにパッケージが選択してあれば大丈夫です。
Windowsユーザーはここで設定完了です。以下からはMacユーザーだけの追加設定をしていきます。pathの設定(Macのみ)
ターミナルを開いて、以下のコマンド入力します。
cd ~/.bash_profile次に、Android StudioのSDK ManagerのAndroid SDK Locationに書かれているpathを以下のようにコマンドする。
.bash_profileexport ANDROID_SDK=/Users/kajigaya/Library/Android/sdk export PATH=/Users/kajigaya/Library/Android/sdk/platform-tools:$PATH最後に、.bash_profileに追加したpathを.zshrcを開いて、コピペする。
AVD Managerの設定
Android studioの⚙ConfigureからAVD Managerを選択し、+Create Virtual Deviceを選択します。
使用したいハードウェアを選択し、Q Downloadでダウンロードを開始し、完了です。
(使用するときは、使いたいハードウェアの▶️ボタンを押します)
3. expoでプロジェクト作成
今回は、ディスクトップにプロジェクトを作成したいので、ディスクトップに移動し、npmでexpoをインストールします。
cd ~/DesktopDesktopnpm install -g expo-cli
expoをインストール完了したら、expoでプロジェクトを作成します。
Desktopexpo init プロジェクト名
インストール中、いろいろと選択する必要がありますが、基本的にEnterで構いません。
4.プロジェクトを起動
まず、作成したプロジェクトに移動します。そこでプロジェクトを立ち上げます。
プロジェクト名cd ~/Desktop/プロジェクト名 npm start
ブラウザが立ち上がり、Run on iOS simulatorを選択します。
このような画面が立ち上がれば環境構築完了です。
お疲れ様でした。参考資料
React Native公式
Expo公式 MacユーザーのためAndroid Studioの設定
Android Studio公式 Android Studioのインストール方法
- 投稿日:2020-07-10T17:11:34+09:00
日本一わかりやすい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
アプリにおいて、
- React の state に保存されている
ユーザー情報
- firebaseにより ブラウザの
indexedDB
へ保存された認証情報
がそれぞれ存在しているということになります。
ユーザー情報を画面描画に使用するためには、当然 React の state に値を入れる必要がありますが、これは画面リロードのたびに消えてしまいます。
対して、
indexedDB
に保存された情報は、画面リロードやブラウザ立ち上げなどでは削除されません。ただし、ここで保存している認証情報
は、ユーザー名やメールアドレスなどのユーザー情報そのものではなく、「データベース(Cloud Firestore)から特定のユーザー情報を引き出すための鍵」のようなもので、そのまま使用することができないものです。そのため、これらを組み合わせた以下のような流れで「認証状態の永続化」を実現します。
- 画面のリロードなどが行われた時、まず
indexedDB
を見て認証の有無を確認。- 未認証なら initialState を state に入れて画面描画。認証済みであれば、
indexedDB
に保存された認証上を用いてCloud Firestoreと通信し、特定のユーザーの情報を取ってくる。- 取ってきたユーザー情報を state に入れて画面描画する
このような流れを実装することで、サインインを行った特定のユーザーのサインイン状態が永続化し、何度もサインインをし直すという状態を防ぐことができます。
この今、ユーザーが認証しているかどうかを確認し、state に適切な値を入れる一連の作業のことを、認証のリッスンと呼びます。
認証のリッスン
Reactで認証のリッスンを行うためには、「
認証のリッスン処理を行うコンポーネント
を作成し、これで対象のコンポーネントをラッピングする」というやり方をとります。
認証のリッスン処理を行うためのコンポーネント
は、一般的には<Auth>
という名前で定義されます。
<Auth>
コンポーネントをRouter.jsx
において呼び出し、「認証済みの状態でのみ描画したいコンポーネント」をラッピングすることで、上記処理を実現します。src/Router.jsximport 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 RouterSignUp, SignIn画面は、認証のリッスンを行いません。なぜなら、ユーザー情報を state に保存する必要がない(=未認証のユーザーでもアクセス可能)なページにしたいからです。
一方、
path={"(/)?"}
へアクセスすると、<Auth>
コンポーネントにより認証のリッスンが行われます。「認証がされていればコンポーネントを描画」「未認証であれば別の処理(例えばSignInへリダイレクト)」という条件分岐を行うことで、認証状態でのみアクセス可能な画面を実装できます。
認証リッスン関数を作成
<Auth>
コンポーネントを作る前に、認証リッスンを実行するlistenAuthState
関数を作成します。firebase authでは、現在認証中のユーザーを取得するためのメソッドとして、
onAuthStateChanged()
があります。これを使用することで認証のリッスンを簡単に実装することができます。users stateに関わる関数のため、
signUp
関数やsignIn
関数と同様に、users/operations.js に定義していきます。src/reducks/users/operationsimport { 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.jsimport { 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.jsximport 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を行うためには、
- firebase.auth でサインアウト処理を実行する(=ブラウザの indexedDB からユーザー情報を削除する)
- 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.jsimport { 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.jsimport * 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.jsximport 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
「アカウントを登録する」をクリックすることで、ユーザーが登録され、画面がルートへリダイレクトされます。
localhost:3000/
先ほど配置した「SIGN OUT」ボタンがありますね。これをクリックすることで、サインアウト処理が行われ、かつ画面がサインイン画面にリダイレクトされます。
localhost:3000/signin
この状態で
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.jsximport 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.jsexport {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.jsximport 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
新規登録が成功すれば、ルートにリダイレクトします。
localhost:3000/
いったんサインアウトします。
localhost:3000/signin
「パスワードをお忘れの方はこちら」より、パスワードリセット画面へ飛びます。
localhost:3000/signin/reset
メールアドレスを入力して、「パスワードリセット」ボタンを押すと、
先ほど定義した通りのalert文が出ていますね。「OK」を押すと、サインイン画面にリダイレクトされます。
実際にメールボックスを確認すると、firebase より、パスワードリセット用のメールが届いているはずです。
URLをクリックすると、firebaseが用意しているパスワードリセット用の画面に飛びます。
新しいパスワードをSAVEします。
再度、Reactアプリのサインイン画面から、新しいパスワードを用いてサインインしてみます。
localhost:3000/signin
localhost:3000/
新しいパスワードでのサインインができました!
おわり
要点をまとめると、
- 認証状態の永続化のため、
認証のリッスン機能
を作成する- 認証のリッスン機能を持たせた
<Auth>
コンポーネントで、リッスン対象をラッピングする- パスワードリセット機能は、firebase.authの機能で簡単に実装できる。
今回はここまで!講座一回あたりの内容がボリューミーになってきたので、基本1講座1記事の単位で投稿していくことになりそうです。
- 投稿日:2020-07-10T17:11:34+09:00
日本一わかりやすい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
アプリにおいて、
- React の state に保存されている
ユーザー情報
- firebaseにより ブラウザの
indexedDB
へ保存された認証情報
がそれぞれ存在しているということになります。
ユーザー情報を画面描画に使用するためには、当然 React の state に値を入れる必要がありますが、これは画面リロードのたびに消えてしまいます。
対して、
indexedDB
に保存された情報は、画面リロードやブラウザ立ち上げなどでは削除されません。ただし、ここで保存している認証情報
は、ユーザー名やメールアドレスなどのユーザー情報そのものではなく、「データベース(Cloud Firestore)から特定のユーザー情報を引き出すための鍵」のようなもので、そのまま使用することができないものです。そのため、これらを組み合わせた以下のような流れで「認証状態の永続化」を実現します。
- 画面のリロードなどが行われた時、まず
indexedDB
を見て認証の有無を確認。- 未認証なら initialState を state に入れて画面描画。認証済みであれば、
indexedDB
に保存された認証上を用いてCloud Firestoreと通信し、特定のユーザーの情報を取ってくる。- 取ってきたユーザー情報を state に入れて画面描画する
このような流れを実装することで、サインインを行った特定のユーザーのサインイン状態が永続化し、何度もサインインをし直すという状態を防ぐことができます。
この今、ユーザーが認証しているかどうかを確認し、state に適切な値を入れる一連の作業のことを、認証のリッスンと呼びます。
認証のリッスン
Reactで認証のリッスンを行うためには、「
認証のリッスン処理を行うコンポーネント
を作成し、これで対象のコンポーネントをラッピングする」というやり方をとります。
認証のリッスン処理を行うためのコンポーネント
は、一般的には<Auth>
という名前で定義されます。
<Auth>
コンポーネントをRouter.jsx
において呼び出し、「認証済みの状態でのみ描画したいコンポーネント」をラッピングすることで、上記処理を実現します。src/Router.jsximport 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 RouterSignUp, SignIn画面は、認証のリッスンを行いません。なぜなら、ユーザー情報を state に保存する必要がない(=未認証のユーザーでもアクセス可能)なページにしたいからです。
一方、
path={"(/)?"}
へアクセスすると、<Auth>
コンポーネントにより認証のリッスンが行われます。「認証がされていればコンポーネントを描画」「未認証であれば別の処理(例えばSignInへリダイレクト)」という条件分岐を行うことで、認証状態でのみアクセス可能な画面を実装できます。
認証リッスン関数を作成
<Auth>
コンポーネントを作る前に、認証リッスンを実行するlistenAuthState
関数を作成します。firebase authでは、現在認証中のユーザーを取得するためのメソッドとして、
onAuthStateChanged()
があります。これを使用することで認証のリッスンを簡単に実装することができます。users stateに関わる関数のため、
signUp
関数やsignIn
関数と同様に、users/operations.js に定義していきます。src/reducks/users/operationsimport { 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.jsimport { 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.jsximport 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を行うためには、
- firebase.auth でサインアウト処理を実行する(=ブラウザの indexedDB からユーザー情報を削除する)
- 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.jsimport { 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.jsimport * 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.jsximport 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
「アカウントを登録する」をクリックすることで、ユーザーが登録され、画面がルートへリダイレクトされます。
localhost:3000/
先ほど配置した「SIGN OUT」ボタンがありますね。これをクリックすることで、サインアウト処理が行われ、かつ画面がサインイン画面にリダイレクトされます。
localhost:3000/signin
この状態で
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.jsximport 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.jsexport {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.jsximport 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
新規登録が成功すれば、ルートにリダイレクトします。
localhost:3000/
いったんサインアウトします。
localhost:3000/signin
「パスワードをお忘れの方はこちら」より、パスワードリセット画面へ飛びます。
localhost:3000/signin/reset
メールアドレスを入力して、「パスワードリセット」ボタンを押すと、
先ほど定義した通りのalert文が出ていますね。「OK」を押すと、サインイン画面にリダイレクトされます。
実際にメールボックスを確認すると、firebase より、パスワードリセット用のメールが届いているはずです。
URLをクリックすると、firebaseが用意しているパスワードリセット用の画面に飛びます。
新しいパスワードをSAVEします。
再度、Reactアプリのサインイン画面から、新しいパスワードを用いてサインインしてみます。
localhost:3000/signin
localhost:3000/
新しいパスワードでのサインインができました!
おわり
要点をまとめると、
- 認証状態の永続化のため、
認証のリッスン機能
を作成する- 認証のリッスン機能を持たせた
<Auth>
コンポーネントで、リッスン対象をラッピングする- パスワードリセット機能は、firebase.authの機能で簡単に実装できる。
今回はここまで!講座一回あたりの内容がボリューミーになってきたので、基本1講座1記事の単位で投稿していくことになりそうです。
- 投稿日:2020-07-10T15:29:28+09:00
React ハンズオン
今日作成するアプリ
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-apptodoプロジェクトを作成します。
npx create-react-app todo3.ディレクトリ構成についての説明
. ├── 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.lock4.実行する
次のコマンドを実行します。
npm startブラウザに次のように表示されたら成功です。(http://localhost:3000のURLでブラウザが起動します)
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/components2) 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;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 は、アプリケーションの副作用(つまり、データフェッチのような非同期のものや、ブラウザキャッシュへのアクセスのような不純なも> > の)を管理しやすく、実行効率が高く、テストが簡単で、障害処理を改善することを目的としたライブラリです。
1) ライブラリをインストールします
npm install redux react-redux redux-saga npm install2) actionの作成
mkdir -p src/actionssrc/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);
- 投稿日:2020-07-10T12:07:55+09:00
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の例がいくつか入っている。
一つの独立したページとして作っているので、上記でリンクをシェア。
- 投稿日:2020-07-10T12:07:55+09:00
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の例がいくつか入っている。
一つの独立したページとして作っているので、上記でリンクをシェア。
- 投稿日:2020-07-10T01:14:01+09:00
atomic designでコンポーネント指向な開発を進めよう
atomic designとは?
アトミックデザインは、ビューコンポーネントライブラリの粒度の定義で、アメリカのWebデザイナーBrad Frost氏が考案・提唱しました。
http://atomicdesign.bradfrost.com/
VueやReactでコンポーネント設計でよく使われる手法です。
この図はコンポーネントの粒度を表しています。
上記のように5分割してコンポーネントを構成していきます。Atoms
ラベル、インプット、ボタンなどこれ以上分解できない最小の単位のものがAtomsになります。
Molecules
Atomsを組み合わせたものがMoleculesです。
上の図だとひとつ前のAtomsの図で3つのAtomsを全て組み合わせて検索フォームのMoleculesを作成しています。Organisms
Moleculesを組み合わせたものがOrganismsです。図はロゴとメニューと先ほどの検索フォームを合わせたヘッダーになっています。
Templates
Moleculesを組み合わせて、ページの構造を作成したものがTemplatesです。ワイヤーフレームの役割で、ページ内容についてはまだこの階層では仮のものとなります。
Pages
配置されたUIの外観を決めるテンプレートの文章や画像などといった内容を持つのがPagesとなります。
一連のコンポーネントの粒度を理解した上で、instagramで表したこの図を見るのが一番理解が深まりそうです。(わかりやすい!!)
出典: 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