How to build a messaging application in React 🤯

How to build a messaging application in React 🤯

Basic setup

  1. We’ll use React.js + ChakraUI for basic UI setup
  2. Rocketgraph to setup the backend
npx create-react-app messaging-app

Install Chakra UI

yarn add @chakra-ui/react @emotion/react @emotion/styled framer-motion

Install other necessary libs

yarn add react-router-dom

Head over to rocketgraph and create an account. Then click on “Create Project”. Don’t worry, it comes with a free trail and no CC required.

Wait for the project to boot and go to the Hasura console.

Skeleton App

Now let’s build the basic UI without the actual data/backend.

create src/components directory to hold our components.

In the index.js file, replace it with the following

// src/index.js
import React from "react";
import ReactDOM from "react-dom";
import { ChakraProvider } from '@chakra-ui/react'
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
import App from "./App";
import Message from "./components/Message"

ReactDOM.render(
  <React.StrictMode>
    <ChakraProvider>
      <Router>
        <Routes>
          <Route path="/login" />
          <Route path="/signup" />
          <Route path="/messages" element={<Message />} />
          <Route path="/" element={<App />} />
        </Routes>
      </Router>
    </ChakraProvider>
  </React.StrictMode>,
  document.getElementById("root")
);

Now let’s create the individual components to show on the app.

Create src/components/Message.js and add the following code.

import React from "react";
import { useSearchParams } from 'react-router-dom';

import { Input } from '@chakra-ui/react'
import { Button, ButtonGroup } from '@chakra-ui/react'
import { Card, CardHeader, CardBody, CardFooter, Heading, Text, Flex, Avatar, Box } from '@chakra-ui/react'


const messages = [
    {
        from: "",
        to: "",
        from_address: "",
        to_address: "",
        message: "View a summary of all your customers over the last month.",
    },
    {
        from: "",
        to: "",
        from_address: "",
        to_address: "",
        message: "View a summary of all your customers over the last month.",
    }
]

export default function DrawerExample() {
    const [searchParams] = useSearchParams();
    console.log(searchParams.get('email')); // ▶ URLSearchParams {}
    // currentUser = Cookies.get("user")
    const currentUser = "kaushik@moneysave.io"
    return (
      <>
        <div className="message-body">
            <div className="messages">
                {
                    messages.map((message) => {
                        return (
                            <div className={message.to == currentUser ? "message-received" : "message-sent"} m={4}>
                                <div className="message-text">
                                    <Card>
                                        <CardHeader>
                                            <Flex spacing='4'>
                                            <Flex flex='1' gap='4' alignItems='center' flexWrap='wrap'>
                                                <Avatar name='Segun Adebayo' src='https://bit.ly/sage-adebayo' />
            
                                                <Box>
                                                <Heading size='sm'>Segun Adebayo</Heading>
                                                <Text>Creator, Chakra UI</Text>
                                                </Box>
                                            </Flex>
                                            </Flex>
                                        </CardHeader>
                                        <CardBody>
                                            <Text>{message.message}</Text>
                                        </CardBody>
                                    </Card>
                                </div>
                            </div>
                        )
                    })
                }


            </div>
            <div className="main-bottom">
                <Input m={2} placeholder='Basic usage' />
                <Button m={2} colorScheme='blue'>Button</Button>
            </div>
        </div>
      </>
    )
}

Here we have created a dummy message component which displays the chat with a user based on the query parameters in the URL. We will discuss this later. Once we implement the actual functionality, the messages will become live

Image description

And in App.css add the following code:

.App {
  text-align: center;
}

.App-logo {
  height: 40vmin;
  pointer-events: none;
}

@media (prefers-reduced-motion: no-preference) {
  .App-logo {
    animation: App-logo-spin infinite 20s linear;
  }
}

.App-header {
  background-color: #282c34;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  font-size: calc(10px + 2vmin);
  color: white;
}

.App-link {
  color: #61dafb;
}

