Home > Enterprise >  How to prevent infinite re-rendering with useEffect() in React?
How to prevent infinite re-rendering with useEffect() in React?

Time:01-12

I am new for development.I have created a chat application using socket io.I have the useEffect change only when socket changes, but I have setMessage in the body of useEffect() meaning update the message and I put the socket as dependency.But this doesn't work and it re-runs infinitum.

import React from 'react';
import ScrollToBottom from 'react-scroll-to-bottom';
import { useState,useEffect } from 'react';

function Chat({socket,username,room}) {
    const [currentMessage, setCurrentMessage] = useState("");
    const [messageList, setMessageList] = useState([]);
    const sendMessage = async () => {
        if (currentMessage !== "") {
            const messageData = {
                room: room,
                author: username,
                message: currentMessage,
                time: new Date(Date.now()).getHours()
                      ':'  
                    new Date(Date.now()).getMinutes()
            };
            await socket.emit("send_message", messageData);
            sendMessage((list)=>[...list,messageData]);
            
        }
    };
    useEffect(() => {
        socket.on("receive_message", (data) => {
          setMessageList((list) => [...list, data]);
          setCurrentMessage("");
          
        });
      }, [socket]);
    return (
        <div className='chat-window'>
            <div className='chat-header'>
                <p>Live Chat</p>
            </div>
            <div className='chat-body'>
                <ScrollToBottom className='message-container'>
                {messageList.map((messageContent)=>{
                    return (
                    <div className='message' id={username===messageContent.author?"you":"other"}>
                       <h1>{messageContent.message}</h1>
                    </div>
                    );
                
                    })}
                </ScrollToBottom>

            </div>
            <div className='chat-footer'>
                <input type="text" value={currentMessage}placeholder='Hey..' onChange={(event) => {
                    setCurrentMessage(event.target.value);
                }} 
                onKeyPress={(event)=>{
                    event.key==='Enter'&&sendMessage();
                }}/>
                <button onClick={sendMessage}>&#9658;</button>
            </div>
        </div>
    );
}
export default Chat;

How to stop rendering the useEffect function?

CodePudding user response:

Why you are passing receive_message socket listener to the useEffect? did you tried without using useEffect

socket.on("receive_message", (data) => {
      setMessageList((list) => [...list, data]);
      setCurrentMessage("");
});

CodePudding user response:

There is a recursion happening in sendMessage Function, which is causing an infinite loop.

Removing the below line may fix the issue

sendMessage((list)=>[...list,messageData]);

Also, don't forget to remove the socket from the dependency array.

CodePudding user response:

Actually i have ran into this problem recently, however there's some options that you can try:

  1. Add listener in the file where socket is initialized

Instead of passing socket into child component, you should only pass the data to the child component

example index.js:

const [messageList, setMessageList] = useState([]);

// Do not use any deps here
useEffect(() => {
   let socket = init() // or something

   socket.on("receive_message", (data) => {
      setMessageList((list) => [...list, data]);
      setCurrentMessage("");
   });
}, [])

return (
    <ChildComponent messageList={messageList} />
)

With this solution, the useEffect should only render once, however if you wanted to pass down the socket, you can use useRef instead of useEffect since it does not re-render and works just like a normal variable.

  1. Use Custom Window Dispatch Event Hook

I understand that you are trying to have clean code here, so maybe the alternatives is using EventTarget.dispatchEvent() browser API, this method is the one i usually going with.

With this you just need to create custom hooks to emit/listen the data, here's mine:

const [data, setData] = useState<T | null>(null);

useEffect(() => {
    const  handler = ((event: CustomEvent<CustomEventDetail>) => {
        if (event.detail.emitId === id) return;
        setData(event.detail.data);

    }) as  EventListener;

    window.addEventListener(eventName, handler);

    return () => {
        window.removeEventListener(eventName, handler);
    };
}, [eventName, id]);
  
const  emit = (data: T) => {
    const  customEvent = new  CustomEvent<CustomEventDetail>(eventName, {
        detail: {
            emitId:  id,
            data:  data,
        },
    });

    setData(data);
    window.dispatchEvent(customEvent);
};
  1. Create Validator in the useEffect

Maybe the last thing you wanted to try is using a validator

useEffect(() => {
    socket.on("receive_message", (data) => {
        if (currentMessage !== data.message) {
            setMessageList((list) => [...list, data]);
            setCurrentMessage("");
        }
    });
}, [socket]);

however, i don't know if it would works on don`t


Also for notes, don't forget to use return on the useEffect, so the function inside it wouldn't called multiple times because of component re-rendering, such as

const handler = () =>  {}
socket.on("receive_message", handler);

return () => {
    socket.off("receive_message", handler);
}

Hope my comments give you a solution, let me know if anything above is works for you!

CodePudding user response:

I think you don't have to pass socket as a dependency.

Test this snippet and let me know if it's working.

useEffect(() => {
  socket.on("receive_message", (data) => {
     setMessageList((list) => [...list, data]);
     setCurrentMessage("");        
  });
  
  return () => {
     socket.off('receive_message');
  }
}, []);
  • Related