React/Redux global state management based on Socket.IO responses on client side?

Viewed 1136

I have an web app with multiple features like private messaging, buying, offers etc. I want to make it to work real time so I decided to use socket.io. I use redux for global state management, but I don't know how can I combine this with socket.IO. This was my idea:

1.Creating a file for socket handling with with exported functions to App.js to create a socket connection, sending and listening different data.

2.Whenever I got something relevant for example a notification or a buying request I update my redux state.

3.Finally in my components I will use useEffect for those global redux states and if it changes I will rerender my component based on my changed state.

Is this a good approach? If not which is a proper way to globally mangage my components based on socket recieved informations?

1 Answers

In general, depending on your needs I see nothing wrong with this approach. I will provide one actionable example here. My example will assume TypeScript as it's easier to transform to JavaScript (in case you do not use TypeScript) than the other way around.

In relation to your 1st question I would suggest to establish and pass Websocket connection as a context as you use it everywhere in your application and create custom hook to use the connection anywhere:

import React, { createContext, FunctionComponent, ReactNode, useContext, useEffect, useMemo, useState } from 'react';
import io from 'socket.io-client';

export const WebsocketContext = createContext<SocketIOClient.Socket | null>(null);

const WebsocketProvider: FunctionComponent<{ children: ReactNode }> = ({ children }: { children: ReactNode }) => {
  const [connection, setConnection] = useState<SocketIOClient.Socket | null>(null);

  const options: SocketIOClient.ConnectOpts = useMemo(() => ({}), []);

  useEffect(() => {
    try {
      const socketConnection = io(process.env.BASE_URL || '127.0.0.1', options);
      setConnection(socketConnection);
    } catch (err) {
      console.log(err);
    }
  }, [options]);

  return <WebsocketContext.Provider value={connection}>{children}</WebsocketContext.Provider>;
};

export const useWebsocket = (): SocketIOClient.Socket | null => {
  const ctx = useContext(WebsocketContext);
  if (ctx === undefined) {
    throw new Error('useWebsocket can only be used inside WebsocketContext');
  }
  return ctx;
};

export default WebsocketProvider;

Above we create context which has type SocketIOClient.Socket and defaults to null, as when connection is not yet ready we must assign default value. Then we create Websocket provider as FunctionComponent which accepts children(s) and holds connection state with useState hook eventually returning provider with Websocket connection. I also mention SocketIOClient.ConnectOpts as depending on your needs you might want to provide connection options; either statically or dynamically when using the hook. Furthermore useEffect hook which will try to establish the connection or throw an error. The only dependency which will rerun this hook is connection options in case they will dynamically change.

Finally we have custom hook useWebsocket which we can import in any component and use inside our context provider. Simply wrap your root component (or any other hierarchy level) with context provider to provide the context like in the example below:

import React, { FunctionComponent } from 'react';
import { BrowserRouter as Router, Switch, Route, Redirect } from 'react-router-dom';
import { v4 as uuid } from 'uuid';
import routes from './App.routes';
import WebsocketProvider from './websocket.context';

const App: FunctionComponent = () => {
  return (
    <WebsocketProvider>
        <Router>
          <Switch>
            {routes.map((route) => (
              <Route key={uuid()} {...route} />
            ))}
          </Switch>
          <Redirect to='/' />
        </Router>
    </WebsocketProvider>
  );
};

export default App;

In relation to your 2nd question you can for example have ´useEffect´ hook to react when connection emits and update your Redux (or other global state management) store. Here I also use Elvis operator to check if the connection is not ready yet (if its not ready yet as null the useEffect hook will re-render on socket connection change when its ready):

import React, { FunctionComponent, useEffect, useState } from 'react';
import { useWebsocket } from './websocket.context';
const Foo: FunctionComponent = () => {
  const dispatch = useDispatch();
  const socket = useWebsocket();

  useEffect(() => {
    socket?.on('myEmitEvent', (data: myEmitData) => {
      dispatch(myStoreAction(data));
    });
    return () => {
      socket?.off('myEmitEvent');
    };
  }, [socket, dispatch]);

  return ...
};

export default Foo;

In relation to your 3rd question as you mention you can use useEffect hook or more simply useSelector hook from react-redux package which automatically captures your state changes triggering re-render on necessary elements.

In short, your idea hits the ballpark and I hope that with this brief actionable example you will be able to refine solution which works for you.

Related