.message-body {
  position: relative;
  height: 100vh;
}
.main-bottom {
  position: absolute;
  bottom: 10px;
  left: 0px;
  width: 100%;
  display: flex;
}

.message-sent {
  padding: 10px;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
}
.message-received {
  padding: 10px;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
}

.message-text {

}

@keyframes App-logo-spin {
  from {
    transform: rotate(0deg);
  }
  to {
    transform: rotate(360deg);
  }
}

In App.js, add the following code

import logo from './logo.svg';
import Profiles from "./components/Profiles"
import './App.css';

function App() {
  return (
    <div className="App">
      <Profiles/>
    </div>
  );
}

export default App;

Now create src/components/Profiles.js

import {
    Heading,
    Avatar,
    Box,
    Center,
    Image,
    Flex,
    Text,
    Stack,
    Button,
    useColorModeValue,
  } from '@chakra-ui/react';
  
  export default function SocialProfileWithImage() {
    return (
      <Center py={6}>
        <Box
          maxW={'270px'}
          w={'full'}
          bg={useColorModeValue('white', 'gray.800')}
          boxShadow={'2xl'}
          rounded={'md'}
          overflow={'hidden'}>
          <Image
            h={'120px'}
            w={'full'}
            src={
              'https://images.unsplash.com/photo-1612865547334-09cb8cb455da?ixid=MXwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHw%3D&ixlib=rb-1.2.1&auto=format&fit=crop&w=634&q=80'
            }
            objectFit={'cover'}
          />
          <Flex justify={'center'} mt={-12}>
            <Avatar
              size={'xl'}
              src={
                'https://images.unsplash.com/photo-1500648767791-00dcc994a43e?ixlib=rb-1.2.1&q=80&fm=jpg&crop=faces&fit=crop&h=200&w=200&ixid=eyJhcHBfaWQiOjE3Nzg0fQ'
              }
              alt={'Author'}
              css={{
                border: '2px solid white',
              }}
            />
          </Flex>
  
          <Box p={6}>
            <Stack spacing={0} align={'center'} mb={5}>
              <Heading fontSize={'2xl'} fontWeight={500} fontFamily={'body'}>
                John Doe
              </Heading>
              <Text color={'gray.500'}>Frontend Developer</Text>
            </Stack>
  
            <Button
              w={'full'}
              mt={8}
              bg={useColorModeValue('#151f21', 'gray.900')}
              color={'white'}
              rounded={'md'}
              onClick={() => {window.location = "/messages?email=kaushik@moneysave.io"} }
              _hover={{
                transform: 'translateY(-2px)',
                boxShadow: 'lg',
              }}>
              Message
            </Button>
          </Box>
        </Box>
      </Center>
    );
  }

Now let’s create the login and signup pages as follows:

src/components/login.js

import {
    Flex,
    Box,
    FormControl,
    FormLabel,
    Input,
    Checkbox,
    Stack,
    Link,
    Button,
    Heading,
    Text,
    useColorModeValue,
  } from '@chakra-ui/react';
  
  export default function SimpleCard() {
    return (
      <Flex
        minH={'100vh'}
        align={'center'}
        justify={'center'}
        bg={useColorModeValue('gray.50', 'gray.800')}>
        <Stack spacing={8} mx={'auto'} maxW={'lg'} py={12} px={6}>
          <Stack align={'center'}>
            <Heading fontSize={'4xl'}>Sign in to your account</Heading>
            <Text fontSize={'lg'} color={'gray.600'}>
              to enjoy all of our cool <Link color={'blue.400'}>features</Link> ✌️
            </Text>
          </Stack>
          <Box
            rounded={'lg'}
            bg={useColorModeValue('white', 'gray.700')}
            boxShadow={'lg'}
            p={8}>
            <Stack spacing={4}>
              <FormControl id="email">
                <FormLabel>Email address</FormLabel>
                <Input type="email" />
              </FormControl>
              <FormControl id="password">
                <FormLabel>Password</FormLabel>
                <Input type="password" />
              </FormControl>
              <Stack spacing={10}>
                <Stack
                  direction={{ base: 'column', sm: 'row' }}
                  align={'start'}
                  justify={'space-between'}>
                  <Checkbox>Remember me</Checkbox>
                  <Link color={'blue.400'}>Forgot password?</Link>
                </Stack>
                <Button
                  bg={'blue.400'}
                  color={'white'}
                  _hover={{
                    bg: 'blue.500',
                  }}>
                  Sign in
                </Button>
              </Stack>
            </Stack>
          </Box>
        </Stack>
      </Flex>
    );
  }

