React useState Hook

react use state hook

There has been a paradigm shift in how React works with regard to state, classes, and functions. React now has a feature called hooks, and it looks like a game changer. From reading the react documentation, and watching React Conf, it looks like us web developers should become familiar with Hooks in React since they make for cleaner React code. Class components are still supported, but it looks like Hooks in combination with Function Components are the favored approach moving forward.


Introducing useState()

The useState() hook is the first one to learn about and to use it we import it into a file like so.

import React, { useState } from 'react';

The useState() function is a Hook that lets you add state to Function Components in React. Prior to this, only Class Components could use state.


Setting A State Variable

We can use this hook to assign a state variable. Imagine we were to create a Todo application, we could put useState() to work like this.

import React, { useState } from "react";

function App() {
  const [todos, setTodos] = useState([
    { text: "Learn React Hooks", isCompleted: false },
    { text: "Take a Drive", isCompleted: false },
    { text: "Play some games", isCompleted: false }
  ]);

  console.log(todos);
  console.log(setTodos);

  return <div />;
}

export default App;

When you call useState(), it returns two things. The current state, and a function you can use to update that state. In the code above, an array of three objects is assigned to the todos variable, and a function which can update that state is stored in setTodos.

useState() Returns a stateful value, and a function to update it.

useState return values

We can see in the console the result of logging out todos and setTodos. The first holds that array of three objects, and the second holds a function as we can see by the f character.

So let’s go ahead and render out those three objects we have stored in the todos variable. We learned that when creating a list with react we can use the map function like so.

import React, { useState } from "react";

function App() {
  const [todos, setTodos] = useState([
    { text: "Learn React Hooks", isCompleted: false },
    { text: "Take a Drive", isCompleted: false },
    { text: "Play some games", isCompleted: false }
  ]);

  return (
    <React.Fragment>
      {todos.map((todo, index) => (
        <div key={index} className="card mb-3">
          <div className="card-body">
            <h5 className="card-title">{todo.text}</h5>
          </div>
        </div>
      ))}
    </React.Fragment>
  );
}

export default App;

We now have three things to do!
learn react hooks


Todo Function Component

Let’s move the markup to render out the todo items to a dedicated component. Create a new file named todo.jsx in src/components and add this code.

import React from "react";

function Todo({ todo, index }) {
  return (
    <React.Fragment>
      <div key={index} className="card mb-3">
        <div className="card-body">
          <h5 className="card-title">{todo.text}</h5>
        </div>
      </div>
    </React.Fragment>
  );
}

export default Todo;

Now in the main App.js file, make the highlighted updates.

import React, { useState } from "react";
import Todo from "./components/todo";

function App() {
  const [todos, setTodos] = useState([
    { text: "Learn React Hooks", isCompleted: false },
    { text: "Take a Drive", isCompleted: false },
    { text: "Play some games", isCompleted: false }
  ]);

  return (
    <React.Fragment>
      {todos.map((todo, index) => (
        <Todo todo={todo} key={index} />
      ))}
    </React.Fragment>
  );
}

export default App;

Now we still have the three todo items rendered on the page, but are using to Function Components to do so.


Add A Text Input

We need more things to do. For that, we can create a new component which will hold a form and text input field to add a new todo. Soon we will see how to update the state! Add a new file named todoform.jsx and add it to src/components.

import React, { useState } from "react";

function TodoForm() {
  const [value, setValue] = useState("");

  return (
    <form>
      <input
        type="text"
        className="form-control"
        value={value}
        onChange={e => setValue(e.target.value)}
        placeholder="Type then hit *Enter*"
      />
    </form>
  );
}

export default TodoForm;

Now we can import and render it in the main App.js file.

import React, { useState } from "react";
import Todo from "./components/todo";
import TodoForm from "./components/todoform";

function App() {
  const [todos, setTodos] = useState([
    { text: "Learn React Hooks", isCompleted: false },
    { text: "Take a Drive", isCompleted: false },
    { text: "Play some games", isCompleted: false }
  ]);

  return (
    <React.Fragment>
      {todos.map((todo, index) => (
        <Todo todo={todo} key={index} />
      ))}
      <TodoForm />
    </React.Fragment>
  );
}

