robtaussig / react-use-websocket

React Hook for WebSocket communication
MIT License
1.6k stars 136 forks source link

Component reloads whenever on every event of websocket #178

Closed googlina closed 1 year ago

googlina commented 1 year ago

Hi,

First of all thanks for this package.

Im using this package to achieve realtime chat functionality within my app.

Im creating a websocket connection in app.js and storing events using redux store so i can access connection across app. Connection is working fine but whenever ws event is triggered in children component App re-render. Im sharing my codes below.

App.js

const wsUrl = ws://localhost:8000/ws/connect/?token=${accessToken}; const { sendMessage, lastMessage, readyState } = useWebSocket(wsUrl, { onOpen: () => console.log("opened"), share: true, shouldReconnect: (closeEvent) => true, });

useEffect(() => { // console.log("readyState changes", readyState); dispatch(setReadyState(readyState)); dispatch(setLastMessage(lastMessage)); dispatch(setSendMessage(sendMessage)); }, [readyState]);

UserChat.js

const { readyState, sendMessage, lastMessage } = store.getState().chatSlice;

const handleSendMsg = (values, resetForm) => { if (values.msg.trim().length) { const dataToPush = { id: 1 + Math.random(), created_by__username: loggenInuser.username, message: values.msg, created_at: new Date(), }; setChatMessages([...chatMessages, dataToPush]); resetForm(); if (readyState === 1) { const data = { message: values.msg, sender_name: ${loggenInuser.first_name}${loggenInuser.last_name}, send_to: selectedUser.chating_with.username, event: "message", rg: selectedUser.id, }; sendMessage(JSON.stringify(data)); } } } };

Whenever this sendmessage is called or message is received, page reloads.

robtaussig commented 1 year ago

Whenever this sendmessage is called or message is received, page reloads.

By this, do you mean what you described earlier as App re-rendering? If the page is reloading (what would happen if you execute location.reload()), then something is definitely wrong!

If you just mean that your App component is re-rendering (as in the component itself is being called again) when you call sendMessage, then I don't have enough information in your example to conclude why (where does setChatMessages come from? How are you accessing dispatch in your App component? If setChatMessages is updating redux, do you have a selector in your App component that is accessing your redux state?)

That said, whenever you receive a message, then App should rerender, and that is by design. If it didn't, then lastMessage would never update.

Finally, for some unsolicited advice:

1) The following is probably not doing what you think:

useEffect(() => {
  // console.log("readyState changes", readyState);
  dispatch(setReadyState(readyState));
  dispatch(setLastMessage(lastMessage));
  dispatch(setSendMessage(sendMessage));
}, [readyState]);

That useEffect is only being called when readyState changes, but not when lastMessage changes. So even when you are receiving messages from your server, the lastMessage in redux will never update (it will be whatever the lastMessage was the last time the readyState changed).

2) Does your WebSocket server respond whenever it receives a message? If so, that would explain why your App component updates whenever you call sendMessage.

3) Storing sendMessage inside of redux is probably not correct. It's been a while since I used redux, but I remember it being very opinionated about only storing primitives in your redux store (i.e., not functions). Since sendMessage is stable, then it shouldn't cause any weird behavior in redux, but who knows...

If the point of this pattern is that you want to access sendMessage throughout your app, then I'd recommend one of two approaches: a) Use React's Context API to pass sendMessage from your App component to the rest of your app. b) Since you are already using useWebSocket using the share: true option, then I would simply include the hook wherever you want to access readyState, lastMessage, or sendMessage. No matter how many times this hook is called, only one WebSocket will be open at a time. It's the point of that option! If are worried about ergonomics/consistency, you can create a custom hook wrapper:

const wsUrl = ws://localhost:8000/ws/connect/?token=${accessToken};

  export const useMyWebSocket = () => {
    return useWebSocket(wsUrl, {
      onOpen: () => console.log("opened"),
      share: true,
      shouldReconnect: (closeEvent) => true,
    });
  };

Now whenever you want to access the WebSocket and its data, just import and call it in all of your components:

//In UserChat.js

  const UserChat = () => {
    const { sendMessage, lastMessage, readyState } = useMyWebSocket();
  };

Hope this helps!

googlina commented 1 year ago

I have to use useMemo to resolve this issue.

RAFA3L commented 1 year ago