src/components/signup.js

import {
    Flex,
    Box,
    FormControl,
    FormLabel,
    Input,
    Checkbox,
    Stack,
    Link,
    Button,
    Heading,
    Text,
    useColorModeValue,
  } from '@chakra-ui/react';
  
  export default function SimpleCard() {
    return (
      <Flex
        minH={'100vh'}
        align={'center'}
        justify={'center'}
        bg={useColorModeValue('gray.50', 'gray.800')}>
        <Stack spacing={8} mx={'auto'} maxW={'lg'} py={12} px={6}>
          <Stack align={'center'}>
            <Heading fontSize={'4xl'}>Create your account</Heading>
            <Text fontSize={'lg'} color={'gray.600'}>
              to enjoy all of our cool <Link color={'blue.400'}>features</Link> ✌️
            </Text>
          </Stack>
          <Box
            rounded={'lg'}
            bg={useColorModeValue('white', 'gray.700')}
            boxShadow={'lg'}
            p={8}>
            <Stack spacing={4}>
              <FormControl id="email">
                <FormLabel>Email address</FormLabel>
                <Input type="email" />
              </FormControl>
              <FormControl id="password">
                <FormLabel>Password</FormLabel>
                <Input type="password" />
              </FormControl>
              <Stack spacing={10}>
                <Stack
                  direction={{ base: 'column', sm: 'row' }}
                  align={'start'}
                  justify={'space-between'}>
                  <Checkbox>Remember me</Checkbox>
                  <Link color={'blue.400'}>Forgot password?</Link>
                </Stack>
                <Button
                  bg={'blue.400'}
                  color={'white'}
                  _hover={{
                    bg: 'blue.500',
                  }}>
                  Sign in
                </Button>
              </Stack>
            </Stack>
          </Box>
        </Stack>
      </Flex>
    );
  }

Great, now we finished setting up the skeleton of the front-end of the application. Now we need to inject data into this via the backend. Let’s create the backend. Head over to the next article in the series.

Backend on Rocketgraph

Head over to the Hasura console. You can find it here on the 2nd tab in Rocketgraph:

Rocketgraph Hasura tab

use the password to login to the console. And head over to the data column and create the following columns:

Image description

Permissions

We have to add user permissions so that the users can view only their messages.

Head over to the permissions tab in messages and create a role called user. A user is a person’s role who is logged in.

Create insert permissions as follows:

Image description

Notice the column presets. WE are letting Hasura to inject custom variables automatically into our columns, so we can use them later.

Next allow select over the messages as follows:

Image description

We are telling Hasura to only allow messages to the user if he is the sender or receiver of the message.

And save:

Image description

Enter GQL

So to fetch the data from our backend we’ll be using GraphQL. So let’s install the necessary libraries.

yarn add @apollo/client
yarn add @rocketgraphql/rocketgraph-js-sdk
yarn add @rocketgraphql/react-apollo 

Now modify the Signup.js page to be able to register new users:

import React, { useState } from "react";
import { useNavigate } from 'react-router-dom';
import { auth } from "../utils/config";

