少し前までAngularを使って開発してきましたが、年明けごろからReactを使い始めたので、自分が使うようになったパターンなどをまとめておきます。随時更新予定です。間違いなどがあればコメントや引用で教えてもらえると大変助かります。
使っている言語・ライブラリはReact, TypeScript, RxJSです。
RxJSはAngularで使っており慣れているのでReactでも引き続き使っていこうと試行錯誤しています。使おうとしたライブラリが型定義の無かったりと面倒だったので、まずは素のReactで解決しようとしています。今後は状態管理に別のライブラリを使うかもしれません。
更新履歴
- 2019/3/5 RxJSの節を修正しました。
型付け
-
state
やprops
のメンバーはComponent内では書き換えてはならないので、すべてのメンバーにreadonlyを付ける。全メンバーにreadonlyを付けるのは面倒なので以下のように定義している。
interface IProps extends Readonly<{
member1: number,
member2: string,
member3: number[],
}>{}
これで
interface IProps {
readonly member1: number;
readonly member2: string;
readonly member3: number[];
}
と同じ意味になる(はず)。
この記事によると、stateとpropsのReadonly化はReactの型定義ですでにやってくれているのでいちいち自前で書かなくてもよいとのことだが、少なくとも自分の環境ではそのような挙動にならなかったのでとりあえず自前でReadonlyを追加している(いずれ解決したい)。
- スタイリングはエラー発見しやすいCSSinJSを使っている。CSSオブジェクトの型定義を以下のように作成した。
import { CSSProperties } from 'react';
export interface CSSobj { readonly [key: string]: CSSProperties; }
使い方
const css: ICSSobj = {
tableWrapper: {
overflowX: 'auto',
},
spacer: {
flexGrow: 1,
},
footer: {
display: "flex",
flexDirection: "row",
alignItems: "center",
},
resetButtonWrapper: {
padding: "7px",
},
};
stylingはパフォーマンスやその他の面でより良い方法がありそうなので、いずれ更新する予定。
関数コンポーネント(FC)
ステートレスコンポーネントを関数で簡単に書けるのがReactを使い始めて一番良いと思ったところ。
自分は基本的に、state管理を行う親コンポーネント(class)と、ロジックをほぼ持たずCSSスタイリング等を行うview専用の子コンポーネント(FC)の親子組を作るようにした。
class InputWithReset extends React.Component<Readonly<{
placeholder: string;
valueChange: (v: string) => void;
}>, Readonly<{
value: string
}>> {
state = {
value: '',
};
valueChange = (value: string) => {
this.setState({ value: value });
this.props.valueChange(value);
}
resetClick = () => {
this.setState({ value: '' });
this.props.valueChange('');
}
render = () => (
<InputWithResetView
value={this.state.value}
placeholder={this.props.placeholder}
valueChange={this.valueChange}
resetClick={this.resetClick}
/>
)
}
const css: ICSSobj = {
input: {
minWidth: "120px",
},
};
const InputWithResetView = (props: Readonly<{
placeholder: string;
value: string;
valueChange: (value: string) => void;
resetClick: () => void;
}>) => {
const onInput = (ev: React.ChangeEvent<HTMLInputElement>) =>
props.valueChange( ev.target.value || '' );
return (
<FormControl>
<InputLabel shrink htmlFor="input">{props.placeholder}</InputLabel>
<Input
style={css.input}
id="input"
type='text'
value={props.value}
onChange={onInput}
endAdornment={
<InputAdornment position="end">
<IconButton
aria-label="Reset Input"
onClick={props.resetClick}
>
<ClearIcon />
</IconButton>
</InputAdornment>
}
/>
</FormControl>
);
};
event.target.value
を取り出すようなhtml要素に近い部分の整形はviewの方で行うようにしている。
import文も整理されるので親子は別ファイルに書くことにした。
Props変更時の処理
class componentにおいてpropsの値が変わったとき、renderは呼ばれるが他の場所に記述したpropsに依存する変数は必ずしも再計算されない。
propsが変わったときにレンダリング以外の処理を行いたい場合については、公式ページの getDerivedStateFromProps の説明に書かれている。stateをpropsに依存させるのはバグの元になりやすいのでgetDerivedStateFromProps
メソッド以外の方法を推奨しているようだった。
対応方法が箇条書きで3つ書かれており、どれが適しているかは場合によるが、自分はconstructor内のロジックも含めて全体的に再計算したかったため3つ目を採用した。親コンポーネントにおいてこのコンポーネントをkey={Date.now()}
を付けて呼ぶことで、key
の変更によりコンポーネントが再生成されることを利用するという方法だ。
interface IProps extends Readonly<{ data: any }>{}
const ParentFC = (props: IProps) => (
<div>
{!!props.data &&
<Child
key={Date.now()} // recreate this component every time when props change
data={props.data}
/>
}
</div>
);
const eq = (prev: IProps, curr: IProps) => (
(prev.data === curr.data)
);
export const Parent = React.memo(ParentFC, eq);
class Child extends React.Component<IProps, IState> {
constructor(props: IProps) {
super(props);
// propsの値を使った様々な計算
}
...
}
また、コンポーネント全体を再生成するため高コストであることも考慮し、メモ化も行っている。
前述記事の箇条書き2つ目の use a memoization helper をやりたくなかった理由は、本来表示に関するメソッドであるはずのrender
内にロジックを書くのが気持ち悪かったので。
props変更時にrender
は呼ばれるので、内部変数の再計算をここで行い、一部のみ変更したい場合に対応するためにメモ化するという方法のようで、必要な再計算が一部のみの場合はこちらを採用した方がパフォーマンスが向上すると思われる。
RxJS
複雑な状態管理にはRxJSを使っている。Angularのようにasyncパイプなどはなさそうなので、subscribe/unsubscribeを書く必要があるが、どこに書くのか分からなかったのでメモ。
Angularでやっていた時と同じく、takeWhile
とalive
変数を使って自動unsubscribeさせるパターンを使っている。
// 訂正前(良くない例)
export class A extends React.Component<IProps, IState> {
state = {
data: [],
};
...
private alive = true;
componentWillUnmount() { this.alive = false; }
componentDidMount() {
this.data$.takeWhile( () => this.alive )
.subscribe( v => {
this.setState({ data: v });
});
}
...
}
↑この方法には問題があるというご指摘をこちらの記事でいただきました。@rithmety さん、ありがとうございます。
unsubscribeをこの方法でやろうとしたのは、subscriberにいちいち名前を付けないといけないのが面倒で、変数が多い時に忘れそうだったため。
何か良い方法はないかと調べていたところAngular: Don't forget to unsubscribe()という記事を見つけ、ここで紹介されていたtakeWhile
を使う方法を使い始めた(が、この記事が私が最後に見た後の2018年7月に更新されており、現在はtakeUntil
を使うべきという主張になっている)。
takeWhile
は、真理値関数f
を受け取り、上流のObservableに値が流れてきたときにf
を実行し、一度結果がfalse
になったらcompleteするというオペレーター。.takeWhile( () => this.alive )
だとコンポーネントがunmountされるときにunsubscribeされるのではなく、次の値が流れてきたときに初めてfalse
に評価されcompleteするのでメモリリークの危険性がある(takeWhile
の挙動については理解していたつもりだったが、この使い方のときに問題があることは見落としていた)。
上記記事にある通り、takeUntil
というメソッドを使うとこれを解決できる。takeUntil
は、Observable a
を受け取り、a
が初めて発火するまでソースのObservableをそのまま流すオペレーター。これを以下のように用いると、コンポーネントのunmountと同時にObservableがcompleteしてくれる。
// 訂正後
export class A extends React.Component<IProps, IState> {
state = {
data: [],
};
...
private readonly unsubscriber$ = new Subject<void>();
componentWillUnmount() {
this.unsubscriber$.next();
this.unsubscriber$.complete();
}
componentDidMount() {
this.data$.takeUntil( this.unsubscriber$.asObservable() )
.subscribe( v => {
this.setState({ data: v });
});
}
...
}
前述の@rithmety さんの記事(「takeWhile はメモリリーク (無駄なメモリ消費) の原因になり得る」)の補足に、ReactとRxJSを組み合わせるときに使えるライブラリなどについて載っておりますのでぜひご確認ください。
取り急ぎ訂正のみの更新をしましたが、このあたりのライブラリも含めて調べたうえで近いうちにまた更新したいと思います。