React Hooks Tutorial with Stream Chat

Learn how to use Hooks in React by using them in our demo app

Whitney B.
Whitney B.
Published January 8, 2020 Updated May 12, 2020

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 hook useFriendStatus) 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 and useEffect 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 into const App = () =>

    • You will also need to add const to the beginning of toggleDemo since we will no longer be able to access it using this.
  • 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 using useState.
  • 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 with true 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 name
    • toggleOpen is the function to update the value
    • true is the value we want to initialize the variable with, so we pass that into useState
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:

Building your own app? Get early access to our Livestream or Video Calling API and launch in days!
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 with useEffect.

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. 🙌

Integrating Video With Your App?
We've built a Video and Audio solution just for you. Check out our APIs and SDKs.
Learn more ->