Yes, wherever the API method sendMessage are referenced create new connections, even wrapping useWebSocket in a custom hook.

My solution was creating a global state for the message to send, then in a custom wrapper hook for useWebSocket I added an effect with the message dependency, simple but it was tricky

useEffect(() => {
if (messageToSend) { sendMessage(JSON.stringify(messageToSend)); setMessageToSend(null); } }, [sendMessage, setMessageToSend, messageToSend]);

abiatarnt commented 11 months ago

As a future reference for others having the same issue, a longer explanation for the solution proposed by @googlina to use the useMemo hook, in my own words:

The reload/re-render occurs because of the plot options object being re-instanced every time the plot's parent component triggers a new render, for example when one or more props change during runtime. The re-instancing of the options object happens independently of the changed prop being used to generate the object or not. In my case, I get the scales titles and the plot legend from a single prop from the parent, which in turn has a total of 4 props.

For making the plot options object only dependent of only the single needed prop, I used the useMemo hook like this:

import { useMemo, useRef } from 'react';

... 

export default function MyChart (prop1, prop2, prop3, plotOptions) {
const chartRef = useRef<ChartJS>(null);

const buildMemoizedPlotOptions = () => {
  return {
      (options object dependent of single "plotOptions" prop)
  };
};
const memoizedPlotOptions = useMemo(buildMemoizedPlotOptions, [plotOptions]);

const plotData = {
    datasets: [
        { (plot data, from another prop) }
    ]
};

...

return <Line ref={chartRef} options={memoizedPlotOptions} data={plotData} />
mnapoli commented 9 months ago

For those interested, the component will always re-render on every new socket message. Even if you don't want to.

My solution was to drop this package and implement websocket manually, store the data I'm interested in in a variable outside of the component, and re-render my component every 200ms.

Example:


let ws: WebSocket;
let data = [];

function connectWs() {
    // Only connect once
    if (ws) return;
    ws = new WebSocket(websocketUrl);
    ws.onopen = () => {
        console.log('connected');
    };
    ws.onmessage = (event) => {
        if (! event.data) return;
        const message: SocketMessage = JSON.parse(event.data);
        data.push(message);
    };
    ws.onclose = () => {
        console.log('closed');
    };
    ws.onerror = (event) => {
        console.log('error', event);
    };
}

export function Visualizer() {

    useEffect(() => {
        connectWs();
        // Refresh every X ms
        const interval = setInterval(() => {
            setTimeStart(now() - timeSize);
            setTimeEnd(now())
        }, 200);
        return () => {
            clearInterval(interval);
        };
    });

    ...
    // use data in my component's template
RAFA3L commented 8 months ago

I found another solution putting sendMessage into a Zustand store

import useWebSocket, { ReadyState } from 'react-use-websocket';
import useStore from '../store/useStore';

export const useSocket = () => {
  const setSendMessage = useStore((state) => state.setSendMessage);
  const setIsConnected = useStore((state) => state.setIsConnected);
  const connect = useStore((state) => state.connect);

  const onOpen = () => {
    setIsConnected(true);
    setSendMessage(sendMessage);
  };

  const onClose = () => {
    setIsConnected(false);
  };

  const onMessage = (message: MessageEvent) => {
    console.log(message.data);
  };

  const { sendMessage, readyState } = useWebSocket(
    'ws://localhost:8080',
    {
      onOpen: onOpen,
      onClose: onClose,
      onMessage: onMessage,
    },
    connect
  );

  // Connection status.
  const connectionStatus = {
    [ReadyState.CONNECTING]: 'Connecting',
    [ReadyState.OPEN]: 'Open',
    [ReadyState.CLOSING]: 'Closing',
    [ReadyState.CLOSED]: 'Closed',
    [ReadyState.UNINSTANTIATED]: 'Uninstantiated',
  }[readyState];

  return { connectionStatus };
};

Then you can use it from another components without re-renders

import { Button } from '@chakra-ui/react';
import useStore from '../store/useStore';

function Sender() {
  const sendMessage = useStore((state) => state.sendMessage);
  const handleSend = () => sendMessage('Message');

  return (
    <Button data-testid={"data-send"} onClick={handleSend}>Send Message</Button>
  )
}

export default Sender
mberdyshev commented 2 weeks ago

For the issue cross-reference: #123