How to: Integrating Socket.IO with Spring Boot and React (React Implementation)
Tags: react, beginners, tutorial, socketio
Published: 2023-12-06 03:28:20

## 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/i4d7jde0lqzr0z9xqfh7.png)
4. Choose `JavaScript` for a variant. Then hit `enter`.
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tpscrsps1wofr9rubzz0.png)
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
```
### Installing Axios
```
npm install axios
```
### Installing Socket.io-client
We are going to use specific version.
```
npm install socket.io-client@2.1.1
```
### Installing Formik and Yup
```
npm install formik yup
```
## 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
```

## 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;
```
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;
}
```
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

```
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}
```
## 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 };
}
```

## 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;
```

## 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fr2gj9g1suvphcz1ku98.png)
4. Now open two windows of your selected browser. Go to the address given in command prompt.
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2twtjwh7uni6dk1n2zdz.png)
5. Enter different usernames and same room at both windows. Then login.
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/u6jg2secm9n4m63dykp4.png)
6. Try to chat from one account and see what happened:
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fb17i89wrb1a6rofzhcz.png)
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/emafsw9snhsiowab5mud.png)