はじめに
この記事はギルドワークス Advent Calendar 2019の21日目の記事になります。
現在、約3年以上開発を続けているReactのProjectに関わっているので、ここ半年で取り組んだカイゼンの内容についてここにまとめたいと思います。
改善前(半年前)のProjectの状況
以下が半年前のProjectの状況です。
現在と構成が変わっていない部分はあります。
- Rails + Webpacker + Reactの構成
- React.js v16.4.x
- Redux v4.0.0
- ただし状態管理はreduxのstoreを使っていたり、Componentのstateを使っていたり統一感がない状態
- 非同期処理のmiddlewareには redux-saga を使用
- UI補助ライブラリとしてmaterial-uiとbootstrapを併用
- Component単位で依存しているUI補助ライブラリが違っている状態
当時の課題
以下が、当時のふりかえりで上がってきたReact Projectに対する主な課題でした。
- Reactのコードがどんどん増えてきて、状態管理もstate, props, reduxのstoreと使い分けができておらず複雑になってバグが多くなってきた
- テストしようにもComponent間の依存がすごくて難しくなってきた
- なので大胆な変更をしようにも状態管理まわりでバグを生みそうで怖い
- コーティングガイドラインもちゃんと統一されていなかった
- Reactのコードが増えてきて読み込みに時間がかかるようになってきた
こういった課題が顕著にでてきていたので、自然とReactまわりのコードを徐々に改善していく運びになっていきました。
やってきたこと
ではここ半年で実施してきた施策について挙げていきます。
TypeScriptの導入
まずは、大胆な変更を行うためにはTypeScriptによる静的解析が必須であると考えました。
ただいきなりTypeScriptを導入して型を定義していくのはかなり敷居が高いので、以下の順序で導入していきました。
- まず既存のコードをいっきに jsからts, jsxからtsxファイルへ置換する
- TypeScriptを 型チェックを有効にしないで コンパイラのみ有効にした状態にする
const PnpWebpackPlugin = require('pnp-webpack-plugin')
module.exports = {
test: /\.(ts|tsx)?(\.erb)?$/,
use: [
{
loader: 'ts-loader',
options: PnpWebpackPlugin.tsLoaderOptions({
transpileOnly: true // 型チェックしない
})
}
]
}
- 警告が出ている箇所はロジック変更をしない形で修正を加えていく
- 最終手段として
@ts-ignore
でエラーを回避 - 依存しているライブラリの型定義をインストールしていく (@types/react等)
ここまで実施して、次の項で導入したESLintでTypeScriptの型チェック等の制限を徐々に強くしていって、段階的にコードを修正していく形になります。
参考
当時は以下の記事を参考にして、TypeScriptを軽量に導入する方法の知見を得たりしました。
ESLint + Prettierの導入
TypeScriptが導入された状態で、段階的に型を有効にしていくために、ESLintの plugin:@typescript-eslint
を有効にして、以下の手順でコードを徐々に修正してく手段をとりました。
- デフォルトの設定だと当然エラーが大量にでてしまうので、地道にエラーとなっている箇所を
warn
に変換して警告がでている状態にする - CI環境では
eslint
がパスする状態にする - 機能追加や修正が入るファイルから徐々に警告の箇所を修正する
- state, props等型定義しやすいところから型定義していく。難しい場合は
any
で回避 - 解消した警告から eslintの設定を見直して制限を強くしていく
なお、細かなコーティングガイドラインが無いという課題に対しては、ESLintの設定で airbnb-base
等一般的なコーディングスタイルをlintに組み込むことができるので、開発メンバーのエディタにlinterとPrettierを有効にしてもらって、自動でコーディングの強制を(ゆるく)実施するようにしました。
Prettierは自動でコード整形をしてくれるのでかなり有効です。
参考
既存ComponentからなるべくState管理の排除
せっかくreduxが導入されているにも関わらず、stateで状態を保持しつつ、同じ情報を複数のComponentで引き回すようなコードがいくつかあり、それが複雑性を上げてバグを生みやすくしていました。
具体的なアンチパターンとしては、以下のようなpropsによってstateを変更するようにしているケースです。
this.state.open
はpropsによって更新もされますが、Component内でも変更ができてしまうため、状態管理が複雑になってしまいます。
constructor(props) {
super(props);
this.state = {
open: this.props.open,
};
}
componentWillReceiveProps(nextProps) {
this.setState({
open: nextProps.open,
});
}
こういったコードは、stateを排除してprops参照にする、あるいは reduxのstoreに openの状態をもたせるようにして、状態を一箇所で管理するようにします。
ちなみに LifeCycle関数である componentWillReceiveProps
は Deprecatedになっているので積極的に排除していっています。
参考
React hooksの導入
前項のState管理の排除は既存に対する対処ですが、React HooksがReact v16.8
から有効になってからは、新規で作成するファイルは積極的に React.FC
を用いてステートレスな関数Componentを実現してState管理の複雑性を排除していきます。
参考
Dynamic Importの導入
Reactのコードが増えてきて、且つ機能が複数ページに跨っていたため、Dynamic Importを利用して機能単位で必要になったファイルをロードするようにして、初期ロード時間の短縮を図りました。
以下のような記述に変更してあげることで実現可能になります。
import React, { lazy, Suspense } from 'react';
const HogeContainer = lazy(() => import(/* webpackChunkName: "HogeContainer" */ '../containers/HogeContainer'));
const App = (props) => (
<Suspense fallback={<Loading />}>
<HogeContainer />
</Suspense>
);
export default App;
なお、Dynamic Importは React v16.8
から有効になっています。
参考
今後の導入したいこと
ここからは、まだ導入できていないけど、今後の改善として導入を検討している施策を挙げてみました。
Redux Starter Kitの導入
Reduxは状態管理やロジックを一箇所で管理できるものの、TypeScriptによる型定義が難しかったり、学習コストがかかたっりするところが難点だなと思っています。そこで今年(2019年)の10月にv1.0
がリリースされた Redux Starter Kit
の導入を検討しています。
Reduxのラッパーのようなものなのですが、いくつか恩恵があります。
- TypeScriptの型定義がしやすい
- React Hooksを利用する前提の設計になっている
- Sliceという機能を使ってreducerの記述を簡潔にできる
以下がreducerのサンプルコードです。reducerの関数の呼び出しもかなりシンプルになっています。
import { createSlice } from "redux-starter-kit";
export type TodoItem = {
title: string;
completed: boolean;
key: string;
};
const todoSlice = createSlice({
name: "todo",
initialState: [] as TodoItem[],
reducers: {
addTodo: (state, action: { payload: TodoItem }) => {
state.push(action.payload);
},
removeTodo: (state, action: { payload: string }) => {
return state.filter(item => item.key !== action.payload);
},
setCompleted: (
state,
action: { payload: { completed: boolean; key: string } }
) => {
state.forEach(item => {
if (item.key === action.payload.key) {
item.completed = action.payload.completed;
}
});
}
}
});
export default todoSlice;
import React from "react";
import { ListGroupItem, Button } from "reactstrap";
import todoSlice, { TodoItem } from "../../reducers/todo";
import { useDispatch } from "react-redux";
type Props = {
item: TodoItem;
};
const TaskItem: React.FC<Props> = props => {
const dispatch = useDispatch();
const {
actions: { setCompleted, removeTodo }
} = todoSlice;
const textStyle = {
textDecoration: props.item.completed ? "line-through" : "none"
};
const completeTask = () => {
dispatch(setCompleted({ completed: true, key: props.item.key }));
};
const deleteTask = () => {
dispatch(removeTodo(props.item.key));
};
return (
<ListGroupItem>
<div className="d-flex">
<span className="flex-fill" style={textStyle}>
{props.item.title}
</span>
<div className="ml-auto">
{props.item.completed ? null : (
<Button color="primary" onClick={completeTask}>
Complete
</Button>
)}
<Button color="danger" onClick={deleteTask} className="ml-3">
Delete
</Button>
</div>
</div>
</ListGroupItem>
);
};
export default TaskItem;
参考
カスタムHooksの導入
React hooksの強みは、独自のhooksを作成して、複雑な処理を共通化して流用可能にしつつ、ステートレスな関数Componentを実現できる点です。
例えばシンプルな例ですと、reduxの特定のstore情報の呼び出しをカスタムhooks化して簡潔に呼び出すといったことも可能になります。
import { useSelector } from "react-redux";
import { CombineState } from "../index";
export const useTodoItems = () => {
return useSelector((state: CombineState) => state.todo);
};
import React from "react";
import { ListGroup } from "reactstrap";
import { useTodoItems } from "../../hooks/todo";
import TaskItem from "./TaskItem";
const TaskList: React.FC = () => {
const items = useTodoItems();
return (
<ListGroup className="mt-3">
{items.map((item) => {
return <TaskItem key={item.key} item={item} />;
})}
</ListGroup>
);
};
export default TaskList;
また、外部ライブラリとして多くの便利Hooksが公開されていたりするので、Hooksのエコシステムを上手く活用して綺麗なComponentの記述を実現することもできそうです。
参考
さいごに
かなり長くなってきましたが、ここ半年で取り組んできたレガシーReact Projectに対する取り組みと、今後の改善点の一部を上げてみました。ここには長くなって書けませんが、テストまわりの取り組み(Cypress等)についてもどこかで書ければと思います。
それでは、よりよいReactライフを!