React × Rails SPAの作成時に画像の変更を動的に反映させるのに苦労したのでアウトプット
やりたいこと
以下のようにモーダル内のフォームからニックネームおよびアバター画像を変更し、非同期的にマイページにも反映させたい。
フォーム送信では以下のようにaxiosを用いてAPIと通信した後、React-routerのnavigationを用いてマイページに遷移させている。
formSubmit(e) {
e.preventDefault()
if (this.props.location.state.content == 'Edit Profile') {
axios
.patch('/api/v1/users', {user: {nickname: this.state.user.nickname, avatar: this.state.avatar }} )
.then(response => {
this.props.history.push('/mypage')
// this.props.history.push('/mypage')
return response
})
遷移先では
componentDidMount() {
axios
.get('/api/v1/mypage')
.then(response => {
if (response.data.avatar) {
this.setState({
user: response.data.user,
books: response.data.books,
avatar: response.data.avatar
})
} else {
this.setState({
user: response.data.user,
books: response.data.books,
avatar: Sample
})
}
return response
})
}
のようにcomponentDidMountを用いてAPIと通信しているので
ユーザー情報の更新→history.push→componentDidMount→画面に変更内容が反映
と処理が進む想定。
しかしこれでは画像のみ非同期で変わらなかった。なんで???
試したこと
history.push時に値を遷移先に送る
formSubmit(e) {
e.preventDefault()
if (this.props.location.state.content == 'Edit Profile') {
axios
.patch('/api/v1/users', {user: {nickname: this.state.user.nickname, avatar: this.state.avatar }} )
.then(response => {
this.props.history.push({pathname: '/mypage', state: {user: response.data.user, avatar: response.data.avatar}})
// this.props.history.push('/mypage')
return response
})
こんな感じでpushの引数にstateとして渡すことで遷移先で取得できないか試してみた。
しかしこれではだめだった。
ターミナルのログを見る→そもそもcomponentDidMountが動いていない?
ターミナルのログを見る限りそもそもcomponentDidMountが動いていなさそう?と気づく。
history.pushでcomponentにマウントしているつもりだったけどモーダルだからコンポーネントにはすでにmountしている扱いなのかな。
componentDidUpdateでsetStateを用いる
history.pushで送った値は
this.props.location.state
から取り出せるのでこれを使ってcomponentDidUpdateを使えばいいんじゃないか?と気づく。
componentDidUpdate() {
let updatedProps = this.props.location.state
if (updatedProps) {
if (updatedProps.avatar) {
this.setState({
avatar: updateProps.avatar
})
}
}
}
しかしこの場合
Uncaught Invariant Violation: Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. React limits the number of nested updates to prevent infinite loops.
というエラーが出てしまう。
調べた限りcomponentDidUpdateでsetStateをすると画面が再描画されるのでそのときにまたcomponentDidUpdateが呼び出されて無限ループに入っている?ような感じとみた。
(参考:https://bit.ly/3hclKYS)
componentDidUpdateでsetStateを使わずにstateに値を代入する
setStateがダメならstateに値を代入すればいいじゃんってことで
componentDidUpdate() {
let updatedProps = this.props.location.state
if (updatedProps) {
if (updatedProps.avatar) {
this.state.avatar = updatedProps.avatar
}
}
}
このように記述した。するとstateは変わったが画像自体に変化はなかった。
画像のsrc属性を直接触ってみる
いろいろ調べてみると画像を動的にjsで変化させる際にsrc属性に値を代入する、ということをしている記事をよく見かけたのでそれをやってみることに。
componentDidUpdate() {
let updatedProps = this.props.location.state
if (updatedProps) {
if (updatedProps.avatar) {
this.state.avatar = updatedProps.avatar
document.getElementById('avatar').src = this.state.avatar
}
}
}
avatar、はアバター画像が入っているimgタグのid。
これで無事画像そのものも非同期で変更できた。
感想
ちょっとしたフロントの実装だと思っていましたが思ったより細かいところで躓いてしまった。
Reactのライフサイクルとかもっと勉強しないとなーという気持ちになりました。