React hooks, released in February 2019, have huge benefits when implemented in your application. Whether you've used hooks before or are just learning, this post aims to show you just how simple it is to implement them with Stream Chat.
The Basics
Using hooks with Stream Chat is as simple as it sounds. Wherever you would regularly need local state or other React features, such as componentDidMount
, is an example of somewhere you could implement hooks and therefore clean up your code.
For a basic overview of the many types of hooks, check out the React Docs. In a nutshell, there are 3 main use cases:
- State Hook:
useState
adds local state to your component. This includes defining a current state variable, a function to update it, and a value to initialize that piece of state with. - Effect Hook:
useEffect
gives you the power to perform "side effects" such as data fetching, subscriptions, or other DOM manipulation within a functional component. - Build Your Own Hooks: By building your own hooks, you can reuse stateful logic between as many components as needed. It's important to use the same naming convention when creating your custom hook by always using the prefix
use
(such as naming your custom hookuseFriendStatus
) so that the React linter plugin is able to detect bugs.
There are a variety of other pre-made hooks you can use which are all laid out within the Hooks API Reference.
Adding The State Hook
To keep it simple, we've just added a bit of functionality to the Demo App we've already provided for you through our demo page found here. This demo chat app is made for users who need to communicate with their customer base. This demo only utilizes local state, so I'd added some lifecycle methods to it for us to refactor later on.
Here is the Codepen we will start with today. As you can see, App is a class component that utilizes local state to determine whether the Button
is set to open or closed. This will be an easy refactor to make the component functional with the useState
hook!
For now, we will ignore componentDidMount
and componentWillUnmount
, and just focus on adding useState
. For that reason, those are both commented out for the time being.
Original Class Component:
class App extends React.Component { constructor(props) { super(props); this.state = { open: true }; } toggleDemo = () => { if (this.state.open) { this.setState({ open: false }); } else { this.setState({ open: true }); } }; // componentDidMount() { // console.log('componentDidMount - rendered!'); // channel.sendMessage({ // text: 'Welcome to our customer chat with React Hooks tutorial!', // }); // }; // componentWillUnmount() { // console.log(`You have ${channel.state.messages.length} stored in local state. Goodbye!`); // }; render() { channel.on('message.new', event => { console.log('received a new message', event.message.text); }); return ( <> <div className={`wrapper ${this.state.open ? 'wrapper--open' : ''}`}> <Chat client={chatClient} theme={'commerce dark'}> <Channel channel={channel}> <Window> <ChannelHeader /> {this.state.open && ( <MessageList typingIndicator={TypingIndicator} Message={MessageCommerce} /> )} <MessageInput Input={MessageInputFlat} /> </Window> </Channel> </Chat> <Button onClick={this.toggleDemo} open={this.state.open} /> </div> </> ); } }
In the process of moving from a class to functional component, there are a few things you need to do first.
- Import what you need from React - because we are using Codepen, we will access
useState
anduseEffect
using dot notation (i.e.React.useState
) instead of with an import at the top of the file. In a typical project use case, you could just add the import to the top of the file:import React, { useState, useEffect } from 'react';
- Change App to be a functional component.
class App extends Component
turns intoconst App = () =>
- You will also need to add
const
to the beginning oftoggleDemo
since we will no longer be able to access it usingthis
.
- You will also need to add
- Remove the
render()
. Don't forget to delete both of the curly braces! 🙂
These are the few steps I always make sure I complete before moving on to the hooks refactor so that they aren't forgotten about later on. Now our component looks like this:
const App = () => { constructor(props) { super(props); this.state = { open: true, }; } const toggleDemo = () => { if (this.state.open) { this.setState({ open: false }); } else { this.setState({ open: true }); } }; // componentDidMount() { // console.log('componentDidMount - rendered!'); // channel.sendMessage({ // text: 'Welcome to our customer chat with React Hooks tutorial!', // }); // }; // componentWillUnmount() { // console.log(`You have ${channel.state.messages.length} stored in local state. Goodbye!`); // }; return ( <div className={`wrapper ${this.state.open ? 'wrapper--open' : ''}`}> <Chat client={chatClient} theme={'commerce dark'}> <Channel channel={channel}> <Window> <ChannelHeader /> {this.state.open && ( <MessageList typingIndicator={TypingIndicator} Message={MessageCommerce} /> )} <MessageInput Input={MessageInputFlat} /> </Window> </Channel> </Chat> <Button onClick={this.toggleDemo} open={this.state.open} /> </div> ); }
Step 1: Functional Component
This will break as is because we are still using constructor()
/super()
/this.state
as well as accessing our local state and functions with this
. That's our next step - refactor the component to utilize the useState
hook.
- First, change
constructor
/super
/this.state
into a hook: in order to accomplish this, you can start by simply removing the entire constructor, because you will be defining an entirely new variable usinguseState
. - After deleting the constructor, use the same key that you used in state as the new variable name. Since we were using
open
as the key withtrue
as the initial value, and using on onClick on the button to toggle that boolean, here is what the hook will look like:const [open, toggleOpen] = React.useState(true);
open
is the new variable nametoggleOpen
is the function to update the valuetrue
is the value we want to initialize the variable with, so we pass that intouseState
const App = () => { const [open, toggleOpen] = React.useState(true); const toggleDemo = () => { if (this.state.open) { this.setState({ open: false }); } else { this.setState({ open: true }); } }; // componentDidMount() { // console.log('componentDidMount - rendered!'); // channel.sendMessage({ // text: 'Welcome to our customer chat with React Hooks tutorial!', // }); // }; // componentWillUnmount() { // console.log(`You have ${channel.state.messages.length} stored in local state. Goodbye!`); // }; return ( <div className={`wrapper ${this.state.open ? 'wrapper--open' : ''}`}> <Chat client={chatClient} theme={'commerce dark'}> <Channel channel={channel}> <Window> <ChannelHeader /> {this.state.open && ( <MessageList typingIndicator={TypingIndicator} Message={MessageCommerce} /> )} <MessageInput Input={MessageInputFlat} /> </Window> </Channel> </Chat> <Button onClick={this.toggleDemo} open={this.state.open} /> </div> ); }
Step 2: Functional Component
Our refactor is almost complete. The last step is to update any references to this
, this.state
, and this.setState
to reflect our new functional component structure and state hook. That will change a few areas:
this.state.open
is now:open
this.setState({ open: [true or false] })
is now:toggleOpen([true or false])
this.toggleDemo
is now:toggleDemo
Here's the final result:
const App = () => { const [open, toggleOpen] = React.useState(true); const toggleDemo = () => { if (open) { toggleOpen(false); } else { toggleOpen(true); } }; // componentDidMount() { // console.log('componentDidMount - rendered!'); // channel.sendMessage({ // text: 'Welcome to our customer chat with React Hooks tutorial!', // }); // }; // componentWillUnmount() { // console.log(`You have ${channel.state.messages.length} stored in local state. Goodbye!`); // }; return ( <div className={`wrapper ${open ? 'wrapper--open' : ''}`}> <Chat client={chatClient} theme={'commerce dark'}> <Channel channel={channel}> <Window> <ChannelHeader /> {open && ( <MessageList typingIndicator={TypingIndicator} Message={MessageCommerce} /> )} <MessageInput Input={MessageInputFlat} /> </Window> </Channel> </Chat> <Button onClick={toggleDemo} open={open} /> </div> ); }
Cleaning It Up
To shorten your code up even more, you could switch the toggleDemo function into a quick ternary conditional since our toggleOpen
update is so short:
const App = () => { const [open, toggleOpen] = React.useState(true); const toggleDemo = () => open ? toggleOpen(false) : toggleOpen(true); // componentDidMount() { // console.log('componentDidMount - rendered!'); // channel.sendMessage({ // text: 'Welcome to our customer chat with React Hooks tutorial!', // }); // }; // componentWillUnmount() { // console.log(`You have ${channel.state.messages.length} stored in local state. Goodbye!`); // }; return ( <div className={`wrapper ${open ? 'wrapper--open' : ''}`}> <Chat client={chatClient} theme={'commerce dark'}> <Channel channel={channel}> <Window> <ChannelHeader /> {open && ( <MessageList typingIndicator={TypingIndicator} Message={MessageCommerce} /> )} <MessageInput Input={MessageInputFlat} /> </Window> </Channel> </Chat> <Button onClick={toggleDemo} open={open} /> </div> ); }
Overview
Overall, this small refactor took our component from 55 lines to 35. Leveraging the useState
hook allows us to quickly and easily set and update local state.
Adding The Effect Hook
Now let's look into adding the useEffect
hook! This means that we get to comment in our componentDidMount
and componentWillUnmount
lifecycle methods. For checking in on the functionality of the lifecycle methods, it's best to go back to our original Codepen. Within that you'll notice:
componentDidMount
does two things:- First, it logs that the component rendered (this is for anyone who's new to React and just wants a reminder of when this fires)
- Then, it utilizes Stream Chat's
sendMessage()
method (see the docs on this here) to demonstrate how you can send a pre-populated message to your customers when they join the chat.
componentWillUnmount
simply logs the number of state messages that you have in local state before unmounting the component. This shows you how you can check the number of local messages in your future app, and is generally just here to show you how to run clean up functions withuseEffect
.
Step 1: Setup The Hook
Refactoring these two lifecycle methods to utilize the Effect Hook is easier than you might think. We will start by hashing out the useEffect
method. Within CodePen, as stated above, you'll have to use dot notation to access it. This is what the refactor looks like to start:
React.useEffect(() => {});
Step 2: Refactoring componentDidMount
Whatever is usually put within your componentDidMount
can just be plopped right into this function. So, in our example, we take the console.log
and channel.sendMessage
within useEffect
like so:
React.useEffect(() => { console.log('componentDidMount - rendered!'); channel.sendMessage({ text: 'Welcome to our customer chat with React Hooks tutorial!', }); });
That is all that you need to do to add the same functionality as componentDidMount
with a hook! 👏
You can see this functionality in action with this Codepen.
Step 3: Refactoring componentWillUnmount
In order to add in logic which "cleans up" just before your component unmounts, all you have to do is return a function within your useEffect
. For example, within our original componentWillUnmount
, the only logic we performed was:
console.log( `You have ${channel.state.messages.length} stored in local state. Goodbye!` );
In order to add this effect to useEffect
, just put that log into a function, and return it at the end of the effect, like so:
return function cleanUp() { console.log( `You have ${channel.state.messages.length} stored in local state. Goodbye!` ); };
Easy as that! Now we've added all functionality back to our component, and the transition to a functional component with Hooks is complete. Here is the complete Codepen for your reference.
React.useEffect(() => { console.log('componentDidMount - rendered!'); channel.sendMessage({ text: 'Welcome to our customer chat with React Hooks tutorial!', }); return function cleanUp() { console.log( `You have ${channel.state.messages.length} stored in local state. Goodbye!` ); }; });
Step 4: Skipping Unnecessary Effects
The only "gotcha" with useEffect
is that there can be performance issues. We encourage you to read up on the strategies to optimize performance based on your needs within your application as referenced in the linked docs above. However, in our simple use case, all we need to do is pass in an empty array as a second argument. Essentially, by passing in this empty array, we are telling the Effect to not re-render when props and/or local state changes. This is because in our case we only want our Effect to run and clean up one time.
React.useEffect(() => { console.log('componentDidMount - rendered!'); channel.sendMessage({ text: 'Welcome to our customer chat with React Hooks tutorial!', }); return function cleanUp() { console.log( `You have ${channel.state.messages.length} stored in local state. Goodbye!` ); }; }, []);
Because each click of the Button component triggers the toggleOpen
function to change the open
bool in state, the entire component re-renders to reflect that. Without the empty array, our useEffect
would fire with each click, and therefore log "componentDidMount - rendered!" and, more importantly, re-send our "Welcome to our customer chat with React Hooks tutorial!" message every time. This is easily avoided by adding the empty array. Our welcome message is sent only once despite the chat box being open/closed - as intended. 🙂
Summary
As we all know, the frameworks we work with daily are constantly changing. React is the perfect example of a powerful framework that is consistently coming out with their versions of the latest and greatest tech. Adding Hooks is a simple process and significantly cleans up your code.
The great news is that there are no plans for React to remove classes, so you can keep the classes you are currently using, and just start implementing hooks within the smaller, and therefore more simple, components as you go. As the docs state, they are 100% backwards compatible. These are just the basics of hooks to get your feet wet and demonstrate how seamlessly they integrate with Stream Chat, which makes for an easy and exciting developer experience. 🙌