DEV Community

Amalia Hajarani
Amalia Hajarani

Posted on

How to: Integrating Socket.IO with Spring Boot and React (React Implementation)

Prerequisite

  1. Node version of 20.10.0

Initializing React-Vite project

  1. Open a directory dedicated to save this project.
  2. Open command prompt from that directory. Run this command:

    npm create vite@latest .
    
  3. Choose React by moving your arrow key in keyboard. Then hit enter.
    Image description

  4. Choose JavaScript for a variant. Then hit enter.
    Image description

  5. Now run npm install.

    Installing dependencies

    I have some preferred dependency to install to make development easier like css framework and such. Still in the command prompt directed to your project directory, run these commands:

    Installing Material UI

npm install @mui/material @emotion/react @emotion/styled
Enter fullscreen mode Exit fullscreen mode

Installing Axios

npm install axios
Enter fullscreen mode Exit fullscreen mode

Installing Socket.io-client

We are going to use specific version.

npm install socket.io-client@2.1.1
Enter fullscreen mode Exit fullscreen mode

Installing Formik and Yup

npm install formik yup
Enter fullscreen mode Exit fullscreen mode

Creating project skeleton

This is the overview of my project skeleton inside src directory:

.
└── src/
    ├── assets/
    ├── components/
    ├── hooks/
    ├── service/
    ├── views/
    ├── app.jsx
    ├── index.css
    ├── main.jsx
    └── theme.js
Enter fullscreen mode Exit fullscreen mode

Defining theme

This step is actually not necessary, but if you want to customize some things like typography and stuffs, you might want to follow this step. As you can see in my src skeleton. I have a file called theme.js. If you already create that, this is my content of the file:

import { createTheme, responsiveFontSizes } from '@mui/material/styles';

let theme = createTheme({
  palette: {
    primary: {
      main: "#003c6d",
      contrastText: "#fff"
    },
    secondary: {
      main: "#B0B0B0",
    }
  },
  typography: {
    fontFamily: [
      'Roboto', 
      'sans-serif'
    ].join(',')
  },
  components: {
    MuiTextField: {
      styleOverrides: {
        root: {
          '& .MuiOutlinedInput-root': {
              borderRadius: 8,
          },
        }
      }
    },
    MuiButton: {
      styleOverrides: {
        root: {
          borderRadius: 8,
          paddingTop: 12,
          paddingBottom: 12
        }
      }
    }
  }
});

theme = responsiveFontSizes(theme);

export default theme;
Enter fullscreen mode Exit fullscreen mode

I'm using font from Google font, hence I need to update the index.css like this (and this is also my final index.css):

@import url('https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap');

:root {
  font-family: 'Roboto', sans-serif;
}

::-webkit-scrollbar {
  display: none;
}
Enter fullscreen mode Exit fullscreen mode

To apply the theme that we've already created, open App.jsx. This is how my final App.jsx lookslike:

import { ThemeProvider } from '@mui/material';
import { useState } from 'react';
import theme from './theme';
import ChatRoom from './views/ChatRoom';
import Login from './views/Login/indes';

function App() {
  const [username, setUsername] = useState("");
  const [room, setRoom] = useState("");
  const [isLoggedIn, setIsLoggedIn] = useState(false);

  return (
    <ThemeProvider theme={theme}>
      {
        !isLoggedIn ? (
          <Login
            room={room}
            setRoom={setRoom}
            username={username}
            setUsername={setUsername}
            setIsLoggedin={setIsLoggedIn}
          />
        ) : (
          <ChatRoom 
            username={username}
            room={room}
          />
        )
      }
    </ThemeProvider>
  )
}

export default App

Enter fullscreen mode Exit fullscreen mode

If you see the coed above carefully, the theme.js is used along with <ThemeProvider theme={theme}> </ThemeProvider> tag.

App.jsx explanation

I'm not using routing in this project like the tutorial I've been mentioned in the first post. So, the logic is actually pretty simple. It uses an approach like lifting state up where Login return the most updated state of username, room and isLoggedIn to the parent (App.jsx) to be then consumed by ChatRoom component.

Configuring .env file

On your project root directory, create a new file called .env. And fill it with your server-side address which by the way is your IPv4 address and the port where socket is running. Mine is at 8086.

VITE_API_BASE_URL=http://{YOUR_IPv4_ADDRESS}:8080
VITE_SOCKET_BASE_URL=http://{YOUR_IPv4_ADDRESS}:{SOCKET_PORT}
Enter fullscreen mode Exit fullscreen mode

Defining service

To working with the messages, we will create a service to communicate with our server side endpoint.

  1. In service directory, create a new directory called config.
  2. Create a new file called axiosConfig.js at config.

    import axios from "axios";
    
    const API_BASE_URL = import.meta.env.VITE_API_BASE_URL;
    
    const api = axios.create({
        baseURL: API_BASE_URL,
    });
    
    export default api;
    
  3. Back to service directory, create a new directory called socket.

  4. Create a new file called index.js at socket directory.

    import api from "../config/axiosConfig";
    
    export const getSocketResponse = async (room) => {
      try {
        const res = await api.get('/message/' + room);
        return res.data;
      } catch (error) {
        console.log(error);
      }
    }
    

