Unlocking how Socket.io can boost your React app's real-time capabilities

gboladetrue

Ibukunoluwa Popoola

Posted on May 19, 2024

Unlocking how Socket.io can boost your React app's real-time capabilities

As front-end developers, we're familiar with the HTTP protocol, which is the foundation of the web. However, when it comes to real-time communication, HTTP falls short. That's where Socket.io comes in – a JavaScript library using the WebSocket protocol that enables real-time, bi-directional communication between the client and server. In this article, we'll explore the differences between HTTP and WebSocket, and how Socket.io brings new functionalities to the table.

HTTP vs. Websocket: A Brief Overview

HTTP (Hypertext Transfer Protocol) is a request-response protocol, where the client sends a request to the server, and the server responds with a response. This protocol is designed for request-response communication, which is perfect for most web applications. However, when it comes to real-time communication, HTTP is not the best choice.

The WebSocket Protocol is a bi-directional communication protocol that enables real-time, low-latency communication between a client (usually a web browser) and a server over the web. Built on top of the Transmission Control Protocol (TCP), WebSocket inherits TCP's reliability and ordering guarantees, ensuring that data is delivered in the correct order. However, unlike TCP, which is designed for general-purpose networking, WebSocket is optimized for real-time communication, making it an ideal choice for applications that require instantaneous data exchange, such as live updates, gaming, and live support chat.

Socket.io: The Real-Time Solution

Socket.io enables server-initiated communication, allowing the server to proactively push updates to connected clients, eliminating the need for clients to continuously request updates. This is a classic example of the publish-subscribe pattern, a messaging paradigm where a single publisher (the server) broadcasts messages to multiple subscribers (the clients) without them having to explicitly request the information. This one-to-many communication model enables efficient and scalable bi-directional communication, where a single update from the server can be simultaneously received by multiple clients, making it an ideal approach for real-time applications that require instantaneous data dissemination.

A Real-World Example: Integrating Socket.io in a React Typescript Project

I was tasked with integrating a critical feature into an administrative web application, which would subscribe to a server that publishes watchlist events triggered by AI-powered smart cameras detecting vehicles for security purposes. Upon receiving these events, the web app would instantly notify all authorized administrators, ensuring timely awareness and response. Furthermore, administrators could respond to the server, updating the system on actions taken regarding the identified vehicle. This requirement presented a perfect opportunity to leverage bi-directional communication using Socket.io, enabling seamless, real-time exchange of information between the client and server.

In this context, we'll focus exclusively on the frontend implementation. To facilitate this functionality, a WebSocket-enabled server is a prerequisite. In a future discussion, we can explore how to set up a simple Node.js server with WebSocket capabilities.

The first step was to install the necessary dependencies:

yarn add socket.io-client
Enter fullscreen mode Exit fullscreen mode

Then I set up global and scalable functionality for how the application would interact with the Socket.io logic using standard React practice, custom hooks, contexts, and following the guidelines in the socket-io-client documentation.

// useSocket.ts
import { useEffect, useRef } from 'react';
import io, { ManagerOptions, Socket, SocketOptions } from 'socket.io-client';

export const useSocket = (url: string, options?: Partial<ManagerOptions & SocketOptions> | undefined): Socket => {
    const { current: socket } = useRef(io(url, options));

    useEffect(() => {
        return () => {
            if (socket) {
                socket.close();
            }
        };
    }, [socket]);

    return socket;
};

/// IWatchlist.ts
export interface IWatchListSocketResponse {
    id: string;
    // any other props you wish to add
    [key: string]: any;
}
export interface IWatchListActionItem {
    id: string;
    // any other props you wish to add
    [key: string]: any;
}

// watchlistContext.tsx
/* eslint-disable no-console*/
import { ReactNode, createContext, useCallback, useContext, useEffect, useReducer, useState } from 'react';
import { Socket } from 'socket.io-client';
import { useSocket } from 'hooks/generic/useSocket';
import { IWatchListActionItem, IWatchListSocketResponse } from 'interfaces/IWatchlist';
import { WATCHLIST_WS_URL } from 'utils/constants';

export interface IWatchListSocketContextState {
    socket: Socket | undefined;
    watchListInfo: IWatchListSocketResponse[];
    watchListActionItem?: IWatchListActionItem;
}

export type WatchListSocketContextAction = 'update_socket' | 'receive_watchList' | 'attend_to_watchList_item';
export type WatchListSocketContextPayload = IWatchListSocketResponse[] | IWatchListActionItem | Socket;
export type SocketConnectionStatus = 'connecting' | 'connected' | 're-connecting' | 'closed';

export interface IWatchListSocketContextAction {
    type: WatchListSocketContextAction;
    payload: WatchListSocketContextPayload;
}

export interface IWatchListSocketContextReturnProps {
    socketState: IWatchListSocketContextState;
    socketDispatch: React.Dispatch<IWatchListSocketContextAction>;
    connectionStatus: SocketConnectionStatus;
    handleWatchListItem: () => void;
}

const defaultWatchListSocketContextState: IWatchListSocketContextState = {
    socket: undefined,
    watchListInfo: [],
    watchListActionItem: {} as IWatchListActionItem
};

export const WatchListSocketContext = createContext<IWatchListSocketContextReturnProps>({
    socketState: defaultWatchListSocketContextState,
    socketDispatch: () => {},
    connectionStatus: 'closed',
    handleWatchListItem: () => {}
});

export const useWatchListSocketContext = () => useContext(WatchListSocketContext);