export default App;


Calling setTodos()

We are ready to update the state of our todos variable. We can raise an event in the <TodoForm /> component, and then handle that event in the <App /> component. We can start in App.js by passing a prop as an attribute on <TodoForm /> along with defining the handleAddTodo() event handler. Both are highlighted here.

import React, { useState } from "react";
import Todo from "./components/todo";
import TodoForm from "./components/todoform";

function App() {
  const [todos, setTodos] = useState([
    { text: "Learn React Hooks", isCompleted: false },
    { text: "Take a Drive", isCompleted: false },
    { text: "Play some games", isCompleted: false }
  ]);

  const handleAddTodo = text => {
    const newToDos = [...todos, { text }];
    setTodos(newToDos);
  };

  return (
    <React.Fragment>
      {todos.map((todo, index) => (
        <Todo todo={todo} key={index} />
      ))}
      <TodoForm onAddTodo={handleAddTodo} />
    </React.Fragment>
  );
}

export default App;

At this point the form has an onSubmit action. This means when the user hits the “Enter” key, the value of the form will be passed to the handleSubmit function. Inside of that function an onAddTodo event is raised. We have access to that since it was passed in to the component as a prop. When this event is raised, the handleAddTodo function runs in App.js. At that point, setTodos() is called, and the state is updated!


Mark A Todo As Complete

Now lets add a button to the <Todo /> component to be able to mark an item as complete. There are a few steps to do this. First, we can add the button to the todo.jsx component file.

import React from "react";

function Todo({ todo, index, onComplete }) {
  return (
    <React.Fragment>
      <div key={index} className="card mb-3">
        <div className="card-body">
          <h5
            className="card-title"
            style={{ textDecoration: todo.isCompleted ? "line-through" : "" }}
          >
            {todo.text}
          </h5>
          <button onClick={() => onComplete(index)} className="btn btn-success">
            Mark Complete
          </button>
        </div>
      </div>
    </React.Fragment>
  );
}

export default Todo;

Above are the important highlights. On line 3, we need to accept the onComplete prop into the component. On line ten, the familiar use of a ternary operator is put to work in order to decide if a line through will be applied to the text of the todo item. Lastly, you can see the new button which when clicked raises that onComplete event while also accepting the index as an argument. That index is needed to know which todo item has been completed. Now we need the following updates to App.js.

import React, { useState } from "react";
import Todo from "./components/todo";
import TodoForm from "./components/todoform";

function App() {
  const [todos, setTodos] = useState([
    { text: "Learn React Hooks", isCompleted: false },
    { text: "Take a Drive", isCompleted: false },
    { text: "Play some games", isCompleted: false }
  ]);

  const handleAddTodo = text => {
    const newToDos = [...todos, { text }];
    setTodos(newToDos);
  };

  const handleComplete = index => {
    const newToDos = [...todos];
    newToDos[index].isCompleted = true;
    setTodos(newToDos);
  };

  return (
    <React.Fragment>
      {todos.map((todo, index) => (
        <Todo
          todo={todo}
          key={index}
          onComplete={handleComplete}
          index={index}
        />
      ))}
      <TodoForm onAddTodo={handleAddTodo} />
    </React.Fragment>
  );
}

export default App;

There is a new event handler named handleComplete() in the component. This event handler runs when a user clicks the button and raises the onComplete event. Once again, we see that the setTodos() function is called inside of handleComplete() in order to update the state. Lastly, notice that the embedded <Todo /> has two new attributes of onComplete and index. That is the reason we can access them over in todo.jsx. This is a sometimes tricky part of React, and that is keeping track of the data sharing between components. So let’s see that in action now.


Mark a Todo as Unfinished

Following the convention we used above, it should now be easy to implement a button which when clicked marks a todo as unfinished.
todo.jsx

import React from "react";