Defining custom hook

In hooks directory, create a new file called useSocket.js.

import { useCallback, useEffect, useState } from "react";
import * as io from 'socket.io-client'

export const useSocket = (room, username) => {
    const [socket, setSocket] = useState();
    const [socketResponse, setSocketResponse] = useState({
        room: "",
        message: "",
        username: "",
        messageType:"",
        createdAt: ""
    });
    const [isConnected, setConnected] = useState(false);

    const sendData = useCallback((payload) => {
        socket.emit("send_message", {
            room: room,
            message: payload.message,
            username: username,
            messageType: "CLIENT"
        });
    }, [socket, room]);

    useEffect(() => {
        const socketBaseUrl = import.meta.env.VITE_SOCKET_BASE_URL;
        const s = io(socketBaseUrl, {
            query: `username=${username}&room=${room}`
        });
        setSocket(s);
        s.on("connect", () => {
            setConnected(true);
        });
        s.on("connect_error", (error) => {
            console.error("SOCKET CONNECTION ERROR", error);
        });
        s.on("read_message", (res) => {
            setSocketResponse({
                room: res.room,
                message: res.message,
                username: res.username,
                messageType: res.messageType,
                createdAt: res.createdAt
            });
        });

        return () => {
            s.disconnect();
        };
    }, [room, username]);

    return { isConnected, socketResponse, sendData };
}
Enter fullscreen mode Exit fullscreen mode

Creating Views

Creating Login view

  1. Open views directory, create a new directory called Login.
  2. Inside Login directory, create a new file called index.jsx. Here, we're using Formik and Yup to validate login form, make sure all field is filled before the user allowed to enter a chat room.

    import { Button, Container, Grid, TextField, Typography } from '@mui/material';
    import { useFormik } from 'formik';
    import React from 'react';
    import * as Yup from 'yup';
    
    const initialValues = {
      username: "",
      room: ""
    };
    
    const validationSchema = Yup.object().shape({
      username: Yup.string().required(),
      room: Yup.string().required()
    });
    
    function Login({ setRoom, setUsername, setIsLoggedin }) {
    
      const onLogin = () => {
        setUsername(formik.values.username);
        setRoom(formik.values.room);
        setIsLoggedin(true);
      };
    
      const formik = useFormik({
        initialValues: initialValues,
        validationSchema: validationSchema,
        onSubmit: () => {
          onLogin();
        },
      });
    
      return (
        <Container>
            <Grid 
              container 
              gap={5}
              flexDirection={'column'} 
              alignItems={"center"} 
              justifyContent={"center"} 
              height={'97vh'}
            >
              <Grid item sx={{ width: '60%' }}>
                <Typography variant='h3' fontWeight={"bold"} color={"primary"}>Hello!</Typography>
                <Typography color={"secondary"}>Login with your username</Typography>
              </Grid>
              <Grid item sx={{ width: '60%' }}>
                <TextField 
                  id="outlined-basic"
                  name="username" 
                  label="Username" 
                  variant="outlined"
                  value={formik.values.username}
                  onChange={formik.handleChange}
                  onBlur={formik.handleBlur}
                  fullWidth
                  error={formik.touched.username && formik.errors.username}
                  helperText={formik.touched.username && formik.errors.username && "Username cannot be empty"}
                />
              </Grid>
              <Grid item sx={{ width: '60%' }}>
                <TextField 
                  id="outlined-basic" 
                  name="room" 
                  label="Room" 
                  variant="outlined"
                  value={formik.values.room}
                  onChange={formik.handleChange}
                  onBlur={formik.handleBlur}
                  fullWidth
                  error={formik.touched.room && formik.errors.room}
                  helperText={formik.touched.room && formik.errors.room && "Room cannot be empty"}
                />
              </Grid>
              <Grid item sx={{ width: '60%' }}>
                <Button
                  fullWidth
                  variant="contained"
                  type='submit'
                  onClick={formik.handleSubmit}
                >
                  Login
                </Button>
              </Grid>
            </Grid>
        </Container>
      )
    };
    
    export default Login;
    

