はじめに
React.useEffectのみを持つコンポーネント(APIコンポーネント)を利用することで、非同期処理を行う方法を紹介します。APIコンポーネントを用いることで、非同期処理を含めたデータの流れがとてもシンプルになります。
APIコンポーネントとは
特徴
- propsを受けとらない
- Elementを返さない
- Redux Storeのみとデータのやり取りを行う
- APIReducer(APIコンポーネント専用のReducer)が管理するプロパティの更新を検知して非同期処理を開始する
SampleAPI.tsx
import * as React from 'react';
import { Dispatch, Action } from 'redux';
import { useSelector, useDispatch } from 'react-redux';
import { RootState } from 'store';
export const SampleAPI: React.FC<{}> = () => { // 1. propsを受けとらない
/*
3. Redux Storeのみとデータのやり取りを行う
*/
const dispatch = useDispatch<Dispatch<Action>>();
const data = useSelector<RootState, any>(
state => state.sampleAPI.data
);
React.useEffect(() => {
/*
非同期処理
*/
}, [data]); // 4. APIReducerが管理するプロパティの更新を検知して非同期処理を開始する
return null; // 2. Elementを返さない
};
以降、最小構成のツイートアプリを用いて具体例を紹介します。 ソースコード(GitHub)
ツイート全体の取得 (UpdateTweetsAPI)
データの流れ
1. tweetsAPIReducerが管理するupdatingプロパティの更新をUpdateTweetsAPIが検知 2. UpdateTweetsAPIはサーバからツイート全体を取得 3. UpdateTweetsAPIはentitiesReducerが管理するtweetsプロパティに取得したツイート全体を保存 4. TweetListコンポーネントはtweetsプロパティの更新に伴って再描画。ツイートの一覧を表示UpdateTweetsAPI.tsx
import * as React from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { Dispatch, Action } from 'redux';
import { RootState } from 'store';
import { entitiesActions } from 'actions/entitiesActions';
import { tweetsAPIActions } from 'actions/tweetsAPIActions';
/*
サーバAPIとの通信用クライアント
*/
const fetchTweets = () => fetch('http://localhost/tweets', {
method: 'GET',
mode: 'cors',
credentials: 'include',
});
export const UpdateTweetsAPI: React.FC<{}> = () => {
const dispatch = useDispatch<Dispatch<Action>>();
/*
tweetsAPIReducerが管理するupdatingプロパティ(boolean型)
非同期処理開始のトリガーとなるAPIReducerが管理するプロパティ
*/
const updating = useSelector<RootState, boolean>(
(state) => state.tweetsAPI.updating,
);
React.useEffect(() => {
if (!updating) return;
// 2. サーバからツイート全体を取得
fetchTweets()
.then((res) => res.json())
.then((res) => {
if (!res.tweets) return;
// 3. entitiesReducerが管理するtweetsプロパティに取得したツイート全体を保存
dispatch(entitiesActions.updateTweets(res.tweets));
})
.then(() => {
/*
非同期処理の終了をディスパッチ
データの流れでは省略
*/
dispatch(tweetsAPIActions.updateTweetsDone());
})
.catch(() => {
dispatch(tweetsAPIActions.updateTweetsDone());
});
}, [updating]); // 1. updatingプロパティの更新を検知
return null;
};
ツイートの送信 (SendTweetAPI)
データの流れ
1. TweetFormコンポーネントで送信ボタンが押されると、フォームの内容をtweetsAPIReducerが管理するnewContentプロパティに保存 2. newContentプロパティの更新をSendTweetAPIが検知 3. SendTweetAPIはサーバへツイートを送信 4. SendTweetAPIはupdateTweetsアクションをディスパッチすることで、tweetsAPIReducerが管理するupdatingプロパティを更新 5. updatingプロパティの更新をUpdateTweetsAPIが検知 (以後、上記の**ツイート全体の取得**)SendTweetAPI.tsx
import * as React from 'react';
import { Dispatch, Action } from 'redux';
import { useSelector, useDispatch } from 'react-redux';
import { RootState } from 'store';
import { tweetsAPIActions } from 'actions/tweetsAPIActions';
/*
サーバAPIとの通信用クライアント
*/
const sendTweet = (content: string) => fetch('http://localhost/tweets', {
method: 'POST',
mode: 'cors',
credentials: 'include',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({ content }),
});
export const SendTweetAPI: React.FC<{}> = () => {
const dispatch = useDispatch<Dispatch<Action>>();
/*
tweetsAPIReducerが管理するnewContentプロパティ(string型)
非同期処理開始のトリガーとなるAPIReducerが管理するプロパティ
*/
const newContent = useSelector<RootState, string>(
(state) => state.tweetsAPI.newContent,
);
React.useEffect(() => {
if (newContent === '') return;
// 3. サーバへツイートを送信
sendTweet(newContent)
.then(() => {
/*
4. SendTweetAPIはupdateTweetsアクションをディスパッチすることで、
tweetsAPIReducerが管理するupdatingプロパティを更新
*/
dispatch(tweetsAPIActions.updateTweets());
})
.then(() => {
/*
非同期処理の終了をディスパッチ
データの流れでは省略
*/
dispatch(tweetsAPIActions.sendTweetDone());
})
.catch(() => {
dispatch(tweetsAPIActions.sendTweetDone());
});
}, [newContent]); // 2. newContentプロパティの更新を検知
return null;
};
おわりに
APIコンポートを利用すると、非同期処理をReduxデータフローの中に組み込むことができます。
また、以下のようにコンポーネントとして配置するだけで、APIコンポートはそのまま機能します。
/* TweetPanel.tsx */
import * as React from 'react';
import { TweetForm } from 'containers/TweetFormCTR';
import { TweetList } from 'containers/TweetListCTR';
import { UpdateTweetsAPI } from 'api/UpdateTweetsAPI';
import { SendTweetAPI } from 'api/SendTweetAPI';
export const TweetPanel: React.FC<{}> = () => (
<div>
<TweetForm />
<TweetList />
<UpdateTweetsAPI />
<SendTweetAPI />
</div>
);