はじめに
今年の1月から就活を始めたところ結果が芳しくなく、スキルの無さに悩みながらも
5~6月でチームでDjangoのWebサービスを開発するという企画に参加した際に、
「最近はフロントの知識もないときついと聞きますがTypeScriptってどれやるのがいいのかわからないし、JavascriptもAjaxとHTMLやCSSの属性変更で使ったくらいなのでハードル高いです……」
と聞いたところ、とりあえずいきなり初めて見るのも手だよとアドバイスを頂いたのでそれならばと興味のあったReactチュートリアルを終えて、Django側もAPI化しないといけないということでDRFのチュートリアルを完走して、1月に作ったポートフォリオからレベルアップし、チーム開発の成果とあわせてアピールできるように「React+Redux+CORS+axios+DRFで作るToDoアプリ」ということで要件定義から設計まで行いました。(なお、設計に関してはまだまだスキルの練度が低く、作業中に色々変えていった方が良かったりする部分もあるのであくまでも作業開始の土台として行いました)
そして、いざ実装の作業に移った結果、チュートリアルの知識だけではまるで足りずに、Reactの部分で沼にハマってしまいどうしたものかと嘆いたところTwitterでHooksについての理解があったほうがいいですよというアドバイスを頂いたので、作業を中止してReact Hooksについてドキュメントを履修してきたのでそれの覚書となります。
ちなみに各チュートリアルの覚書やチーム開発企画、ポートフォリオについてのアウトプットは以下の通りです、ご興味あれば読んでいただけると嬉しいです。
未経験からweb系エンジニアになるための独学履歴~初めてのポートフォリオ作成記録 製作記録編~
初心者がDjangoによる6週間でチームビルディングからプロダクト公開までやるプロジェクトに参加した話
Reactチュートリアルをこなしてみてその1
Reactチュートリアルやってみたその2
DRFチュートリアルを終えてみて
どこで沼にハマったのか
今回DjangoやLaravelのようなHTMLテンプレートまで用意されたフルスタックなフレームワークのみでなく、フロントとサーバー(バックエンド)で使うフレームワークを分けて何かをやるのが初めてでReact(フロント側)でも認証についての処理を書かないといけないということを知らなかったというところがことの発端でした。
ちなみに、この認証に関しては今回とは別にJWTでやるのかJWTでやるにしてもその保存方法はどうするのか? はたまたSessionでやるのか? などあまりにも(日本語での)情報が少ない上に、錯綜していて海外の各サイトのフォーラムなどを解読して……みたいなことをやって悩み悩むのですがそれは別の話です。
閑話休題、今まで認証に関しては完全フレームワーク任せだったので困った私はなにか参考になるものはないかと探してたところ
上記のリソースが使えそうだなと思い、コードが古かったのとそのまま使うのはということで勉強も兼ねてRe-Ducksの書き方にリファクタリングしようと思って見様見真似でやっていたのですが、ログインの処理はリソースを参考にしてできたものの、上記リソースの処理の中で
export function logoutUser() {
localStorage.removeItem("token");
return {
type: AuthTypes.LOGOUT
};
}
import React, { Component } from "react";
import PropTypes from "prop-types";
import { connect } from "react-redux";
import { logoutUser } from "../../actions/authActions";
class Logout extends Component {
static propTypes = {
logoutUser: PropTypes.func.isRequired
};
componentWillMount() {
this.props.logoutUser();
}
render() {
return (
<h2>Sorry to see you go...</h2>
);
}
}
export default connect(null, { logoutUser })(Logout);
こちらの処理をRe-Ducks方式で書き直そうとしてlogoutUser()
をdispacth()
を使うように書き換えようとしたところdispatch is not a function
エラーが出て、それを解消できず、さらおまけにcomponentWillMount()
もレガシーで書き換えることが推奨されおり、どうしたものかと頭を抱えていた……というところで冒頭に繋がります。
Hooksの基本的な考え方
// 以下のようにStateを定義する
import React, { useState, useEffect } from 'react';
function ExampleWithManyStates() {
// age変数に42を代入、以後setAgeでage変数を変更する
const [age, setAge] = useState(42);
// fruit変数に'banana'を代入、以後setFruitで管理する
const [fruit, setFruit] = useState('banana');
// todos変数にtext: 'Learn Hooks' プロパティを代入、以後setTodosで管理
const [todos, setTodos] = useState([{ text: 'Learn Hooks' }]);
// count変数に0を代入、以後setCountで管理
const [count, setCount] = useState(0);
// ...
}
// すると以下のように呼び出してStateに干渉することができる
import React, { useState, useEffect } from 'react';
function Example() {
const [count, setCount] = useState(0);
// Similar to componentDidMount and componentDidUpdate:
useEffect(() => {
// Update the document title using the browser API
document.title = `You clicked ${count} times`;
});
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
このとき useEffect()には関数を渡すことでcomponentDidMount and componentDidUpdateのようにrenderのあとに、Stateを変更する処理を加えることができる。
この場合は、onClick = {() => setCount(count + 1)}
で変化したcountの分だけdocument.titleの${ count }
も変わるということになる。
もっとわかりやすい例が以下である
import React, { useState, useEffect } from 'react';
function FriendStatus(props) {
const [isOnline, setIsOnline] = useState(null);
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
useEffect(() => {
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});
if (isOnline === null) {
return 'Loading...';
}
// isOnlineがnullなら'Offline'を返す
return isOnline ? 'Online' : 'Offline';
}
isOnlineは最初nullであるが、useEffectによってsetIsOnline()が呼び出され、isOnlineとなる。
なので最終的にreturnされるisOnlineはOnlineとなる。
さらにuseEffect()内でreturnで関数を返すとコンポーネントがDOMから削除された際の処理を定義できる。
つまりuseEffectとuseStateを用いたこれら一連の処理で
componentDidMount()
・ 初回のコンポーネントマウント時における挙動。例えばログイン処理などの非同期処理やタイマーのセット。
↓
componentDidUpdate()
・ componentDidMount()以降でpropsやstateが変更された場合の処理
↓
componentWillUnmount()
・ コンポーネントを破棄する場合の挙動。例えばログインに対するログアウト、タイマーセットに対する解除、非同期処理の中止。
というクラスコンポーネントの一連の挙動を定義できるということになる。
上記の例だとcomponentDidMount及びcomponentWillUnmountにおける挙動がuseEffectで定義されているということになる。
実際の場合だと、購読ボタンを押すとこの関数が呼び出されてrenderが始まり、subscribeToFriendStatus
となり、購読解除ボタンを押すと再度呼び出され再度renderされunsubscribeFromFriendStatus
となるといった処理になる。
また、Hookは定義したコンポーネントでしか呼び出せないが関数にすることで別のコンポーネントや関数で呼び出せる。
import React, { useState, useEffect } from 'react';
function useFriendStatus(friendID) {
const [isOnline, setIsOnline] = useState(null);
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
useEffect(() => {
ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
};
});
return isOnline;
}
// すると上記のHookは以下のように2つの関数で使うことができる
function FriendStatus(props) {
const isOnline = useFriendStatus(props.friend.id);
if (isOnline === null) {
return 'Loading...';
}
return isOnline ? 'Online' : 'Offline';
}
function FriendListItem(props) {
const isOnline = useFriendStatus(props.friend.id);
return (
<li style={{ color: isOnline ? 'green' : 'black' }}>
{props.friend.name}
</li>
);
}
また、クラスコンポーネントでHookを使うことはできない。
主にこのあたりの話を中心にドキュメントは進んでいきます。
useState
例えば以下のようなHookがあるとする
import React, { useState } from 'react';
function Example() {
// Declare a new state variable, which we'll call "count"
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
これをReactのこれまでの書き方で書くと以下の通りになる。
// クラスとconstructorとset.Stateを使う例
class Example extends React.Component {
// state初期化
constructor(props) {
super(props);
this.state = {
count: 0
};
}
render() {
return (
<div>
{/* 変更が反映される部分 */}
<p>You clicked {this.state.count} times</p>
{/* state変更フラグ */}
<button onClick={() => this.setState({ count: this.state.count + 1 })}>
Click me
</button>
</div>
);
}
}
useStateは上記のクラスコンポーネントにおいてのconstructorの役割を担う。
stateの初期値とそのstateの状態を変化させる関数をセットするイメージ。
本来ならconstructorのあとにcomponentDidMountなどの処理を定義してrenderするのでそれに相当するuseEffectがあるがそれについては後述する。
Hookはクラスではないのでthis.stateを使えない。
よってここまでをまとめると
// useStateのインポート
import React, { useState } from 'react';
function Example() {
// useStateの定義。第1引数にState変数、第2にそれを管理する関数名を定義
// 同時にState変数に初期値となるuseStateの引数を代入、今回はcount=0が初期値。
const [count, setCount] = useState(0);
return (
<div>
{/* setCount(count + 1)の結果が{ count }に反映される。クラスだと {this.state.count }*/}
<p>You clicked {count} times</p>
{/* setCount関数によりボタンが押される度にcount + 1の処理がなされる。クラスだと{ count: this.state.count + 1 } */}
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
ということになる。
ちなみに上記のように値の増減でなく、状態の管理や値の置き換えでStateを使う場合以下のように定義しておく
function Example2(friendID) {
const [isOnline, setIsOnline] = useState(null);
// state変数を置き換えるメソッドをHookの中に定義しておく
function handleStatusClick(status) {
setIsOnline(status.isOnline);
}
// ....
}
これは次のuseEffectなどと一緒に使うことになる。
useEffect
以下のようなHookを使ったコンポーネントがあるとする
import React, { useState, useEffect } from 'react';
function Example() {
const [count, setCount] = useState(0);
// Similar to componentDidMount and componentDidUpdate:
useEffect(() => {
// Update the document title using the browser API
document.title = `You clicked ${count} times`;
});
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
これをHookを使わないで書くと以下のようになる。
class Example extends React.Component {
constructor(props) {
super(props);
this.state = {
count : 0
};
}
componentDidMount() {
document.title = `You clicked ${this.state.count} times`;
}
componentDidUpdate() {
document.title = `You clicked ${this.state.count} times`;
}
render () {
return (
<div>
<p>You clicked {this.state.count} times</p>
<button onClick={() => this.setState({ count: this.state.count + 1 })}>
Click me
</button>
</div>
);
}
}
改めてになるがHookでやりたい事というのはconstructorで初期化と初期値セット(useStateの場合はstateを管理する関数のセットも)をして、componentDidMountでrenderされた直後に動作する処理を定義し、componentDidUpdateで初回以後のrenderの実行やstateの変化に対してどういう処理をなすかという基本的なサイクルを構築するということである。
上記のクラスコンポーネントの場合、this.stateのcountプロパティの値をボタンがクリックされた数だけ増加させ、{ this.state.count }の部分の表示を変更したいということになる。
Hookを使う場合、constructorの役割はuseStateが担うことは先程までで見てきたが、useEffectはcomponentDidMountやcomponentDidUpdateの役割を担うことになる。
ではuseEffectとはというとチュートリアルから引用すると
・ useEffect は何をするのか?
このフックを使うことで、あなたのコンポーネントがレンダリング後に何かをする必要があることをReactに伝えます。
Reactはあなたが渡した関数(ここでは「エフェクト」と呼ぶ)を記憶し、DOMの更新を行った後にそれを呼び出します。
このエフェクトでは、ドキュメントタイトルを設定していますが、データの取得や他の必須APIを呼び出すこともできます。
・ useEffectはなぜコンポーネントの内部で呼ばれるのか?
コンポーネント内部に useEffect を配置すると、エフェクトからカウント状態変数(または任意のプロップ)に直接アクセスすることができます。それを読み取るための特別なAPIは必要ありません - それはすでに関数スコープ内にあります。フックは jsx のクロージャを採用し、jsx がすでに解決策を提供しているような React 固有の API を導入することを避けています。
・ useEffectはすべてのレンダリングの後に実行されますか?
デフォルトでは、最初のレンダリングの後と更新の後の両方で実行されます。(これをカスタマイズする方法については後ほど説明します。) 「マウント」と「更新」という観点で考えるよりも、エフェクトは「レンダリング後」に実行されると考えた方がわかりやすいかもしれません。React はエフェクトを実行するまでに DOM が更新されていることを保証します。
ということになる。
useEffectを使う際、特にComponentを破棄した場合、componentDidMountを使った処理を解除したい時はcomponentWillUnmountの処理を加えないといけない。
例えば、以前も書いたユーザーのアクティブを管理するという処理をHookを使わないで書くと以下のようになる。
class FriendStatus extends React.Component {
constructor(props) {
super(props);
this.state = { isOnline: null };
this.handleStatusChange = this.handleStatusChange.bind(this);
}
componentDidMount() {
ChatAPI.subscribeToFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
componentWillUnmount() {
ChatAPI.unsubscribeFromFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
handleStatusChange(status) {
this.setState({
isOnline: status.isOnline
});
}
render() {
if (this.state.isOnline === null) {
return 'Loading...';
}
return this.state.isOnline ? 'Online' : 'Offline';
}
}
これをHookで書くとこうなる。
import React, { useState, useEffect } from 'react';
function FriendStatus(props) {
// constructorに相当
const [isOnline, setIsOnline] = useState(null);
// レンダリング後に実行される処理
useEffect(() => {
function handleStatusChange(status)
return () => {
setIsOnline(status.isOnline);
}
// FriendStatusクラスのcomponentDidMountに相当
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
// FriendStatusクラスのcomponentWillUnmountに相当
return () => {
ChatAPI.unsubscribeToFriendStatus(props.friend.id, handleStatusChange);
};
});
if (isOnline === null) {
return 'Loading...';
}
return isOnline ? 'Online' : 'Offline';
}
// useEffect内でreturnで関数を返すようにするのが肝
// 当然componentWillUnmountが必要のない処理の場合は返す必要はない
Hookの分割とHookの処理順について
例えば以下のようなコンポーネントがあったとする。
class FriendStatusWithCounter extends React.Component {
constructor(props) {
super(props);
this.state = {
count : 0,
isOnline : null
};
this.handleStatusChange = this.handleStatusChange.bind(this);
}
componentDidMount() {
document.title = `You clicked ${this.state.count} times`;
ChatAPI.subscribeToFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
componentDidUpdate() {
document.title = `You clicked ${this.state.count} times`;
}
componentWillUnmount() {
ChatAPI.unsubscribeFromFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
handleStatusChange(status) {
this.setStatus({
isOnline: status.isOnline
});
}
}
これは先程まで例に出してきたコンポーネントになるが、これだとdocument.title = 'You clicked ${this.state.count} times'
がcomponentDidMountとcomponentDidUpdateとで重複し、subscribeToFriendStatus及びunsubscribeFromFriendStatusのロジックがcomponentDidMountとcomponentWillUnmountとで重複してしまっている。
これを解消しようと、useEffectを使うとどうなるかというと
function FriendStatusWithCounter(props) {
// document.title = `You clicked ${this.state.count} times`
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `You clicked ${count} times`;
});
// subscribeToFriendStatus & unsubscribeFromFriendStatus
const [isOnline, setIsOnline] = useState(null);
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});
// ...
}
このように複数のHookに分割して表現することできる。処理順は指定の順番もといデフォルトでは定義した順となる。
では、もう少しこの処理について詳しく見てみる。
まず、componentDidUpdateをなぜ定義するのか? ということについてになるが、それについてまずクラスコンポーネントにおける以下の箇所に注目してみる。
componentDidMount() {
ChatAPI.subscribeToFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
componentWillUnmount() {
ChatAPI.unsubscribeFromFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
この状態だと一見問題ないように見えるが、コンポーネントがマウントされている際にprops.friend.idが変更、つまりisOnline = nullへと戻ってオフラインになると画面上にはこれを更新するメソッドがないのでオンライン表示のままである。
それは問題であるし、コンポーネントを破棄した際にメモリリークやクラッシュの原因にもなる。
これを解決するためのがcomponentDidUpdateになる。
componentDidMountが初回のrender時に実行する処理を書くのに対して、こちらはそれ以降propsまたはstateが変更されたときに実行する処理を書く。
componentDidMount() {
ChatAPI.subscribeToFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
componentDidUpdate(prevProps) {
// prePropsを引数にして直前のprops.friend.idをunsubscribeする。
ChatAPI.unsubscribeFromFriendStatus(
preProps.friend.id,
this.handleStatusChange
);
// props.friend.idを改めてsubscribeする。
ChatAPI.subscribeToFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
componentWillUnmount() {
ChatAPI.unsubscribeFromFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
これをHookで書くとこうなる。
function FriendStatus(props) {
const [isOnline, setIsOnline] = useState(null);
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline)
}
ChatAPI.subscribeToFriendStatus(
props.friend.id, handleStatusChange
);
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});
}
useEffect はデフォルトで更新を処理し、次のuseEffectを適用する前に、前のuseEffectをクリーンアップする。
その処理過程は以下の通りになる。
// 最初のマウントで { friend: { id: 100 } } がpropに渡される
ChatAPI.subscribeToFriendStatus(100, handleStatusChange); // Run first effect
// 更新があった場合 { friend: { id: 100 } } が削除され、 { friend: { id: 200 } } がpropsに渡される
ChatAPI.unsubscribeFromFriendStatus(100, handleStatusChange); // Clean up previous effect
ChatAPI.subscribeToFriendStatus(200, handleStatusChange); // Run next effect
// U上記の処理と同様に { friend: { id: 300 } } がpropsに渡される。
ChatAPI.unsubscribeFromFriendStatus(200, handleStatusChange); // Clean up previous effect
ChatAPI.subscribeToFriendStatus(300, handleStatusChange); // Run next effect
// Unmount
ChatAPI.unsubscribeFromFriendStatus(300, handleStatusChange); // Clean up last effect
ただし、上記の場合の例はAPIとの通信がないほか、ある1人のユーザーがオンラインかどうかの管理であるはずなので、あくまで挙動の理解のための処理いうことで理解を留めておく。
似た例でより実践的なものは後述する。
また、ここまでの過程を見るとif文で制御するべきでは? と感じました。
Reactのライフサイクルの考え方の中でもif文は使えるのでそれを見ていきます。
componentDidUpdate(prevProps) {
// this.state.countが変動していないときはDクリーンアップを行わない。
if(prevState.count !== this.state.count) {
document.title = `You clicked ${this.state.count} times`;
}
}
// これをHookで書くと以下のようになる。
useEffect(() => {
document.title = 'You clicked ${count} times';
}, [count]);
第2引数の[count]の部分でコンポーネントがレンダリングを比較することになる。
例えばcount = 5 だとしてそのまま変動がなかったとする。
となると前後のレンダリングの結果はdocument.title = 'You clicked ${5} times' と変わらないので更新の処理はスキップされる。
では、先程までのケースでみるとどうなるだろうか。
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(
prop.friend.id, handleStatusChange
);
return () => {
ChatAPI.unsubscribeFromFriendStatus(
props.friend.id, handleStatusChange
);
};
}, [props.friend.id]);
これでprops.friend.idに変更がない場合はcomponentDidUpdateに当たる部分の処理は行われない。
もちろんコンポーネントを破棄する際のreturn以下の処理は行われる。
ここで、注意しなければならないのuseEffectで使用されるコンポーネントのスコープのすべての値(propsやstateなど)が第2引数の配列に含まれていないといけないこと。
そうしないと、直前のレンダリングではなく、さらに古い値を参照することになり適切に比較することができない。
また、useEffectを実行して一度だけ(マウント時とアンマウント時に)クリーンアップしたい場合は、第二引数に空の配列([])を渡すことができる。
これは、useEffectがpropsやStateからの値に依存せず、再実行する必要がないことをReactに伝えている。これは特別なケースとして扱われるわけではなく、依存性配列が常にどのように動作するかに直接従っているということらしい。
つまり、この場合useEffectは初回のみの実行で以降再renderされることはないということになる。
空の配列([])を渡すと、useEffect内のpropsやstateは常に初期値を保つ。
Hooksを使うためのルール
大まかには以下の3点。
- ループや条件、入れ子になった関数の中でフックを呼び出してはいけない。
- 常にReact関数のトップレベルでHooksを使う。
- 通常のjsxで使わず、必ず関数コンポーネントかカスタムフックで定義をする。
ブレークポイントの設定(if文を使うときどこに定義するのか?)
改めて簡単にではあるがHooksの処理の順番を確認する。
function Form() {
const [name, setName] = useState('Mary');
useEffect(function persistForm() {
localStorage.setItem('formData', name);
});
const [surname, setSurname] = useState('Poppins');
useEffect(function updateTitle() {
document.title = name + '' + surname;
});
}
上記のようなHooksがあったとしてこれが実際に処理されるときどのように処理されるかが以下の過程である。
// ------------
// First render
// ------------
useState('Mary') // 1. Initialize the name state variable with 'Mary'
useEffect(persistForm) // 2. Add an effect for persisting the form
useState('Poppins') // 3. Initialize the surname state variable with 'Poppins'
useEffect(updateTitle) // 4. Add an effect for updating the title
// -------------
// Second render
// -------------
useState('Mary') // 1. Read the name state variable (argument is ignored)
useEffect(persistForm) // 2. Replace the effect for persisting the form
useState('Poppins') // 3. Read the surname state variable (argument is ignored)
useEffect(updateTitle) // 4. Replace the effect for updating the title
定義した順から処理されているのがわかるかと思う。
スキップは定義されていないので読み込まれるたびに処理が行われ都度レンダリングが行われる。
では、下記のブレークポイントを入れるとどうなるか?
// localStorageにnameプロパティの値が入っていない場合にsetItemを実行する
if (name !== '') {
useEffect(function persistForm() {
localStorage.setItem('formData', name);
});
}
最初のレンダリングの際には条件を満たすのでFirst renderは変わらない。
ただし、Second renderはそうではなく以下の通りになる。
useState('Mary') // 1. Read the name state variable (argument is ignored)
// useEffect(persistForm) // This Hook was skipped!
useState('Poppins') // 2 (but was 3). Fail to read the surname state variable
useEffect(updateTitle) // 3 (but was 4). Fail to replace the effect
useState('Poppins')
からエラーが出てしまっているのがわかる。
これはuseState Hookコールに対して返すべき値(useEffect(persistForm)
)がスキップされてしまい、未参照のまま処理が進んでしまったから。
よって、上記のブレークポイントは下記のようにuseEffect内に内包しないといけない。
これが冒頭の常にReact関数のトップレベルでHooksを使うということである。
// useEffect内にif文を定義する。
useEffect(function persistForm() {
if (name !== '') {
localStorage.setItem('formData', name);
}
});
カスタムフックとHooks間での情報をパス、Reducerについて
これまで見てきたオンライン状態を管理する以下のようなコンポーネント。
import React, { useState, useEffect } from 'react';
function FriendStatus(props) {
const [isOnline, setIsOnline] = useState(null);
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});
if (isOnline === null) {
return 'Loading...';
}
return isOnline ? 'Online' : 'Offline';
}
これを外部のコンポーネントでロジックとして呼び出し、処理上では以下の通りにしたい。
import React, { useState, useEffect } from 'react';
function FriendListItem(props) {
const [isOnline, setIsOnline] = useState(null);
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});
// オンライン状態の場合は名前が緑色に、オフライン時は黒で表記される
return (
<li style={{ color: isOnline ? 'green' : 'black' }}>
{props.friend.name}
</li>
);
}
それではどうするのかというと
const [isOnline, setIsOnline] = useState(null);
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});
この部分をカスタムフックとして共通化させることになる。
定義してみると以下のようになる。
import { useState, useEffect } from 'react';
function useFriendStatus(friendID) {
const[isOnline, setIsOnline] = useState(null);
useEffect( () => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
};
});
return isOnline;
}
やりたいこととしてはisOnlineの状態を返したいということなので普通に関数を作ってやればいいということになるが、注意する点としては以下の3点である。
- 通常のHooksと同じようにカスタムフックの最上位レベルで他のフックを呼び出すこと
- 名前は必ずuseで始まるもので定義すること
- 何を引数として取り、何を返すかは自分で定義すること
ではこのカスタムフックを実際に使ってみる。
import { useState, useEffect } from 'react';
function FriendStatus(props) {
const isOnline = useFriendStatus(props.friend.id);
if(isOnline === null) {
return 'Loading....';
}
return isOnline ? 'Online' : 'Offline';
}
function FriendListItem(props) {
const isOnline = useFriendStatus(props.friend.id);
return (
<li style={{ color: isOnline ? 'green' : 'black' }}>
{props.friend.name}
</li>
);
}
これまで見てきたようにHooksは定義した順に処理されているので処理としては
1 isOnlineがnullか否か返される
2 1で返ってきた結果によってisOnlineプロパティとして'Online'
あるいは'Offline'
のいずれかが値として返る。(初回render時は'Loading....'
が返ってくる)
3 isOnlineがnullか否か返される
4 1で返ってきた結果によってstyleタグ内でカラープロパティの値が決まり、props.friend.nameがどの色で描画されるか決まる
という順番になる。
注意する点は2つのコンポーネントは共通しているので const isOnline = useFriendStatus(props.friend.id);
は状態を共有しないということ。
つまり、FriendListItemで再度呼び出した段階でFriendStatusで呼び出したuseState及びuseEffectとは独立したものになっているということ。
では今度は作ったuseFriendStatusでHooks間で情報をパスするということをやってみる。
// 通常だとDBから引っ張ってくる部分
const friendList = [
{ id: 1, name: 'Phoebe' },
{ id: 2, name: 'Rachel' },
{ id: 3, name: 'Ross' },
];
function ChantRecipientPicker() {
const [recipientID, setRecipientID] = useState(1);
const isRecipientOnline = useFriendStatus(recipientID);
return (
<>
<Circle color={isRecipientOnline ? 'green' : 'red'} />
<select
value={recipientID}
onChange={e => setRecipientID(Number(e.target.value))}
>
{friendList.map(friend => (
<option key={friend.id} value={friend.id}>
{friend.name}
</option>
))}
</select>
</>
);
}
処理順としては以下の通り
1 ChantRecipientPickerのuseStateによりrecipientIDに1がセットされる
2 const isRecipientOnlineにuseFriendStatusの結果を返す。このときrecipientIDを引数にセットする。
3 friendListの数だけが描画され、isRecipientOnlineの結果によってオンラインとオフライン時で{friend.name}の色が変わる
これによって選択したユーザーがオンライン状態であるか否かを表現することができる。
またReducerを使いたい場合は以下のようにする
// Reducer
function todosReducer(state, action) {
switch (action.type) {
case 'add':
return [...state, {
text : action.text,
completed: false
}];
// ... other actions ...
default:
return state;
}
}
// Hooks
function useReducer(reducer, initialState) {
const [state, setState] = useState(initialState);
function dispatch(action) {
const nextState = reducer(state, action);
setState(nextState);
}
return [state, dispatch];
}
// ReducerでHooksを使いたい
function Todos() {
const [todos, dispatch] = useReducer(todosReducer, []);
function handleAddClick(text) {
dispatch({ type: 'add', text });
}
// ...
}
処理順としては
1 Todosを実行。useReducerを実行する。
2 useReducerによって現在のstateとdispatchが返される(Constructorの役割)
3 Todosに戻り、handleAddClickによってdispatchが決定し、それに則ってtodosReducerが実行される(今回は case 'add'
の処理)。
4 todosReducerの返り値がtodosに保存される。
useReducerについて
useReducerについてもう少し、補足が欲しかったのでAPIを取得するケースを例に処理の流れを確認してみる。
import React, {useReducer, useEffect} from 'react';
import axios from 'axios';
const initialState = {
isLoading: true,
isError: '',
post: {}
}
const dataFetchReducer = (dataState, action) => {
switch(action.type) {
case: 'FETCH_INIT':
return {
isLoading: true,
post: {},
isError: ''
}
case: 'FETCH_SUCCESS':
return {
isLoading: false,
post: action.payload, // 後述のpayloadプロパティに入るdataを定義しておく
isError: ''
}
case: 'FETCH_ERROR':
return {
isLoading: false,
post: {},
isError: 'Fetch is failure'
}
default:
return datastate
}
const [dataState, dispatch] = useReducer(dataFetchReducer, initialState)
useEffect(() =>
axios
.get() // 本来なら引数にURLリクエストURLを書くが省略
.then(res =>{
dispatch({type:'FETCH_SUCCESS', payload: res.data}) // 第2引数にデータを受け取るプロパティを設定する
})
.catch(err => {
dispatch({type: 'FETCH_ERROR'})
})
)
}
処理順としては
1 useReducer実行、dataStateにinitialStateが代入される。
2 useEffect処理、axiosでリクエストを叩いて実行結果によってdispatchを選択し、dataFetchReducerを実行。
3 リクエスト成功ならcase: 'FETCH_SUCCESS'
を実行、responseからpayloadにpost: action.payloadを代入。
4 リクエスト失敗ならcase: 'FETCH_ERROR'
を実行。
5 dataStateが更新され、dataFetchReducerでreturnされたものがdataStateで管理される
→ postプロパティのtitleの値がほしければ、dataState.post.data、isLoadingプロパティの値がほしければdataState.isLoadingでアクセスできる。
最後に
やっぱりJavascript……ひいてはTypescriptはカロリーが高いなというものを感じました。
一応ここだけでも理解ができていればReact及びReduxのコードがだいぶわかるとは思うんですけども、Reduxの概念とかReactやReduxに付随するライブラリでHookを使うにはみたいなことも押さえておかないといけないので、フロントは結構根気がいるスキルなのだなと感じました。
とりあえず次はReact ReduxのHooksについてドキュメントを読んだあとReact Routerの同項を読んでReduxでReducerについて一度ちゃんと仕様を把握しようと思っています。
そこまで理解できればあとはAPIでログイン認証後にSessionIDを発行しそれを元にPermissionの制御とかSessionのCookieにユーザー情報入れたりでソーシャルログイン以外の基本の認証はどうにかなると踏んでいます……
今回、ここに行き着くまでにfirebase覚えればこのあたりすべてクリアじゃない? というアドバイスと知見も得たので独学だといずれ限界にくるレベルでやること、やらなきゃいけないことが増えていきますね……
あと、冒頭でも申し上げましたがエンジニアに就職しようとしています。
もしご興味持っていただけた方がいらっしゃいましたらTwitterのDMやリプライでお誘いいただけると幸いです……