import {
    Flex,
    Box,
    FormControl,
    FormLabel,
    Input,
    Checkbox,
    Stack,
    Link,
    Button,
    Heading,
    Text,
    useColorModeValue,
  } from '@chakra-ui/react';
  
  export default function SimpleCard() {
    const [ email, setEmail ] = useState()
    const [ password, setPassword ] = useState()
    const navigate = useNavigate();

    const signup = async () => {
      // login
      try {
        await auth.register({email, password});
        navigate("/")
      } catch (error) {
        alert("error signing up");
        console.error(error);
        return;
      }
    }
    return (
      <Flex
        minH={'100vh'}
        align={'center'}
        justify={'center'}
        bg={useColorModeValue('gray.50', 'gray.800')}>
        <Stack spacing={8} mx={'auto'} maxW={'lg'} py={12} px={6}>
          <Stack align={'center'}>
            <Heading fontSize={'4xl'}>Create your account</Heading>
            <Text fontSize={'lg'} color={'gray.600'}>
              to enjoy all of our cool <Link color={'blue.400'}>features</Link> ✌️
            </Text>
          </Stack>
          <Box
            rounded={'lg'}
            bg={useColorModeValue('white', 'gray.700')}
            boxShadow={'lg'}
            p={8}>
            <Stack spacing={4}>
              <FormControl id="email">
                <FormLabel>Email address</FormLabel>
                <Input type="email" onChange={(e) => setEmail(e.target.value)}/>
              </FormControl>
              <FormControl id="password">
                <FormLabel>Password</FormLabel>
                <Input type="password" onChange={(e) => setPassword(e.target.value)}/>
              </FormControl>
              <Stack spacing={10}>
                <Stack
                  direction={{ base: 'column', sm: 'row' }}
                  align={'start'}
                  justify={'space-between'}>
                  <Checkbox>Remember me</Checkbox>
                  <Link color={'blue.400'}>Forgot password?</Link>
                </Stack>
                <Button
                  onClick={() => signup()}
                  bg={'blue.400'}
                  color={'white'}
                  _hover={{
                    bg: 'blue.500',
                  }}>
                  Sign up
                </Button>
              </Stack>
            </Stack>
          </Box>
        </Stack>
      </Flex>
    );
  }

Notice that we are using the Rocketgraph’s auth.register() method. This will take care of handling the signup and setting the user cookies for us.

And similarly for the login:

import React, { useState } from "react";
import { useNavigate } from 'react-router-dom';
import { auth } from "../utils/config";

import {
    Flex,
    Box,
    FormControl,
    FormLabel,
    Input,
    Checkbox,
    Stack,
    Link,
    Button,
    Heading,
    Text,
    useColorModeValue,
  } from '@chakra-ui/react';
  
  export default function SimpleCard() {
    const [ email, setEmail ] = useState()
    const [ password, setPassword ] = useState()
    const navigate = useNavigate();

    const signup = async () => {
      // login
      try {
        await auth.signIn({email, password, provider: "local"});
        navigate("/")
      } catch (error) {
        alert("error signing up");
        console.error(error);
        return;
      }
    }
    return (
      <Flex
        minH={'100vh'}
        align={'center'}
        justify={'center'}
        bg={useColorModeValue('gray.50', 'gray.800')}>
        <Stack spacing={8} mx={'auto'} maxW={'lg'} py={12} px={6}>
          <Stack align={'center'}>
            <Heading fontSize={'4xl'}>Login to your account</Heading>
            <Text fontSize={'lg'} color={'gray.600'}>
              to enjoy all of our cool <Link color={'blue.400'}>features</Link> ✌️
            </Text>
          </Stack>
          <Box
            rounded={'lg'}
            bg={useColorModeValue('white', 'gray.700')}
            boxShadow={'lg'}
            p={8}>
            <Stack spacing={4}>
              <FormControl id="email">
                <FormLabel>Email address</FormLabel>
                <Input type="email" onChange={(e) => setEmail(e.target.value)}/>
              </FormControl>
              <FormControl id="password">
                <FormLabel>Password</FormLabel>
                <Input type="password" onChange={(e) => setPassword(e.target.value)}/>
              </FormControl>
              <Stack spacing={10}>
                <Stack
                  direction={{ base: 'column', sm: 'row' }}
                  align={'start'}
                  justify={'space-between'}>
                  <Checkbox>Remember me</Checkbox>
                  <Link color={'blue.400'}>Forgot password?</Link>
                </Stack>
                <Button
                  onClick={() => signup()}
                  bg={'blue.400'}
                  color={'white'}
                  _hover={{
                    bg: 'blue.500',
                  }}>
                  Sign up
                </Button>
              </Stack>
            </Stack>
          </Box>
        </Stack>
      </Flex>
    );
  }

