简体   繁体   中英

Why is my react element rendering many times?

Problem : I have the following react component. Basically, I'm building a chat application in react with socket.io

import { io } from 'socket.io-client';

const socket = io(ServerEndURL);
export default function Chat (props) {
    const uuid = props.uuid;
    const classes = useStyles();
    const [open, setOpen] = useState(false);
    const [btnColor, setBtnColor] = useState('white');
    const [activeStatus, setActiveStatus] = useState('Offline');
    const [unreadMsgs, setUnreadMsgs] = useState(0);
    const [msgArr, setMsgArr] = useState([]);
    const chatBtnRef = useRef();
    const chatBoxRef = useRef();
    const msgInputRef = useRef();
    useEffect(() => {
        socket.emit('join', uuid);
        socket.emit('isActive', uuid);
        return () => {
            console.log('removed');
            socket.off('newMsg');
        }
    }, []);

    socket.on('newMsg', msg => {
        console.log('debug');
        let msgs = msgArr.map(msg => msg);
        msgs.push({
            type : 'received',
            msg,
        });
        setMsgArr(msgs);
        if(!open) setUnreadMsgs(unreadMsgs + 1);
        chatBoxRef.current.scrollTop = chatBoxRef.current.scrollHeight;
    });

When the server fires the newMsg event on the socket, I want it to update the msgArr state with a new msg, but I see that the console.log('debug') statement runs more times than expected, and things don't work as I expect it to. I mean, it should only once right. But it doesn't, it runs multiple times, and at some point, the "times" even reaches 40. Being new to react, I don't understand why this is happening. So I would appreciate it if someone would shed some light on this problem, which has kept me awake for days now. I need help. Anyone?

It looks like you're trying to mutate state directly which you should never do . Try this instead:

import { io } from 'socket.io-client';

const socket = io(ServerEndURL);
export default function Chat (props) {
    const uuid = props.uuid;
    const classes = useStyles();
    const [open, setOpen] = useState(false);
    const [btnColor, setBtnColor] = useState('white');
    const [activeStatus, setActiveStatus] = useState('Offline');
    const [unreadMsgs, setUnreadMsgs] = useState(0);
    const [msgArr, setMsgArr] = useState([]);
    const chatBtnRef = useRef();
    const chatBoxRef = useRef();
    const msgInputRef = useRef();
    useEffect(() => {
        socket.emit('join', uuid);
        socket.emit('isActive', uuid);
        return () => {
            console.log('removed');
            socket.off('newMsg');
        }
    }, []);

    socket.on('newMsg', msg => {
        console.log('debug');
        
        // Note: This is a no-op if you don't need to extract info from `msg`
        let msgs = msgArr.map(msg => msg);

        // Update state directly with the new message
        setMsgArr([ ...msgs, { type: 'received', msg }]);

        if(!open) setUnreadMsgs(unreadMsgs + 1);
        chatBoxRef.current.scrollTop = chatBoxRef.current.scrollHeight;
    });

Towards the second part of your question, you tell React what you want to be rendered into the DOM, and then it's responsible for actually updating the DOM on your behalf. Because of this, it may re-render your component more than once if it needs to (usually, it's when state has been updated). In your case, you define an event handler for newMsg events that updates the state so when this event handler is fired React re-renders your component (which executes the console.log('debug') statement).

My assumption is that you keep subscribing to your newMsg event every time react renders your component. If your component re-renders 40 times, you will have 40 subscriptions to your 'newMsg' event and it will run your event callback 40 times. That said:

Define your 'newMsg' event listener in a 'useEffect' hook instead.

Either in your existing 'useEffect' as long as you're only proving an empty array ([]) as your second argument:

useEffect(() => {

  socket.on('newMsg', ...)

  socket.emit('join', uuid);
  socket.emit('isActive', uuid);
  return () => {
    console.log('removed');
    socket.off('newMsg');
  }
}, []);

or if your logic changes and you "monitor" changes in your original 'useEffect' you could simply place it in its own. (Example includes the message array)

const [msgArr, setMsgArr] = useState([]);
useEffect(() => {
  socket.on('newMsg', msg => {
    setMsgArr([ ...msgArr, { type: 'received', msg }]);
    //Alternative:
    //setMsgArr(msgArr.concat({ type: 'received', msg }));
  })
}, []);

The second argument in 'useEffect' simply tells react to re-run the effect if something in your second argument changes. However, you only want to subscribe once, thus providing an empty array so it will never see a change and re-run the effect.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM