Build a Simple Chat Room App in React with Laravel Breeze and Twilio's Conversations API
Time to read: 10 minutes
2020 was a huge year for Laravel. It was the year that we saw this beloved framework take leaps and bounds over its competition and push the PHP ecosystem further. The Laravel team managed to make the framework even more powerful by adding new authentication and scaffolding features, which include Jetstream and Breeze.
Many developers know that full-stack apps with a Vue frontend can be quickly spun up with Laravel Jetstream. What many don’t know is that recently, the Laravel team made it easy to make an Inertia app with Laravel Breeze.
With this knowledge, let’s have some fun and make a very simple Discord-like app called “Twilcord” that will let multiple users join a room via a phone number or username. Instead of the default Vue frontend, we’ll be using React. This article will show you that you don’t need to build separate backends and frontends anymore; Laravel monoliths are the future!
Prerequisites
In order to get started with this tutorial you will need the following:
- A Twilio account
- Composer globally installed
- Laravel 8 installed and set up
- Node and NPM installed
- Postman installed
I’ll assume that you have some experience with Laravel and it’s templating structure. If not, links will be available throughout for additional reading. It’s also helpful if you have some familiarity with JSX or JavaScript XML which makes it easier to write HTML in React.
Laravel App Setup
Install your Laravel application by going to your terminal, navigating to the folder where you store your projects and running:
Once our app is installed, we will install the Laravel Breeze package using Composer. You should now be inside of the twilcord directory:
Next, we’ll set up Breeze’s resources, routes, and views. Instead of Blade, we’ll be using the Inertia stack.
NOTE: We won’t do any migrations or set up any models for this project.
Scaffolding the Frontend
Laravel Breeze comes with a Vue frontend by default. Since we’re using React for this project, we’ll need to make a few adjustments. The first thing that we’ll need to do is install React and add some dependencies by running:
Next, we’ll need to go to resources/js/app.js and make some changes so that it looks and functions like the entry-point of a React application:
We’ll then need to ensure that Babel (our JavaScript compiler) can understand the JSX code that we’ll be writing, so we’ll need to install a Babel preset by running npm install --save-dev @babel/preset-react
.
This will install three plugins: @babel/plugin-syntax-jsx
, @babel/plugin-transform-react-jsx
, and @babel/plugin-transform-react-display-name
.
We can then use this preset by creating a .babelrc file in the root with this code:
After this, we’ll configure TailwindCSS, which should already be installed. We want to easily make forms, so we’ll install the Tailwind Forms plugin by running: $ npm install @tailwindcss/forms
. Next, add the plugin to the tailwind.config.js file like so:
Go to your webpack.mix.js file and ensure that Tailwind is added as a plugin for PostCSS. Also, if you see .vue()
attached to mix
, remove it or there will be errors.
Next, we’ll need to remove all Vue files from the frontend. You could simply delete the Components, Layouts, and Pages folders in resources/js, or you could rename them if you want to keep the files for reference or for some other use.
Create a new Pages folder and a Home.js file within. We’ll make a simple “Hello World” app to make sure that everything is working properly.
Let’s compile our assets by running $ npm run dev
.
NOTE: If you get a Laravel Mix error, try to upgrade to the latest version of Laravel Mix by running npm install laravel-mix@latest --save-dev
.
Finally, go to routes/web.php where you’ll see a bunch of routes already pre-defined by Breeze. Delete these routes and create a route for the home page. We’ll be using the Inertia package here, but we won’t be sending any data to the front page.
NOTE: For simplicity, we’ll be making an SPA (or single page application) of sorts without much routing. We want the user to sign in on the ‘Home’ route and we’ll let all the messages load on that same route when signed in. In the regular way, we would have a ‘Sign In’ route and load all the messages separately for our ‘Home’ route.
It’s time to get our app up and running. Run php artisan serve
. By default, you should see your app by typing http://localhost:8000 in your browser.
Phew, that was a lot of set-up! Now, let’s start building our app.
Implementing Twilio’s Conversation API
With Twilio’s Conversation API, we’ll be able to create multi-party chats where participants can use chat, SMS, MMS, and WhatsApp to create messages. The possibilities are endless, but for this tutorial, we’ll only be building a chatroom with SMS and chat capabilities.
Remember to ensure that you create a Twilio account, and if you’re on a free trial, have at least one verified phone number so that you can send text messages to the Conversations API. Install the Twilio PHP SDK by running the following in a new terminal window:
Add your Twilio Phone Number, Twilio Account SID, and Twilio Auth Token to your project’s .env file. You can locate the Account SID and Auth Token from the Console. It should look something like this:
Now we’re going to set up a Twilio service that will use these variables. Create a Services folder in the Laravel app/
folder and create a Twilio.php file. In this file, we’ll be making methods that create the conversation, add SMS participants, add chat participants, create messages, and fetch all messages.
Testing Our Methods
We have a long way to go, but testing our backend now will ensure that we don’t encounter any surprises. Let’s go to our api.php route and set endpoints to create conversations, create SMS users, create chat users, send messages, and fetch messages. Your API routes should look something like this:
NOTE: You may realize that we’re not using Models or Controllers. We’re not doing any database work, so we’ll do route logic in Closures instead.
Awesome! With our routes created, let’s test them out in Postman. The first thing we’ll test is our endpoint for creating conversations. Make a POST request to http://localhost:8000/api/convo/create. The JSON response should return a key-value pair with sid
as the index and a unique token as the value as seen in the following screenshot:
NOTE: Twilio can return many more properties than this. If you look at how my route is set up, you will see I chose to only return sid
. If you want to see all the possibilities, look at Twilio’s Conversation Resource.
To create an SMS user, copy the sid
, make a new tab, and place it into the endpoint like so: http://localhost:8000/api/convo/{id}/sms-participant-new. Click on the Body
tab under the URL bar and select the form-data
radio button. You can then enter the phone number
that you’ll be using there. Make sure that you supply the phone number in E.164 format.
You can then follow this pattern to create a new chat participant with a username
using the http://localhost:8000/api/convo/{id}/chat-participant-new endpoint.
Now let’s create our first message with Postman! Pass a username
and message
to the http://localhost:8000/api/convo/{id}/create-message endpoint. Both values should be strings.
You should now realize that you’ve gotten a text message. You can respond to it right from your phone! When you’ve done so, you can then make a GET request to http://localhost:8000/api/convo/{id}/messages. When we fetch all our messages, it should look something like this:
Creating the Chat UI
Finally, we’re at the part you’ve been waiting for. A reminder: we are creating a very simple chat room app with some compromises. Our app will not be real-time, but will instead update every 3 seconds. In another article, we’ll go deeper into how we can make a real-time chat application using webhooks. Now let’s get down to details on how we’re going to create the Twilcord UI with React.
First, a little more backend stuff. We’ll need to copy over our API routes and make them applicable for web.php
since Laravel uses this route for frontend traffic.
Inertia::render()
doesn’t work with POST
requests, so we have to redirect back to the page with our data. This mechanism is Laravel’s and not Inertia’s, so we need a way for Inertia to share the data to the React frontend through props. Because message
is flashed to the session, we can get Laravel to share that flashed message with Inertia. We do this through the boot()
method of our Providers/AppServiceProvider.php file.
I always set up my React projects with pages in one folder, and components in a separate folder:
I like to compartmentalize as much as possible, so Home.js will be pretty barebones. When a user visits Twilcord, they will be asked if they want to join an already established room or create a new one. They will be able to join via SMS or by supplying a username. To make things simple, the room displayed will be the last one that was made. Since there is no database, the best way to store this information is in a file. We’ll store the room information in this file and read from it with Laravel, since PHP comes with these features baked in.
We’ll create this file as a trait and access its methods statically. We’ll also give each chat a very random name with the Faker library. To do this, create a Traits/ folder in the Laravel app/ folder. Create a new file called FileHelpersTrait.php.
Now we need to call our trait in our routes.
Now in our Home.js file, we’ll programmatically detect whether to display our component that allows people to sign in or the component that allows messages to be sent. Because we can’t simply just pass data up from child to parent, we need a way to tell Home that a submission was made in our SignUp component. We can accomplish this by passing the onChange
prop to the Signup component. Then we can pass a state to the props in the child component that only changes when a submission is made. This is how Home is set up:
In our SignUp component, we’ll give the user the option to sign in with a number or a username (which will hit two different endpoints), and decide if they want to join the most recent room. First, we’ll have a very simple layout with two buttons; an input, and a checkbox. The checkbox will set a value of true
or false
to denote whether the user will join the most recent room. We’ll load the name of this room from the JSON file we made earlier.
Create the SignUp component in resources/js/Components called SignUp.js. Add the following code:
Awesome! Now that we have that, we’ll need to handle the submission of our code and the mechanism by which we’ll tell the parent component (Home) that we’ve made a submission. There are different ways to enter a chat room, hence, we’ll use some if
statements. We’ll be doing it this way for clarity, but you’re free to rewrite these if
statements in any way you choose.
The two functions that we’ll be writing, changeChat()
and handleSubmit()
, can be placed wherever you like, but I suggest that you place them below the handleChange()
function we made a while ago in SignUp.js. We’ll be using Inertia to submit our data. Usually, we would use Inertia Links that compile to HTML buttons, but to mimic Promises, we’ll handle these submissions manually.
Create the ChatForm component in resources/js/Components called ChatForm.js. We’ll leave it empty for now. Run npm run dev
and you should see the SignUp form. If you sign in, you may see an error in a modal. We’ll discuss error handling when we do a deeper dive into Inertia apps at a later date. If you’d like to get ahead now, you could take a look at this old blog article discussing the topic. The frontend is in Vue, so you may need to make some adjustments.
Plugging and Playing the Chat UI
In our ChatForm component, we’ll have a box that contains all of the room’s messages and an input field at the bottom. When we create a message, we’ll send it to the Conversation API with the correct Conversation SID. Just like in our SignUp component, we’ll use Inertia Manual Visits that mimic Promises instead of Inertia Links.
Once we have submissions done, we need a way to load our messages automatically. We’ll use the useEffect
React hook to achieve this. There is no way to achieve real-time message-loading with a REST architecture without technologies such as websockets or webhooks, so we’ll “cheat” by fetching our messages every three seconds. Also, we’ll be using async-await
instead of Inertia to load our messages because of how our app is set up.
This is how our component will look when done:
Look at our getMessages()
function. Because we’re fetching messages every three seconds, we need a mechanism to only load messages when there is a new one. We do this by finding the length of our message collection, and comparing it with the previous length. Once the new length is greater than the previous length, we set our message collection and the new length in our state.
If you look at our last useEffect
hook, you will see that we’re using the JavaScript function setInterval()
. which accepts a function and a time period (in milliseconds) as parameters. We call the clearInterval()
function afterwards to stop this function when the component unmounts or it will go on forever!
There’s one more thing we need to do in order for this chat room to work properly. You will see a Message component being called that takes three props (plus key
). This component will house each message in the conversation. We need to define this component, so we’ll make a Message.js file in our Components/ folder.
Amazing! Now we’re ready to try “Twilcord” out! Make sure that your server is running, run npm run dev
, and go to http://localhost:8000. You’ll be asked to sign up again. Let’s start fresh, so if there’s a room already there, don’t check it. Enter a username, and once you’re in the room, send a message.
Open a new tab, go to http://localhost:8000, and check the room that you see. This time, we’ll enter our phone number so that we can send messages via text message. With your phone number that you added to the Twilio Dashboard, send a text message to your Twilio number (the one that you got from Twilio). You will now see a message from your own phone number in “Twilcord”!
Conclusion
Phew! That was a lot of work, but you did it. You made a chat room app with Laravel Breeze, React, and Twilio’s new Conversations API! It is good to learn this API now because soon it will replace the Programmable Chat API. In this tutorial we learned how to create a Laravel Breeze app with Inertia.js and React.js. This should still be useful information even if you decide to use the default Vue scaffolding instead. I believe that soon, we’ll be able to simply choose between React, Vue, and maybe even Svelte. I believe that building monoliths with Laravel and Inertia will be the hot new trend once this happens.
In a later article, we’ll discuss how we can build a real-time chat room app using the webhooks provided by Twilio’s Conversations API. We’ll also discuss a better way to route and authenticate our application, and how to send errors to our frontend the Inertia way. Until then, enjoy your new creation!
Lloyd Miller is a freelance full-stack web developer based in New York. He enjoys creating customer-facing products with Laravel, Gatsby, Next.js and Nuxt.js, and documenting his journey through blog posts at https://lloydmiller.dev
Related Posts
Related Resources
Twilio Docs
From APIs to SDKs to sample apps
API reference documentation, SDKs, helper libraries, quickstarts, and tutorials for your language and platform.
Resource Center
The latest ebooks, industry reports, and webinars
Learn from customer engagement experts to improve your own communication.
Ahoy
Twilio's developer community hub
Best practices, code samples, and inspiration to build communications and digital engagement experiences.