実現したいこと
最終的に実現したいことは、コメントの一覧に投稿した時刻を表示すること
です。
背景
上記の画像の様に、formと表示が一体となっているコメント一覧をReact
、TypeScript
、Firestore
を用いて作成しています。
コメントを投稿すると、firestoreのonSnapshotでリアルタイムで検知してしたのコメント一覧に表示するという仕組みになっていて、実装自体は完了していて、リアルタイムでの表示もうまくできています。
ソースコード
以下は該当部分のソースコードです。
※importなど少し省略しています。
CommentForm.tsx
CommentForm.tsx
1export default function CommentForm({ id }: any) { 2 const [loading, setLoading] = useState(false); 3 const [error, setError] = useState(""); 4 const [content, setContent] = useState(""); 5 const { currentUser, createComment }: any = useAuth(); 6 const [open, setOpen] = React.useState(false); 7 const [comment, setComment] = useState([]); 8 9 const handleClose = (event?: React.SyntheticEvent, reason?: string) => { 10 if (reason === "clickaway") { 11 return; 12 } 13 setOpen(false); 14 }; 15 16 const inputContent = useCallback( 17 (event) => { 18 setContent(event.target.value); 19 }, 20 [setContent] 21 ); 22 23 const handleSubmit = async (e: any) => { 24 e.preventDefault(); 25 26 if (content === "") { 27 return setError("コメントを入力してください"), setOpen(true); 28 } 29 if (content.length > 200) { 30 return setError("200文字以内で入力してください"), setOpen(true); 31 } 32 33 try { 34 setError(""); 35 setLoading(true); 36 const uid = currentUser.uid; 37 return createComment(id, content, uid); 38 } catch { 39 setError("投稿に失敗しました"); 40 setOpen(true); 41 } finally { 42 setLoading(false); 43 setContent(""); 44 } 45 }; 46 47 useEffect(() => { 48 let comments: any = []; 49 const unsubscribe: any = db 50 .collection("posts") 51 .doc(id) 52 .collection("comments") 53 .orderBy("createdAt", "desc") 54 .onSnapshot((snapshots) => { 55 snapshots.docChanges().forEach((change) => { 56 const data = change.doc.data({ serverTimestamps: "estimate" }); 57 const changeType = change.type; 58 const date = data.createdAt.toDate(); 59 60 switch (changeType) { 61 case "added": 62 comments.push({ ...data, createdAt: date }); 63 break; 64 case "modified": 65 const index = comments.findIndex( 66 (comment: any) => comment.id === change.doc.id 67 ); 68 comments[index] = comment; 69 break; 70 case "removed": 71 comments = comments.filter( 72 (comment: any) => comment.id !== change.doc.id 73 ); 74 break; 75 default: 76 break; 77 } 78 }); 79 setComment(comments); 80 return () => unsubscribe(); 81 }); 82 }, []); 83 84 return ( 85 <> 86 <form noValidate autoComplete="off" onSubmit={handleSubmit}> 87 <textarea 88 className="comment-textarea" 89 name="content" 90 id="content" 91 placeholder="コメントを残す" 92 onChange={inputContent} 93 value={content} 94 ></textarea> 95 <div className="comment-submit-wrapper"> 96 <Button 97 className="comment-submit" 98 color="primary" 99 variant="contained" 100 type="submit" 101 disabled={loading} 102 > 103 投稿する 104 </Button> 105 </div> 106 </form> 107 {console.log(comment)} 108 {comment.map((commentItem: any) => ( 109 <Comment 110 key={commentItem.id} 111 id={commentItem.id} 112 uid={commentItem.uid} 113 content={commentItem.content} 114 createdAt={commentItem.createdAt} 115 /> 116 ))} 117 </> 118 ); 119}
CommentForm.tsx
のhandleSubmit
内にあるcreateComment
の中身は以下の通りです。
AuthContext.jsx
1// timestampはfirebaseのserverTimestampです。 2 const createComment = (postId: string, content: string, uid: string) => { 3 db.collection("posts") 4 .doc(postId) 5 .collection("comments") 6 .add({ 7 content: content, 8 createdAt: timestamp, 9 uid: uid, 10 }) 11 .then(async (result: any) => { 12 const id = result.id; 13 const commentsRef = db 14 .collection("posts") 15 .doc(postId) 16 .collection("comments") 17 .doc(id); 18 await commentsRef.set({ id: id }, { merge: true }); 19 }); 20 };
Comment.tsx
Comment.tsx
1export default function Comment({ id, uid, content, createdAt }: any) { 2 const [commentUsername, setCommentUsername] = useState(); 3 4 useEffect(() => { 5 db.collection("users") 6 .doc(uid) 7 .get() 8 .then((snapshot: any) => { 9 const data: any = snapshot.data(); 10 const username = data.username; 11 setCommentUsername(username); 12 }); 13 }, []); 14 15 return ( 16 <div className="comment-block"> 17 <p>{commentUsername}</p> 18 <p>{content}</p> 19 {/* <p>{createdAt}</p> */} 20 {/* createdAtを入れるとError: Objects are not valid as a React childというエラーになる */} 21 </div> 22 ); 23}
問題点
実装自体はうまくいっているのですが、リアルタイムで投稿を表示するために、CommentForm.tsx
のuseEffectで投稿を取得する部分をgetからonSnapshotに変えたあたりから、何かしらコードを間違えたのか、console.log(comment)
でuseEffectで取得した内容を入れたstateをlogに出力してみたところ、3つしかコメントがないのに、以下の画像の様にコンソールに大量にログが出力されました。
もちろん、returnの中でsetStateの様なことをすると無限にレンダリングされることは知っているのですが、今回はhooksのセッター関数はreturn内では使っていません。
また、propsとして、子のコンポーネントであるComment.tsx
に渡したcreatedAt
をComment.tsx
でlogに出力してももちろん変わらず無限にレンダリングされます。
さらに、Comment.tsx
でcreatedAt
のpropsを無理やり表示しようとしてもError: Objects are not valid as a React child (found: Thu Dec 17 2020 19:48:18 GMT+0900 (日本標準時)). If you meant to render a collection of children, use an array instead.
というエラーが出ます。
Reactについての理解が乏しく、原因がわかりそうになかったので質問させていただきました。
ご回答お待ちしております。
追記
useEffect内にてリスナーのデタッチのためのunsbscribeをreturnしていますが、ご指摘いただいた通り、returnを消してunsubscribe()
だけに変更したらレンダリングが抑えられる様になりました。
しかしながら、時間が経って再びconsoleに出力すると、直す前と変わらず無限にレンダリングがされてしまっていました。その上で、リアルタイムのコメントの反映もされなくなってしまったので、onSnapshot((snapshots) => {})
の外にreturn unsubscribe;
とすることで、リアルタイムのコメントの反映の部分は直ったのですが、依然として無限にレンダリングされる現象が直りません。
CommentForm.tsx
1 useEffect(() => { 2 let comments: any = []; 3 const unsubscribe: any = db 4 .collection("posts") 5 .doc(id) 6 .collection("comments") 7 .orderBy("createdAt", "desc") 8 .onSnapshot((snapshots) => { 9 snapshots.docChanges().forEach((change) => { 10 const data = change.doc.data({ serverTimestamps: "estimate" }); 11 const changeType = change.type; 12 13 switch (changeType) { 14 case "added": 15 comments.push(data); 16 break; 17 case "modified": 18 const index = comments.findIndex( 19 (comment: any) => comment.id === change.doc.id 20 ); 21 comments[index] = comment; 22 break; 23 case "removed": 24 comments = comments.filter( 25 (comment: any) => comment.id !== change.doc.id 26 ); 27 break; 28 default: 29 break; 30 } 31 }); 32 setComment(comments); 33 }); 34 return unsubscribe; //変更箇所 35 }, []); 36
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
2020/12/18 02:02