function Todo({ todo, index, onComplete, onUnfinished }) {
  return (
    <React.Fragment>
      <div key={index} className="card mb-3">
        <div className="card-body">
          <h5
            className="card-title"
            style={{ textDecoration: todo.isCompleted ? "line-through" : "" }}
          >
            {todo.text}
          </h5>
          <button onClick={() => onComplete(index)} className="btn btn-success">
            Mark Complete
          </button>{" "}
          <button
            onClick={() => onUnfinished(index)}
            className="btn btn-secondary"
          >
            Mark Unfinished
          </button>
        </div>
      </div>
    </React.Fragment>
  );
}

export default Todo;

App.js

import React, { useState } from "react";
import Todo from "./components/todo";
import TodoForm from "./components/todoform";

function App() {
  const [todos, setTodos] = useState([
    { text: "Learn React Hooks", isCompleted: false },
    { text: "Take a Drive", isCompleted: false },
    { text: "Play some games", isCompleted: false }
  ]);

  const handleAddTodo = text => {
    const newToDos = [...todos, { text }];
    setTodos(newToDos);
  };

  const handleComplete = index => {
    const newToDos = [...todos];
    newToDos[index].isCompleted = true;
    setTodos(newToDos);
  };

  const handleUnfinished = index => {
    const newToDos = [...todos];
    newToDos[index].isCompleted = false;
    setTodos(newToDos);
  };

  return (
    <React.Fragment>
      {todos.map((todo, index) => (
        <Todo
          todo={todo}
          key={index}
          onComplete={handleComplete}
          index={index}
          onUnfinished={handleUnfinished}
        />
      ))}
      <TodoForm onAddTodo={handleAddTodo} />
    </React.Fragment>
  );
}

export default App;

Now we can also mark a todo as unfinished.


Delete A Todo

To round out the application, we can add one more button to allow for deleting a todo. Once again, this follows the same pattern.
App.js

import React, { useState } from "react";
import Todo from "./components/todo";
import TodoForm from "./components/todoform";

function App() {
  const [todos, setTodos] = useState([
    { text: "Learn React Hooks", isCompleted: false },
    { text: "Take a Drive", isCompleted: false },
    { text: "Play some games", isCompleted: false }
  ]);

  const handleAddTodo = text => {
    const newToDos = [...todos, { text }];
    setTodos(newToDos);
  };

  const handleComplete = index => {
    const newToDos = [...todos];
    newToDos[index].isCompleted = true;
    setTodos(newToDos);
  };

  const handleUnfinished = index => {
    const newToDos = [...todos];
    newToDos[index].isCompleted = false;
    setTodos(newToDos);
  };

  const handleDeleteTodo = index => {
    const newToDos = [...todos];
    newToDos.splice(index, 1);
    setTodos(newToDos);
  };

  return (
    <React.Fragment>
      {todos.map((todo, index) => (
        <Todo
          key={index}
          index={index}
          todo={todo}
          onComplete={handleComplete}
          onUnfinished={handleUnfinished}
          onDeleteTodo={handleDeleteTodo}
        />
      ))}
      <TodoForm onAddTodo={handleAddTodo} />
    </React.Fragment>
  );
}

export default App;

todo.jsx

import React from "react";

function Todo({ todo, index, onComplete, onUnfinished, onDeleteTodo }) {
  return (
    <React.Fragment>
      <div className="card mb-3">
        <div className="card-body">
          <h5
            className="card-title"
            style={{ textDecoration: todo.isCompleted ? "line-through" : "" }}
          >
            {todo.text}
          </h5>
          <button onClick={() => onComplete(index)} className="btn btn-success">
            Mark Complete
          </button>{" "}
          <button
            onClick={() => onUnfinished(index)}
            className="btn btn-secondary"
          >
            Mark Unfinished
          </button>{" "}
          <button
            onClick={() => onDeleteTodo(index)}
            className="btn btn-danger"
          >
            Delete Todo
          </button>
        </div>
      </div>
    </React.Fragment>
  );
}

export default Todo;

The final result allows you to add a todo, mark a todo as complete, mark a todo as unfinished, and delete a todo.


Learn More about useState


React useState Hook Summary

In this tutorial we had a look at the new useState() hook in React. In doing so, we build a little todo application. The useState() function now allows web developers to add state handling capability to Function Components, whereas in the past only Class Components had such ability. In addition, this new approach does away with the confusing nature of using this in JavaScript.