const SocketReducer = (state: IWatchListSocketContextState, action: IWatchListSocketContextAction) => {
    console.log('Message received - Action: ' + action.type + ' - Payload: ', action.payload);

    switch (action.type) {
        case 'update_socket':
            return { ...state, socket: action.payload as Socket };
        case 'receive_watchList':
            return { ...state, watchListInfo: action.payload as IWatchListSocketResponse[] };
        case 'attend_to_watchList_item':
            return { ...state, watchListActionItem: action.payload as IWatchListActionItem };
        default:
            return state;
    }
};

export const WatchListSocketContextProvider = ({ children }: { children: ReactNode }) => {
    const [socketState, socketDispatch] = useReducer(SocketReducer, defaultWatchListSocketContextState);
    const [connectionStatus, setConnectionStatus] = useState<SocketConnectionStatus>('closed');

    const socket = useSocket(WATCHLIST_WS_URL, {
        reconnectionAttempts: 5,
        reconnectionDelay: 1000,
        autoConnect: false
    });

    const startListeners = useCallback(() => {
        /** Messages */
        socket.on('connecting', () => {
            console.info('Connecting to WatchList service');
            setConnectionStatus('connecting');
        });

        /** Messages */
        socket.on('watchList_received', (watchList: any[]) => {
            console.info('WatchList info received');
            socketDispatch({ type: 'receive_watchList', payload: watchList });
            setConnectionStatus('connected');
        });

        /** Connection / reconnection listeners */
        socket.on('reconnect', attempt => {
            console.info('Reconnected on attempt: ' + attempt);
            setConnectionStatus('re-connecting');
            console.info('Retrying to get watchList ...');

            socket.emit('retry_watchList', async (watchList: any[]) => {
                console.info('User handshake callback message received');
                socketDispatch({ type: 'receive_watchList', payload: watchList });
            });
        });

        socket.on('reconnect_attempt', attempt => {
            console.info('Reconnection Attempt: ' + attempt);
        });

        socket.on('reconnect_error', error => {
            console.info('Reconnection error: ' + error);
        });

        socket.on('reconnect_failed', () => {
            console.info('Reconnection failure.');
            alert(
                'We are unable to connect you to the watchList service.  Please make sure your internet connection is stable and try again.'
            );
            setConnectionStatus('closed');
        });
    }, [socket]);

    useEffect(() => {
        socket.connect();
        socketDispatch({ type: 'update_socket', payload: socket });
        startListeners();
        // eslint-disable-next-line react-hooks/exhaustive-deps
    }, [socket]);

    const handleWatchListItem = () => {
        console.info('Handling watch list item ...');

        socket.emit('attend_to_watchList_item', async (watchListItem: IWatchListActionItem) => {
            console.info('User handshake callback message received');
            socketDispatch({ type: 'attend_to_watchList_item', payload: watchListItem });
        });
    };

    return (
        <WatchListSocketContext.Provider value={{ socketState, socketDispatch, connectionStatus, handleWatchListItem }}>
            {children}
        </WatchListSocketContext.Provider>
    );
};

// App.tsx (or your apps entry file)
import ReactDOM from 'react-dom/client';
import VehicleAdminApp from './VehicleAdminApp';
import WatchListSocketContextProvider from 'contexts/watchlistSocket';
import reportWebVitals from './reportWebVitals';

const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement);
root.render(
    <WatchListSocketContextProvider>
        <VehicleAdminApp />
    </WatchListSocketContextProvider>
);

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
Enter fullscreen mode Exit fullscreen mode

Now, an example of how the socket logic can be used in a component:

// VehicleAdminApp.tsx
import { useWatchListSocketContext } from 'contexts/watchlistSocket';

const VehicleAdminApp = () => {
    const { socketState } = useWatchListSocketContext();
    const { socket, watchListInfo } = socketState;

    return (
        <div>
            <h2>Socket IO Information:</h2>
            <p>
                Your socket ID: <strong>{socket?.id}</strong>
                <br />
                Watchlist items: <strong>{watchListInfo.length}</strong>
                <br />
                <br />
            </p>
        </div>
    );
};

export default VehicleAdminApp;
Enter fullscreen mode Exit fullscreen mode

The provided code establishes a Socket.IO context that manages the socket connection, watch list information, and action items. The WatchListSocketContext is created using the createContext hook, which allows components to access the socket state, dispatch actions, connection status, and a function to handle watch list items. The useWatchListSocketContext hook is also defined for easy access to the context in other components.

The WatchListSocketContextProvider is the heart of the implementation, where the socket connection is established using the useSocket hook. The provider sets up listeners for various socket events, such as connecting, receiving watch list information, and reconnecting. It also defines the startListeners function, which sets up these listeners, and the handleWatchListItem function, which emits an event to attend to a watch list item. The provider returns a context provider that wraps the application, making the socket context available to all components. This implementation provided a solid foundation for building the watchlist functionality in my application.

Conclusion

Socket.io is a powerful library that enables real-time communication between the client and server. By understanding the differences between HTTP and TCP, and how Socket.io uses the WebSocket protocol, you can unlock the power of real-time communication in your React applications.

Implementing Socket.IO in a React application requires careful planning and a thorough understanding of its underlying principles. However, the rewards of real-time communication make it a valuable investment for any React developer seeking to elevate their applications. This article provides a step-by-step guide on how to correctly integrate Socket.IO in a React/Typescript application, using a watchlist feature as a practical example, to help you unlock the full potential of real-time functionality in your projects.

Useful links:

  1. https://socket.io/how-to/use-with-react
  2. https://www.youtube.com/watch?v=-aTWWl4klYE
💖 💪 🙅 🚩
gboladetrue
Ibukunoluwa Popoola

Posted on May 19, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related