React消息列表映射时Key(index/msgId)失效,无法新增消息容器
问题描述
我正在开发一个简易消息应用,发送新消息时,旧的消息div会被新消息覆盖,而不是新增一个新的div。一开始我用数组index作为map的key,但没用;后来给每条消息加了msgId作为key,问题还是没解决。
相关代码
OpenConversation.js
import React, { useState } from 'react' import {Button, Form, InputGroup, ListGroup} from 'react-bootstrap' import { useConversations } from '../contexts/ConversationsProvider' export default function OpenConversation() { const[text,setText]=useState() const {sendMessage,selectedConversation}=useConversations() function handleSubmit(e) { e.preventDefault() console.log("index",selectedConversation) sendMessage( selectedConversation.recipients.map(r => r.id), text ) setText('') } return ( <div className="d-flex flex-column flex-grow-1"> <div className="flex-grow-1 overflow-auto"> <div className="d-flex flex-column align-items-start justify-content-end px-3"> {selectedConversation.messages.map((message) => { return ( <div key={message.msgId} className={`my-1 d-flex flex-column ${message.fromMe ? 'align-self-end align-items-end' : 'align-items-start'}`} > <div className={`rounded px-2 py-1 ${message.fromMe ? 'bg-primary text-white' : 'border'}`}> {message.text} {message.msgId} </div> <div className={`text-muted small ${message.fromMe ? 'text-right' : ''}`}> {message.fromMe ? 'You' : message.senderName} </div> </div> ) })} </div> </div> <Form style={{position:'absolute',bottom:'0rem'}} onSubmit={handleSubmit} > <Form.Group className='m-2'> <InputGroup > <Form.Control as='textarea' required value={text} onChange={e=>setText(e.target.value)} style={{height:'75PX',resize:'none',}} /> <Button type="submit" style={{background:'#7c73e6',border:'none'}}>Send</Button> </InputGroup> </Form.Group> </Form> </div> ) }
ConversationsProvider.js
import React, { useContext, useState, useEffect, useCallback } from 'react' import useLocalStorage from '../hooks/useLocalStorage'; import { useContacts } from './ContactsProvider'; import {v4 as uuidv4} from 'uuid' const ConversationsContext = React.createContext() export function useConversations() { return useContext(ConversationsContext) } export default function ConversationsProvider({ id, children }) { const [conversations, setConversations] = useLocalStorage('conversations', []) const [selectedConversationIndex,setSelectedConversationIndex]=useState(0) const { contacts } = useContacts() function createConversation(recipients) { setConversations(prevConversations => { return [...prevConversations, { recipients, messages: [] }] }) } function selectConversationIndex(i) { setSelectedConversationIndex(i) } function addMessageToConversation({recipients,text,sender}) { setConversations(prevConversations=>{ let madeChange=false let msgId=uuidv4() const newMessage={sender,text,msgId} const newConversations=prevConversations.map(conversation=>{ if(arrayEquality(conversation.recipients,recipients)) { madeChange=true return{ ...conversation, messages:[conversation.messages, newMessage] // 问题核心所在 } } return conversation }) if(madeChange){ return newConversations } else{ return [...prevConversations,{recipients,messages:[newMessage]}] } }) console.log(conversations) } function sendMessage(recipients,text) { console.log("working") addMessageToConversation({recipients,text,sender:id}) } const formattedConversations = conversations.map((conversation, index) => { const recipients = conversation.recipients.map(recipient => { const contact = contacts.find(contact => { return contact.id === recipient }) const name = (contact && contact.name) || recipient return { id: recipient, name } }) const messages = conversation.messages.map(message => { const contact = contacts.find(contact => { return contact.id === message.sender }) const name = (contact && contact.name) || message.sender const fromMe = id === message.sender return { ...message, senderName: name, fromMe } }) const selected = index === selectedConversationIndex return { ...conversation, messages, recipients, selected } }) const value = { conversations: formattedConversations, selectedConversation: formattedConversations[selectedConversationIndex], sendMessage, selectConversationIndex: setSelectedConversationIndex, createConversation } return ( <ConversationsContext.Provider value={value}> {children} </ConversationsContext.Provider> ) } function arrayEquality(a, b) { console.log("working") if (a.length !== b.length) return false a.sort() b.sort() return a.every((element, index) => { return element === b[index] }) }
问题分析与修复
核心bug
你代码里的关键问题在ConversationsProvider.js的addMessageToConversation函数中,更新消息数组时用了错误的语法:
messages:[conversation.messages, newMessage]
这会把原有的消息数组作为新数组的第一个元素,导致messages变成嵌套数组(比如[[msg1, msg2], msg3])。后续map渲染时,第一个元素是数组而非单个消息对象,React无法正确识别,进而出现旧消息被覆盖的异常。
为什么index和msgId都失效?
不管用index还是msgId,前提是数据源是正确的一维数组。当messages变成嵌套数组后:
- 用index时,React的diff算法会认为原来的消息元素被替换成了一个数组元素,无法正确保留旧DOM;
- 用msgId时,嵌套数组里的第一个元素是原消息数组,没有msgId属性,React会警告且无法匹配到正确的元素,导致渲染混乱。
修复方案
把消息数组的更新逻辑改成展开原数组后添加新消息,保持一维数组结构:
messages: [...conversation.messages, newMessage]
其他映射消息的方式
只要数据源正确,用uuid生成的msgId是最优方案(唯一且稳定)。如果不想用uuid,也可以用发送时间戳+发送者ID的组合作为key,比如:
key={`${message.sender}-${message.timestamp}`}
不过需要给每条消息添加timestamp字段,相比之下uuid更省心。
内容的提问来源于stack exchange,提问作者tanaya sharma
相关产品推荐
相关产品推荐

