Sergio Xalambrí

Introduction to React with the State & Effects hooks

Learn how to start a React project, create your first component and use the State and Effects hooks to implement basic behaviors.

Starting a React Project

Let's start by creating a new React application, we could use the Create React App (CRA) tool to generate a basic boilerplate without configuration.

$ npx create-react-app my-app

The npx command comes bundled with npm and let us use a command from a npm package without installing it globally.

Running Your Project

Now that we have the project created we could access our application directory and start the project, to do so run the following commands.

$ cd my-app
$ yarn start

Rendering an Element with React

You will notice your project comes with some files already created, delete all files inside the src folder and create a new index.js file with the content below.

// src/index.js
import React from "react";
import { render } from "react-dom";

const $root = document.getElementById("root");
render(<h1>Hello, World!</h1>, $root);

This will render <h1>Hello, World!</h1> to the DOM using React, we just rendered our first element.

Creating a Component

Now let's create our first component.

// src/index.js
function HelloWorld() {
  return <h1>Hello, World!</h1>;
}

const $root = document.getElementById("root");
render(<HelloWorld />, $root);

A component is a normal JavaScript function but the name should start with a capitalized letter and it must return some kind of HTML code. There are other valid values like arrays or null, but you usually want to return HTML.

Note: This is not real HTML, is something called JSX and in build time is transformed to function calls, specifically to the React.createElement function.

Running an Effect

This time we will run a side effect, in this case we will change the title of the page, the one you read on the tab of your browser. To do it we need to use React.useEffect.

// src/index.js
function HelloWorld() {
  React.useEffect(() => {
    document.title = "Hello, World!";
  });

  return <h1>Hello, World!</h1>;
}

This useEffect function is called a hook, a function you can use to rely on React to do different things, in this case to run a side effect after the component is rendered in the screen.

The useEffect hook receives a function and execute it after each render of the component (if the component is updated it will run it again). In our function we are changing the document.title to the string Hello, World!.

Handling Events

One thing you will always need to do is to listen to events happening on the application and react to them, events like clicks, changes, submits, scroll, etc. In React we do that using onEventName where EventName is the name of the event, e.g. onClick, onChange, onSubmit, onMouseOver, etc.

// src/index.js
function HelloWorld() {
  React.useEffect(() => {
    document.title = "Hello, World!";
  });

  function handleChange(event) {
    console.log(event.target.value);
  }

  return (
    <main>
      <h1>Hello, World!</h1>
      <input type="text" defaultValue="Hello, World!" onChange={handleChange} />
    </main>
  );
}

We are now creating an input of type text with a default value Hello, World! and we will listen to the change event, when the input changes it will call our handleChange function and run the console.log(event.target.value).

Using State

But we don't usually want to only log the value, we want to keep it and use it elsewhere in our application, to do so we use another hook from React, this one is called React.useState and let us keep values in memory and change them when we need them, when a state changes the component is rendered again with the new value.

// src/index.js
function HelloWorld() {
  const [title, setTitle] = React.useState("HelloWorld");

  React.useEffect(() => {
    document.title = "HelloWorld";
  });

  function handleChange(event) {
    setTitle(event.target.value);
  }

  return (
    <main>
      <h1>HelloWorld</h1>
      <input type="text" value={title} onChange={handleChange} />
    </main>
  );
}

We are creating a new state and destructuring the resulting array in two elements, the first one title is the state value, the second one setTitle is a function React provides us to change the value of the state, we need to call it with the new state value.

In our input we changed defaultValue to value, this force the input to have our title state as value, that means it doesn't matter if the user writes something as long as the state doesn't change the input value will not change.

Here is where our handleChange works, it will read the new supposed value from the change event and pass it to setTitle to update the state, this will trigger a new render and update the input with the new value.

Using State & Effect together

Using the state only to keep track of the value of an input is ok but it's not something really useful, let's synchronize the state of the input with the title of the document. We can use our title state inside our useEffect hook and change the title of the document dynamically based on what the user wrote in the input.

// src/index.js
function HelloWorld() {
  const [title, setTitle] = React.useState("HelloWorld");

  React.useEffect(() => {
    document.title = title;
  });

  function handleChange(event) {
    setTitle(event.target.value);
  }

  return (
    <main>
      <h1>{title}</h1>
      <input type="text" value={title} onChange={handleChange} />
    </main>
  );
}

We could also use the value of the title state inside the <h1> to update it while the user is writing.

Adding a Second State & Effect

Now let's add a second state and effect, inside our component we could have as many states and effects as we want/need, the only rule is they can't be inside an condition or loop. Let's keep track if the user is currently writing, like Slack or Facebook does in their chats.

// src/index.js
function HelloWorld() {
  const [title, setTitle] = React.useState("Hello, World!");
  const [isWriting, setIsWriting] = React.useState(false);

  React.useEffect(() => {
    if (!isWriting) {
      document.title = title;
    }
  });

  React.useEffect(() => {
    setTimeout(() => setIsWriting(false), 1000);
  });

  function handleChange(event) {
    setIsWriting(true);
    setTitle(event.target.value);
  }

  return (
    <main>
      <h1>{title}</h1>
      <input type="text" value={title} onChange={handleChange} />
      User is writing: {isWriting.toString()}
    </main>
  );
}

We created a new state using React.useState and defaulted its value to false, the state we call it isWriting and the function to change it setIsWriting. We updated the original effect to only update the title of the document while the user is not writing.

Now we run a second effect where we are doing a setTimeout to update the isWriting state to false after a second. In the handleChange function we are changing both state, the isWriting to true and the title to the new content the user wrote.

At the end we added a single line to show in the UI if the user is writing, the .toString() is required to show the true or false as content.

Adding an Effect Dependencies Array

If we run the example above it's possible to see before first second it's working fine and then it starts to update the state without waiting for the user to stop writing. This is because both effects are running after each render.

We could pass a second argument to useEffect which is an array listing the values from outside the effect our effect depends on. In our case the first effect will use isWriting and title from state, that means it depends on the values of those states, while the second one depends only in the isWriting.

The idea of this array of dependencies is we could limit our effect to only run if those dependencies changed. If isWriting didn't changed the second effect will not run, if title didn't changed too then even the first effect will not run.

// src/index.js
function HelloWorld() {
  const [title, setTitle] = React.useState("Hello, World!");
  const [isWriting, setIsWriting] = React.useState(false);

  React.useEffect(() => {
    if (!isWriting) {
      document.title = title;
    }
  }, [isWriting, title]);

  React.useEffect(() => {
    setTimeout(() => setIsWriting(false), 1000);
  }, [isWriting]);

  function handleChange(event) {
    setIsWriting(true);
    setTitle(event.target.value);
  }

  return (
    <main>
      <h1>{title}</h1>
      <input type="text" value={title} onChange={handleChange} />
      User is writing: {isWriting.toString()}
    </main>
  );
}

Clearing an Effect

This is working a little bit better, but still we are seeing the title of the document change after one second. What we can do now is clear the timeout between each call of our effect.

Inside an effect it's possible to return a function which will be executed before the next run of that effect, this let us clear the results of the previously run effect. In our case we could use it to run clearTimeout.

// src/index.js
function HelloWorld() {
  const [title, setTitle] = React.useState("Hello, World!");
  const [isWriting, setIsWriting] = React.useState(false);

  React.useEffect(() => {
    if (!isWriting) {
      document.title = title;
    }
  }, [isWriting, title]);

  React.useEffect(() => {
    const timer = setTimeout(() => setIsWriting(false), 1000);
    return () => clearTimeout(timer);
  }, [isWriting]);

  function handleChange(event) {
    setIsWriting(true);
    setTitle(event.target.value);
  }

  return (
    <main>
      <h1>{title}</h1>
      <input type="text" value={title} onChange={handleChange} />
      User is writing: {isWriting.toString()}
    </main>
  );
}

Lifting State Up

So far we created a single component, if we keep adding functionality to that component it will start to grow until it's hard, if not impossible, to maintain and add new features.

We could avoid that splitting it in different components and compose them in a parent component.

// src/title.js
import React from "react";

function Title({ value, isWriting }) {
  React.useEffect(() => {
    if (!isWriting) {
      document.title = value;
    }
  }, [isWriting, value]);

  return <h1>{value}</h1>;
}

export default Title;

In our first component we move the <h1> and the effect to update the document's title to another component called Title. Our component will receive an object as first argument, this is called props and we can destructure it to read their properties, in our case value and isWriting.

// src/input.js
import React from "react";

function Input({ value, onWrite }) {
  React.useEffect(() => {
    const timer = setTimeout(() => onWrite(value), 1000);
    return () => clearTimeout(timer);
  }, [value, onWrite]);

  function handleChange(event) {
    onWrite(event.target.value);
  }

  return <input type="text" value={value} onChange={handleChange} />;
}

export default Input;

In our second component we move the <input />, the handleChange and the effect to set if it's writing to another component called Input. This will receive two values inside our prop, the value of the input, the same we receive in Title, and a function to change the value called onWrite.

We will call this function with event.target.value to update it when the user writes something and inside our effect after one second with the same value, this change will make sense in the next component.

// src/hello-world.js
import React from "react";

import Title from "./title";
import Input from "./input";

function HelloWorld() {
  const [title, setTitle] = React.useState("Hello, World!");
  const [isWriting, setIsWriting] = React.useState(false);

  function handleWrite(value) {
    setIsWriting(value !== title);
    setTitle(value);
  }

  return (
    <main>
      <Title value={title} isWriting={isWriting} />
      <Input value={title} onWrite={handleWrite} />
      User is writing: {isWriting.toString()}
    </main>
  );
}

export default HelloWorld;

Our latest component is our HelloWorld, this will import the Title and Input components and use them inside its return value sending value, isWriting and onWrite as props.

This component will also keep the states for title and isWriting, this is called "lift the state up", in our example those state are used inside our other component and our HelloWorld component too, because of this we can't move the value directly to the input since the data flow in React is single way from the top to the bottom of the component tree, we need to keep the state as near the top as required to be able to share the value, in our case that is HelloWorld.

Inside the handleWrite function we will update the value of title with the new received value and we will change isWriting to the result of the condition value !== title, this means if the value we received is the same as the current value we will set isWriting to false, if they are different we will set it to true.

With this we only need to render the HelloWorld component.

// src/index.js
import React from "react";
import { render } from "react-dom";

import HelloWorld from "./hello-world";

const $root = document.getElementById("root");
render(<HelloWorld />, $root);
Read next: Working with conditions & list in React

Learn how to conditionally render elements and how to properly work with lists in React.