この記事はプログラミング学習者がアプリ開発中に躓いた内容を備忘録として記事におこしたものです。内容に不備などあればご指摘頂けると助かります。
0.前提条件
X(旧Twitter)のクローンサイトの制作過程でユーザーフォロー機能を実装した時に内容を修正した過程についてご紹介したいと思います。
フロントエンド:React(JavaScript) 19.1.0
バックエンド:Ruby on Rails(Ruby) 7.0.0 APIモード
インフラ:Docker
PC;Mac Book Air M2チップ
1.当初の実装内容と修正の経緯
遷移元のLinkタグからProfilePageコンポーネントへ遷移する際にstateでtweetを受け取り、それをuseLocationで処理した後にuseStateの初期値に設定しています。
handleFollow関数とhandleUnFollow関数でユーザーのフォローを管理しています。
プロフィール画面に表示されている「フォロー」若しくは「フォロー中」のボタン表示を切り替えるためにuseEffectのトリガーとなるupdate変数を用意してフォローが切り替わる度に更新されたデータをuseEffect内の非同期処理で取得して、再レンダリングしていました。
// グローバルステートのログインユーザーを取得
const { userInfo } = useContext(saveUserDataContext);
const location = useLocation();
const { tweet } = location.state || ""; //一覧ページからLinkで付与された値を受け取る
const [tweetData, setTweetData] = useState(tweet);
useEffect(() => {
const getUpdatedDesignatedTweet = async () => {
const response = await axiosInstance.get(`/tweets/${tweetData.id}`);
console.log(response.data);
setTweetData(response.data.data.tweet);
};
getUpdatedDesignatedTweet();
}, [update]);
const handleFollow = async (id) => {
const response = await axiosInstance.post(`/users/${id}/follow`);
console.log(response.data);
// state変数を反対の値に切り替えることで再レンダリングを誘発する
setUpdate(update ? false : true);
};
const handleUnfollow = async (id) => {
const response = await axiosInstance.delete(`/users/${id}/unfollow`);
console.log(response.data);
// state変数を反対の値に切り替えることで再レンダリングを誘発する
setUpdate(update ? false : true);
};
<ProfileIconAndEditButton>
{tweetData.user.icon_urls ? (
<ProfileIcon src={tweetData.user.icon_urls} />
) : (
<ProfileDummySpace />
)}
{tweetData.user.followers.some(
(follower) => follower.id === userInfo.id
) ? (
<EditButton onClick={() => handleUnfollow(tweetData.user.id)}>
フォロー中
</EditButton>
) : (
<EditButton onClick={() => handleFollow(tweetData.user.id)}>
フォロー
</EditButton>
)}
</ProfileIconAndEditButton>
このコードについてコードレビューを受けた所、不必要に非同期処理を増やさずに遷移元から取得したtweetの内容を更新関数を使って変更することで同様の機能を実装することになりました。
2.修正した内容の説明
修正内容ですが、useEffectを使って最新のデータをバックエンド側から取得していた実装を更新関数で必要箇所だけ書き換えるように実装しました。
書き換えるデータはtweetData内のuser情報にあるfollowersです。
followersは対象ユーザーをフォローしているユーザーを指しています。
言い換えると、ツイートを投稿したユーザーをフォローしているユーザーとなります。
※ここではバックエンド側のアソシエーションに関する説明は省きます。
// ユーザーフォロー時の処理、tweetData変数はフォローしているユーザーにログインユーザーを追加している。
const handleFollow = async (id) => {
const response = await axiosInstance.post(`/users/${id}/follow`);
console.log(response.data);
setTweetData({
...tweetData,
user: {
...tweetData.user,
followers: [...tweetData.user.followers, userInfo],
},
});
};
// フォロー解除時の処理、tweetData変数はフォローしているユーザーからログインユーザーを削除している。
const handleUnfollow = async (id) => {
const response = await axiosInstance.delete(`/users/${id}/unfollow`);
console.log(response.data);
setTweetData({
...tweetData,
user: {
...tweetData.user,
followers: tweetData.user.followers.filter(
(follower) => follower.id !== userInfo.id
),
},
});
};
最終的には上記の内容で問題無く動いていますが、配列・オブジェクトの更新処理を実装するのがちょっと久しぶりになってしまったので、スプレッド構文のことをすっかり忘れてしまっていました。
以下、間違った実装をした時の例です。
setTweetData(
tweetData.user.followers.filter((follower) => follower.id === userInfo.id)
);
この実装では2つ誤りがあります。
実装の目的はフォロー解除した時にfollowersの中からログインユーザーを取り除くことです。
- 間違い1:filter関数を使っていますが、条件の指定が間違っています。
follower.id === userInfo.idの条件だとfollowerのidとログインユーザーのidが一致するデータだけを残すことになります。
正しくはfollower.id !== userInfo.idと実装する必要があります。
これでフォロワーのidとログインユーザーのidが一致しないデータだけを残すことになります。
言い換えると、フォロワーのidとログインユーザーのidが一致するデータを削除できます
- 間違い2:更新関数内でfilter関数の結果だけで更新しようとしている。
更新関数は中の値でstate変数を変更するための関数です。
上の実装ではtweetDataのfollowersをfilter関数にかけて実行した結果で更新しようとしているので、中身がfollowersだけの値になってしまいます。実際のtweetData変数の中身は先に載せたスクリーンショットに記載されたものです。
親 tweetData
子 - comments
子 - favorites
子 - retweets
子 - user - followers ・・・今回の更新したい内容
誤った内容で更新した場合は下記のようになります。
親 tweetData
子 - followers
他のデータは全て消えてしまいます。
正しい実装は前述していますが、下記の通りになります。
setTweetData({
...tweetData,
user: {
...tweetData.user,
followers: tweetData.user.followers.filter(
(follower) => follower.id !== userInfo.id
),
},
});
階層上のデータを更新する場合、...(スプレッド構文)で既存のデータを全て展開する必要があります。
その上で、更新したい部分だけを関数や配列操作で変更します。
これにより変更前と同じ構造のデータで中身の一部が変わった状態として更新してデータの構造を保つことができます。
以上が実装内容の説明となります。
最後まで読んで頂きありがとうございました。