Creating ChatRoom view

  1. Open views directory, create a new directory called ChatRoom.
  2. Inside ChatRoom directory, create a new file called index.jsx. Below is the content of this view, but mind that we haven't create ChatBubble component.

    import { Box, Button, Container, Grid, TextField, Typography } from '@mui/material';
    import React, { useEffect, useState } from 'react';
    import ChatBubble from '../../components/ChatBubble';
    import { useSocket } from '../../hooks/useSocket';
    import { getSocketResponse } from '../../service/socket';
    import { connect } from 'formik';
    
    function ChatRoom({ username, room }) {
      const { isConnected, socketResponse, sendData } = useSocket(room, username);
      const [messageInput, setMessageInput] = useState("");
      const [messageList, setMessageList] = useState([]);
    
      const addMessageToList = (val) => {
        if (val.room === "") return;
        setMessageList([...messageList]);
        fetchMessage();
      }
    
      const sendMessage = (e) => {
        e.preventDefault();
        if (messageInput !== "") {
          sendData({
            message: messageInput
          });
          addMessageToList({
            message: messageInput,
            username: username,
            createdAt: new Date(),
            messageType: "CLIENT"
          });
          setMessageInput("");
        }
      }
    
      const fetchMessage = () => {
        getSocketResponse(room)
                .then((res) => {
                    setMessageList([...res]);
                }).catch((err) => {
                    console.log(err);
                });
      }
    
      useEffect(() => {
        fetchMessage();
      }, []);
    
      useEffect(() => {
        addMessageToList(socketResponse);
      }, [socketResponse]);
    
      return (
        <Container>
          <Grid
            container 
            gap={3}
            flexDirection={'column'} 
            alignItems={"center"} 
            justifyContent={"center"} 
            height={'97vh'}
          >
            <Grid item sx={{ width: '60%' }}>
              <Typography variant='h5'>
                Welcome to room <b>{room}</b>, {username}.
              </Typography>
            </Grid>
            <Grid item sx={{ width: '60%', bgcolor: '#ccd8e2', paddingX: '2rem', borderRadius: 6 }}>
              <Box 
                className="chat-box"
                sx={{ 
                  width: '100%',
                  paddingY: '2rem',
                  borderRadius: 4,
                  height: '60vh',
                  overflow: 'auto'
                }}
              >
                {
                  messageList.map((message) => {
                    if (message.messageType === 'CLIENT') {
                      console.log(message);
                      return (
                        <ChatBubble
                          key={message.id} 
                          isSender={message.username === username}
                          username={message.username}
                          message={message.message}
                          time={"12:12"}
                        />
                      )
                    } 
                  })
                }
              </Box>
              <Grid 
                container 
                alignItems={"center"}
                width={"100%"} 
                sx={{
                  paddingY: '0.5rem',
                  borderTop: '2px solid #99b1c5',
                }}
              >
                <Grid item xs={11}>
                  <TextField 
                    variant="standard"
                    placeholder='Type your message'
                    value={messageInput}
                    onChange={(e) => setMessageInput(e.target.value)}
                    fullWidth
                    InputProps={{
                      disableUnderline: true,
                      sx: {
                        paddingX: '0.5rem'
                      }
                    }}
                  />
                </Grid>
                <Grid item xs={1}>
                  <Button
                    onClick={(e) => sendMessage(e)}
                  >
                    Send
                  </Button>
                </Grid>
              </Grid>
            </Grid>
          </Grid>
        </Container>
      )
    }
    
    export default ChatRoom;
    

Creating ChatBubbleComponent

Moving to components directory, create a new directory called ChatBubble. Inside the newly created directory, create a new file called index.jsx.

import { Avatar, Box, Grid, Typography } from '@mui/material'
import React from 'react'

function ChatBubble({ isSender, username, message="" }) {
  const avatar = "https://random.imagecdn.app/500/150";
  const date = new Date();
  const time = date.getHours() + ':' + date.getMinutes();
  return (
    <Box>
      <Grid 
        container 
        gap={2} 
        flexDirection={isSender ? "row-reverse" : "row"}
        sx={{
          width: '100%',
          display: 'flex',
          justifyContent: isSender? 'end' : "start"
        }} 
      >
        <Grid item>
          <Avatar src={avatar} />
        </Grid>
        <Grid item sx={{ textAlign: isSender ? 'right' : 'left' }}>
          <Box>
            <Typography fontSize={14}> {username} </Typography>
            <Box 
              sx={{ 
                marginBottom: '0.5rem',
                paddingRight: isSender ? '0.5rem' : '2rem',
                paddingLeft: isSender ? '2rem' : '0.5rem',
                paddingY: '0.25rem',
                color: isSender ? '#e6ecf0' : '#001e37',
                bgcolor: isSender ? '#001e37' : '#e6ecf0',
                borderRadius: '8px'
              }}>
              <Typography> {message} </Typography>
              <Typography fontSize={10}> {time} </Typography>
            </Box>
          </Box>
        </Grid>
      </Grid>
    </Box>
  )
}

export default ChatBubble;
Enter fullscreen mode Exit fullscreen mode

Running the application and demo.

  1. Make sure server side service is running.
  2. Open command prompt from project root directory and run:

    npm run dev
    
  3. You will see this log if it's running correctly:
    Image description

  4. Now open two windows of your selected browser. Go to the address given in command prompt.
    Image description

  5. Enter different usernames and same room at both windows. Then login.
    Image description

  6. Try to chat from one account and see what happened:
    Image description
    Image description

Top comments (0)