Now test that you are able to retrieve the users list via the following code in App.js:

import logo from './logo.svg';
import Profiles from "./components/Profiles"
import './App.css';

function App() {
  return (
    <div className="App">
      <Profiles/>
    </div>
  );
}

export default App;

And create a component named Profiles.js in /components:

import {
    Heading,
    Avatar,
    Box,
    Center,
    Image,
    Flex,
    Text,
    Stack,
    Button,
    useColorModeValue,
    useRangeSlider,
  } from '@chakra-ui/react';
  import { gql, useQuery, useMutation, useSubscription } from "@apollo/client";

  const GET_TODOS = gql`
    subscription {
      users {
        id
        email
      }
    }
  `;

  export default function SocialProfileWithImage() {
    const { data, loading } = useSubscription(GET_TODOS);
    const bgColor800 = useColorModeValue('white', 'gray.800');
    const bgColor900 = useColorModeValue('#151f21', 'gray.900');
    const users = data?.users;
    return (
      <Center py={6}>
        {
          users && users.map((user) => {
            return (
              <Box
              maxW={'270px'}
              w={'full'}
              bg={bgColor800}
              boxShadow={'2xl'}
              rounded={'md'}
              overflow={'hidden'}>
              <Image
                h={'120px'}
                w={'full'}
                src={
                  'https://images.unsplash.com/photo-1612865547334-09cb8cb455da?ixid=MXwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHw%3D&ixlib=rb-1.2.1&auto=format&fit=crop&w=634&q=80'
                }
                objectFit={'cover'}
              />
              <Flex justify={'center'} mt={-12}>
                <Avatar
                  size={'xl'}
                  src={
                    'https://images.unsplash.com/photo-1500648767791-00dcc994a43e?ixlib=rb-1.2.1&q=80&fm=jpg&crop=faces&fit=crop&h=200&w=200&ixid=eyJhcHBfaWQiOjE3Nzg0fQ'
                  }
                  alt={'Author'}
                  css={{
                    border: '2px solid white',
                  }}
                />
              </Flex>
      
              <Box p={6}>
                <Stack spacing={0} align={'center'} mb={5}>
                  <Heading fontSize={'2xl'} fontWeight={500} fontFamily={'body'}>
                    {user.email}
                  </Heading>
                  <Text color={'gray.500'}>Frontend Developer</Text>
                </Stack>
      
                <Button
                  w={'full'}
                  mt={8}
                  bg={bgColor900}
                  color={'white'}
                  rounded={'md'}
                  onClick={() => {window.location = `/messages?email=${user.email}`} }
                  _hover={{
                    transform: 'translateY(-2px)',
                    boxShadow: 'lg',
                  }}>
                  Message
                </Button>
              </Box>
            </Box>
            )
          })
        }

      </Center>
    );
  }

Postgres Triggers

SO what are Postgres triggers and why are they so great. To be honest I also didn’t know about them until I wrote this application. And realising just how powerful they are made me rethink my view on web-development.

Basically Postgres triggers are functions that are triggered when an event(CREATE, READ, UPDATE, DELETE) happens. You can do powerful joins and interpolations using these triggers. They are completely on the backend and you can use them to set restrictions on the data.

So let’s say you want to embed to_id field automatically into every message. to_id is the id of the user the message is addressed to. Since this is not available on the frontend, we will do it from the backend.

Go to the SQL tab in your Hasura console:

Image description

And add the following code:

CREATE FUNCTION create_to_id()
    RETURNS trigger AS $BODY$
    BEGIN
    SELECT id INTO NEW.to_id FROM users WHERE email = NEW.to_email;
    RETURN NEW;
    END;
    $BODY$ LANGUAGE plpgsql;

CREATE TRIGGER insert_id BEFORE INSERT OR UPDATE ON messages FOR EACH ROW EXECUTE PROCEDURE create_to_id();

We are telling SQL to get the id from email and insert that into the new record before inserting the record. This way we get the to id information in the table for using later.

Now modify the code in Message.js component to the following:

import React, {useEffect, useState} from "react";
import { useSearchParams } from 'react-router-dom';

import { Input } from '@chakra-ui/react'
import { Button, ButtonGroup } from '@chakra-ui/react'
import { Card, CardHeader, CardBody, CardFooter, Heading, Text, Flex, Avatar, Box } from '@chakra-ui/react'
import { gql, useQuery, useMutation, useSubscription } from "@apollo/client";


const ADD_MESSAGE = gql`
    mutation MyMutation($message: String!, $from_email: String!, $to_email: String!) {
        insert_messages_one(object: {from_email: $from_email, message: $message, to_email: $to_email}) {
            from_email
            message
            to_id
            to_email
        }
    }
`;

const GET_MESSAGES = gql`
    subscription MySubscription {
        messages {
            id
            message
            from_email
            to_email
        }
    }
`;

// CREATE FUNCTION check_author_active()
//     RETURNS trigger AS $BODY$
//     BEGIN
//     SELECT id INTO NEW.to_id FROM users WHERE email = NEW.to_email;
//     RETURN NEW;
//     END;
//     $BODY$ LANGUAGE plpgsql;

// CREATE TRIGGER insert_article BEFORE INSERT OR UPDATE ON messages FOR EACH ROW EXECUTE PROCEDURE check_author_active();


export default function DrawerExample() {
    const [searchParams] = useSearchParams();
    const [messages, setMessages] = useState([]);
    console.log(searchParams.get('email')); // ▶ URLSearchParams {}
    const currentUser = "kaushik@moneysave.io";
    const { data, loading } = useSubscription(GET_MESSAGES);
    console.log("messages: ", data, loading);
    const [addMessage, { messages_mutate, message_loading }] = useMutation(ADD_MESSAGE);
    const senderAddress = searchParams.get('email');
    const [message, setMessage] = useState("");
    const sendMessage = (text, from_email, to_email) => {
        addMessage({ variables: { message: text, from_email: from_email, to_email: to_email } });
    }
    useEffect(() => {
        if (data && data.messages.length) {
            setMessages(data?.messages);
        }
    }, [data])
    return (
      <>
        <div className="message-body">
            <div className="messages">
                {
                    messages.map((message) => {
                        return (
                            <div key={message.id} className={message.to_email == currentUser ? "message-received" : "message-sent"} m={4}>
                                <div className="message-text">
                                    <Card>
                                        <CardHeader>
                                            <Flex spacing='4'>
                                            <Flex flex='1' gap='4' alignItems='center' flexWrap='wrap'>
                                                <Avatar name='Segun Adebayo' src='https://bit.ly/sage-adebayo' />
            
                                                <Box>
                                                <Heading size='sm'>{message.from_email}</Heading>
                                                <Text>Creator, Chakra UI</Text>
                                                </Box>
                                            </Flex>
                                            </Flex>
                                        </CardHeader>
                                        <CardBody>
                                            <Text>{message.message}</Text>
                                        </CardBody>
                                    </Card>
                                </div>
                            </div>
                        )
                    })
                }


            </div>
            <div className="main-bottom">
                <Input m={2} placeholder='Basic usage' onChange={(e) => {setMessage(e.target.value)}}/>
                <Button m={2} colorScheme='blue' onClick={() => {sendMessage(message, "kaushik@rocketgraph.io", "kaushik");}}>Button</Button>
            </div>
        </div>
      </>
    )
}

You should be able to send messages and receive on this component.

Be sure to check out Rocketgraph