目的
ツイッターぽいいいねを実装する。
ツイッターていいね押すとすぐハートが赤くなるけど、(少し昔の)airbnbは赤くなるまで少し時間がかかる。
どちらが良いかという話ではなく、自分はツイッターみたくすぐ赤くしたかった。
これを実現するのが目的。
もう一つの話として、
reactのライフサイクルが新しくなる話。
https://mae.chab.in/archives/60040
今まで componentWillReceiveProps
をダメと知りながらも愛用してきたが、やっぱり文句言われすぎて、手放すことにした。
上の記事によると、
componentWillMount
、componentWillReceiveProps
、 componentWillUpdate
と合わせて、api通信時に色々問題を起こす可能性があるかつ、使い方が明確でなく、混乱するユーザーが多くいたらしい。
ので書き換えることにした。
componentWillReceiveProps
の代用はgetDerivedStateFromProps
でいけるらしい。
今まで
ツイッターみたいな「いいね」をreact native + rails + mysqlで実装するときcomponentWillReceiveProps
を使っていた。
(いまどきfirebase使えという話ではあるが、、)
流れ的には
アイテムをいいねする(ハートをonPress)
→apiにrequest
→サーバ側でこのアイテムのステータスを(いいねされた状態に)変える
→レスポンスを返す
→レスポンスが返ってきたらハートの色を変える
である。
このレスポンスが返ってきたとき、propsが変化するのでこれをcomponentWillReceivePropsでキャッチしていた。
具体的には以下。
import React, { Component } from 'react';
...
type Props = {
...
}
class Good extends Component<Props> {
state = {
isGood: false, <-- アイテムがいいねされているか
}
componentDidMount() {
...
this.setState({
isGood, <--- 初期状態でapiからいいねされているか取ってきてsetStateする
});
}
componentWillReceiveProps(nextProps) {
const { isGood } = nextProps;
this.setState({
isGood, <--- いいねした時apiからレスポンス返ってきたらsetStateする
});
}
render() {
const { isGood } = this.state;
return (
...
<TouchableOpacity
onPress{() =>
this.setState({
isGood: !isGood,
});
this.someAction(...); <--いいねするaction
}
>
... <--- いいねボタン(ハート)
</TouchableOpacity>
...
)
}
こんな感じ。
わざわざ、storeで管理しているものをいちいちsetStateしているのはいいねして返ってくるまで時間がかかって、色が変わるのがラグく感じてしまうのをやめたかった。
昔のairbnbのいいねはこのタイプだった。
ツイッターはすぐ色変わる。
つまり、onPress内でまず、stateを更新してすぐ色変え、api通信から戻ってきた結果をcomponentWillReceiveProps
内でsetStateしている、ということ。
onPress内でsetStateしないバージョンが上のgif(さっき載せたやつと同じ)
setStateすると下のようになる
これで一応やりたいことはできている。
ちなみにcomponentWillUpdate
の中身でsetStateするのはメモリ食いつぶすので禁止されているのでここでは使えない。
getDerivedStateFromPropsを使う
componentWillReceiveを使っていたところを書き換えた。
// componentWillReceiveProps(nextProps) {
// const { isGood } = nextProps;
// this.setState({
// isGood, <--- いいねした時apiからレスポンス返ってきたらsetStateする
// });
//}
static getDerivedStateFromProps(nextProps, prevState) {
const { isGood } = nextProps;
if (prevState.isGood !== isGood) { <--nextPropsが更新されたら
return { isGood: isGood };
}
return {};
}
nextPropsには新しいprops, prevStateには今のstateが自動的に入る。
nextPropsが更新されたらstateを書き換える。
これで全く同じことが実現できた。
getDerivedStateFromProps
はstatic メソッドでこれを実行することは
componentDidUpdate
+ setStateするのと同じことらしい。
【追記】reduxを使っていればgetDerivedStateFromPropsは不要
コメントをいただいて気づいた。
上で実現したかったことはreduxを使い、変数isGoodをstoreで管理していれば
getDerivedStateFromPropsは不要になる。
一例を書けば、以下のようになる。
import React from 'react';
...
type Props = {
...
}
const Good = (props: Props) => {
const { isGood } = props;
return (
<TouchableOpacity
onPress{() =>
this.setIsGood(!isGood) <--これがないときはツイッターみたいにすぐ色変わらない
this.updateIsGood(!isGood); <--いいねするaction
}
>
... <--- いいねボタン(ハート)
</TouchableOpacity>
...
)
}
export const setIsGood = isGood => ({
type: SET_IS_GOOD,
payload: isGood,
});
export const updateIsGood = isGood => ({
postでapiにリクエスト飛ばす処理
.then((res) => { <-- resに いいねされているかに対応する true or falseが入っているとする
dispatch(setIsGood(res)); <-- storeに値をsetする
.catch((err) => {
dispatch(setIsGood(!isGood)) <-- レスポンスエラーの時、storeに変更前の値を入れる
}
});
const initialState = {
isGood: false,
};
export function isGoodState(state = initialState, action) {
switch (action.type) {
case SET_IS_GOOD:
return {
...state,
isGood: action.payload,
};
default:
return state;
}
}
...
const rootReducers = combineReducers({
isGoodState,
...
})
export default rootReducers;
これならgetDerivedStateFromPropsはいらないし、
pure functionで書くことができる