🥵
Reduxによる状態管理
今回はMattermostというオープンソースのSlackクローンアプリの状態管理で使われているReduxというライブラリに触れていきたいと思います。
Mattermostのビルド方法については以下の記事で解説しています。
Reduxとは?
ReduxはJavascriptやReactアプリケーションでの状態管理を効率的に行うためのライブラリです。
しかし、Reactの状態管理で、よく使われるのはuseState
やuseContext
フックだと思います。実際、簡単なシングルページwebアプリケーションなどであれば、useState
で十分に状態管理を行うことができると思いますし、ページ間でのちょっとした状態管理であればuseContext
を使えば事足りることが多いと思います。Redux
はuseState
やuseContext
では管理しきれないような大規模なアプリケーションの状態管理に用いられることが多いようです。
Reduxの仕組み
Reduxの状態管理の概念図は、以下のようになっている。登場する概念は大きく以下の3つです。
- Store(アプリケーション全体の状態を一元的に管理する)
- Action(状態を変更するために発行されるイベントのようなもの)
-
Reducer(
Action
の基づいてどのように状態を変更するか定義する)
Reduxの具体的な実装
今回は授業課題の一環として、投稿のドロップダウンメニューにその投稿を翻訳するという機能を実装することを目標としました。この実装の中で、Reduxによる状態管理を利用したので、それを元にReduxを利用した具体的な実装を解説していきます。(軽微な編集も含めれば、実際には編集するファイルの数は膨大なので、ここでは重要な部分のみを提示します)
1. ドットメニューに翻訳ボタンを追加する
-
DotMenu
コンポーネントに翻訳用のMenuItem
ボタンを追加 -
translatePost
という関数を受け取るようにPropsに追加
mattermost/webapp/channels/src/components/dot_menu/dot_menu.tsx
@@ -122,6 +123,11 @@ type Props = {
*/
setThreadFollow: (userId: string, teamId: string, threadId: string, newState: boolean) => void;
+ /**
+ * Function to translate the Post Content
+ */
+ translatePost: (post: Post, locale: string) => void;
}; // TechDebt: Made non-mandatory while converting to typescript
canEdit: boolean;
@@ -327,6 +333,10 @@ export class DotMenuClass extends React.PureComponent<Props, State> {
);
};
+ handleTranslateMenuItemActivated = (e: ChangeEvent) => {
+ this.props.actions.translatePost(this.props.post, this.props.intl.locale);
+ }
+
handleCommentClick = (e: ChangeEvent) => {
trackDotMenuEvent(e, TELEMETRY_LABELS.REPLY);
this.props.handleCommentClick?.(e);
@@ -695,6 +705,20 @@ export class DotMenuClass extends React.PureComponent<Props, State> {
isDestructive={true}
/>
}
+ <Menu.Item
+ id={`translate_post_${this.props.post.id}`}
+ data-testid={`translate_post_${this.props.post.id}`}
+ labels={
+ <FormattedMessage
+ id='post_info.translate'
+ defaultMessage='Translate'
+ />
+ }
+ leadingElement={<GlobeCheckedIcon size={18}/>}
+ trailingElements={<ShortcutKey shortcutKey='T'/>}
+ onClick={this.handleTranslateMenuItemActivated}
+ />
</Menu.Container>
);
}
ソースコード
2. Actionの定義
- 先ほど定義した
transaltePost
関数をdispatchする関数を定義する
mattermost/webapp/channels/src/actions/post_actions.ts
@@ -432,3 +440,10 @@ export function emitShortcutReactToLastPostFrom(emittedFrom: keyof typeof Consta
payload: emittedFrom,
};
}
+export function translatePostContent(post: Post): ActionFuncAsync {
+ return async (dispatch) => {
+ dispatch(PostActions.translatePost(post));
+ return {data: true};
+ };
+}
ソースコード
3. DispatchされたActionに対しての翻訳処理の実装
- 翻訳を行うバックエンドAPIである
Client4
に対してPost
のデータを送って翻訳を行う - 翻訳された
translatedPost
をdispatchする
mattermost/webapp/channels/src/packages/mattermost-redux/src/actions/posts.ts
@@ -1130,6 +1130,23 @@ export function removePost(post: ExtendedPost): ActionFunc<boolean> {
};
}
+export function translatePost(post: Post): ActionFuncAsync {
+ return async (dispatch, getState) => {
+ try {
+ const translatedPost = await Client4.translatePost(post);
+ dispatch({
+ type: TRANSLATE_POST_SUCCESS,
+ data: translatedPost,
+ });
+ } catch (error) {
+ forceLogoutIfNecessary(error, dispatch, getState);
+ dispatch(logError(error));
+ return {error};
+ }
+ return {data: true};
+ };
+}
+
export function moveThread(postId: string, channelId: string): ActionFuncAsync {
return async (dispatch, getState) => {
try {
ソースコード
4. バックエンドと通信して翻訳処理を行う部分を実装
- バックエンドサーバーは
Go
言語で記述されているので、具体的な実装は別メンバーの記事で紹介されています。 - ここでは、簡易的に翻訳メッセージを返す処理を書いています。
mattermost/webapp/platform/client/src/client4.ts
@@ -1,6 +1,20 @@
+ translatePost = (post: Post) => {
+ // バックエンドがまだ実装されていないので、モックデータを返します
+ return new Promise<Post>((resolve) => {
+ // モックデータ: ここでは`message`が翻訳されたと仮定して、"Translated: "を追加
+ const translatedMessage = `Translated: ${post.message}`;
+ const newPost = {
+ ...post,
+ message: translatedMessage, // 翻訳後のメッセージをセット
+ };
+
+ // 1秒後にモックデータを返す
+ setTimeout(() => {
+ resolve(newPost);
+ }, 1000);
+ });
+ };
getPostThread = (postId: string, fetchThreads = true, collapsedThreads = false, collapsedThreadsExtended = false) => {
// this is to ensure we have backwards compatibility for `getPostThread`
ソースコード
5. Reducderで状態をStoreに反映させる
-
ActionTypes
で定義したTRANSLATE_POST_SUCCESS
(実装自体は省略)に対して、具体的にStoreをどう更新するかの処理を書く
mattermost/webapp/channels/src/packages/mattermost-redux/src/reducers/entities/posts.ts
@@ -323,6 +324,23 @@ export function handlePosts(state: IDMappedObjects<Post> = {}, action: AnyAction
case UserTypes.LOGOUT_SUCCESS:
return {};
+ case TRANSLATE_POST_SUCCESS: {
+ const translatedPost = action.data;
+ // 既存のポストを更新する
+ const nextState = {
+ ...state,
+ [translatedPost.id]: {
+ ...state[translatedPost.id],
+ message: translatedPost.message, // メッセージを翻訳された内容に更新
+ translated: true, // 翻訳済みのフラグを追加
+ },
+ };
+ return nextState;
+ }
default:
return state;
}
@@ -1620,3 +1638,4 @@ export default function reducer(state: Partial<PostsState> = {}, action: AnyActi
return nextState;
}
ソースコード
Reduxのメリットとデメリット
Reduxによる状態管理のメリットとしては、以下の点があると思います。
-
useState
に比べて状態変数に対しての操作のTYPEなどが定義できて、状態変数への操作の種類が増してきたときに、管理がしやすくなる -
useContext
に比べて、状態変数の種類が膨大でも管理が行いやすく、また、状態変数の変更履歴なども保持することができる - とにかく、大規模なアプリケーションにおいて、様々な状態変数の管理と状態変数の操作がある場合にはスケーラビリティの観点から見ても
Redux
を利用することはメリットがある
一方で、以下のようなデメリットもあるように思いました。
- そもそも処理が複数のファイルに分割されすぎていて、よくわからない(今回の機能追加においても、一つの機能を追加するだけで操作するべきファイルの数が膨大すぎて正直よくわからなくなる)
- ちょっとした機能を追加するだけでも、
Action
とDispatch
とReducer
を別々で定義する必要があり、かなり面倒 - よっぽど大規模なアプリでない限り、
useContext
やuseState
で事足りているのではないか?と思う。
Mattermostを通してReduxに触れてみての感想
- Mattermostは非常に大規模なアプリケーションで、ファイルの数が膨大であるし、同じ名前のファイルがたくさん存在しているため、どこに何が実装されているのかがよくわからなくなります。
- もっと、それぞれのディレクトリやファイルの責務を明確にしたドキュメントなどがあると私のような初学者でも触りやすくなるのではないかと感じました。
-
React
のuseReducer
がRedux
に似ているので、まずはそこからトライするのがいいように思いました笑。(後日談)
Discussion