DEV Community πŸ‘©β€πŸ’»πŸ‘¨β€πŸ’»

Zeyad Etman
Zeyad Etman

Posted on

Build Todo App with ReactJS Functional Components (hooks) and Class Components

Originally Published on my blog

Hello everyone, In this post we will write a todo app using class components and react hooks in functional components.
First of all, you haven't to re-write your projects to use hooks, you're completely free to use whatever you want, this post
to give you basic knowledge about react hooks and how to use this way.

Todo App

State

Let's start our app small with just input, this input will handle our new todo through the app.

In our class version, we create a class with constructor then create our state, that holds our variable todoInput,
then we write a render function and return our input element.

import React from 'react';

export default class TodoInput extends React.Component{
  constructor(props){
   super(props);

   this.state = {
    todoInput: '';
   }
  }

  render() {
    return <input placeholder="ex. drink milk" onChange={(e) => this.setState({todoInput: e.target.value})} />
  }
}
Enter fullscreen mode Exit fullscreen mode

useState hook

In our functional version, we just imported useState hook to manage our state - we can use it multiple times or use another
hook called useReducer in complex state explained later - simply useState hook takes one argument the initial state, and
return an array with two values the first is the current value, and the second is the function to set this state/value.

import React, { useState } from 'react';

export default function TodoInput() {
  const [todoInput, setTodoInput] = useState('');
  return <input placeholder="ex. drink milk" onChange={(e) => setTodoInput(e.target.value)} />
}
Enter fullscreen mode Exit fullscreen mode

If you're still confused about this line

const [todoInput, setTodoInput] = useState('');
Enter fullscreen mode Exit fullscreen mode

Waiting my next post about destructuring in JavaScript.

useReducer Hook

useReducer is a hook that takes two arguments callback function, and initial state, then return pair of values the state, and dispatch action that changes the state. The callback function takes two arguments, the current state, and action that dispatched from dispatch function, and returns the new state.
As we mentioned before, If we have a complex state management scenario, then useReducer will be great for this.
In our app, we need to create a state with all todos we've created and store it to localstorage, We need to intialize our todos state with its localstorage item, but in our first running there's no localstorage item for our state, its null so we need to check if it's null then initialize it with empty array. Also we need to handle remove and add todos so we can change our state and localstorage item.

In our class version:

  constructor(props) {
    super(props);

    this.state = {
      todos: JSON.parse(localStorage.getItem("todos")) || []
    };

    this.handleRemoveItem = this.handleRemoveItem.bind(this);
    this.handleAddTodo = this.handleAddTodo.bind(this);
  }

  handleRemoveItem(id) {
    const newTodos = this.state.todos.filter(todo => todo.id !== id);
    this.setState({ todos: [...newTodos] });
    localStorage.setItem("todos", JSON.stringify(this.state.todos));
  }

  handleAddTodo(todo) {
    if (this.state.todos) this.setState({ todos: [...this.state.todo, todo] });
    else this.setState({ todos: [todo] });
    localStorage.setItem("todos", JSON.stringify(this.state.todos));
  }
Enter fullscreen mode Exit fullscreen mode

Using useReducer Hook we'll do something like this:

import React, { useReducer } from "react";

const [todos, dispatch] = useReducer((state, action) => {
    if (action.type === "add") {
      if (state) state = [...state, action.value];
      else state = [action.value];
    } else if (action.type === "remove") {
      const newTodos = state.filter(todo => todo.id !== action.value);
      state = [...newTodos];
    }

    localStorage.setItem("todos", JSON.stringify(state));
    return state;
  }, JSON.parse(localStorage.getItem("todos")) || []);

  ...
  <label>
    Add new todo: <TodoInput handleAddTodo={(todo)=> { dispatch({ type: "add", value: todo }); }} />
  </label>
Enter fullscreen mode Exit fullscreen mode

Binding functions

Okay, until now we have known how to change our todo task (state), in the react lifecycle, But we need to save this todo
in somewhere so we can create multiple more than one todo. And here's what we're doing in our class component Binding.

Binding: Let's consider that you're getting a new mobile phone, and you need to view your data from your laptop, So you simply send them to your new laptop. Same with binding we have data called it this and need to send it from place to another.

in our class version we need to pass this from our entire class to our function, we write it like this

class App extends React.Component {
  constructor(props) {
    super(props);
    ...
    this.handleAddTodo = this.handleAddTodo.bind(this);
  }

  ...

  handleAddTodo(todo) {
    if (this.state.todos) this.setState({ todos: [...this.state.todo, todo] });
    else this.setState({ todos: [todo] });
    localStorage.setItem("todos", JSON.stringify(this.state.todos));
  }

  render() {
    return (
      <div className="App">
        <h1>Todo App React Class</h1>
        <label>
          Add new todo: <TodoInput handleAddTodo={this.handleAddTodo} />
        </label>
    ...
}
Enter fullscreen mode Exit fullscreen mode

But in our functional version, there's no this, So we create our function easily like this:

import React, { useState } from 'react';

export default function TodoInput() {
  ...
   const handleAddTodo = todo => {
    dispatch({ type: "add", value: todo });
  };

  return (
    <div className="App">
      <h1>Todo App React Hooks</h1>
      <label>
        Add new todo: <TodoInput handleAddTodo={handleAddTodo} />
      </label>
    ...
}
Enter fullscreen mode Exit fullscreen mode

Lifecycle

In Class version, we're using function like componentDidMount, componentWillUnmount, ...etc to handle state/props changes, But using hooks, React community found another concept, that every change in props/state is an effect to our component, So they create a new hook called useEffect that handle all the effects happened in the component (Lifecycle).

useEffect

useEffect is a hook that control the lifecycle or changes through the entire component, takes two arguments a call function and array of dependicies, the callback function will trigger at the early mount of component and if one of this dependencies change.

useEffect Vs componentDidMount

componentDidMount is one of react lifecycle that fires after the component mount, So if we want to display a welcome message when the user open the app, we can do this:

In class version we will use componentDidMount:

componentDidMount() {
    setTimeout(() => {
      this.setState({ showWelcomeMessage: false });
    }, 4000);
  }
Enter fullscreen mode Exit fullscreen mode

In functional version we'll use useEffect hooks, we said that it takes array of dependencies, but we want to trigger the function only the component mount then there's no dependencies, So :

useEffect(() => {
    setTimeout(() => {
      triggerWelcomeMessage(false);
    }, 4000);
  }, []);
Enter fullscreen mode Exit fullscreen mode

useEffect Vs componentWillUnmount

componentWillUnmount fires when we unmount the component, in our app we want to clear the storage when we close the app, and the main component of the app is App Tada :D

In class version

componentWillUnmount() {
  localStorage.removeItem("todos");
}
Enter fullscreen mode Exit fullscreen mode

In functional version we return this function in useEffect like this:

useEffect(() => {
    return () => {
      localStorage.removeItem("todos");
    };
  }, []);
Enter fullscreen mode Exit fullscreen mode

Note: we can use merge the previous hooks in one.

useContext Hook

You may want to share state between all of app components, theme for example, for this purpose we can use useContext to achieve this.
To do this, we have to create a context then wrap our app main component in it, then pass the state or whatever we want as a object to it, in our app we need to create a theme mode night/day mode, so we can write it like this:

// index.js

import React, { createContext } from "react";

export const TodoAppContext = createContext("");
function App() {
  ...
  const [themeMode, setThemeMode] = useState({ mode: "day" });

<TodoAppContext.Provider value={themeMode}>
      <div className={"App " + themeMode.mode}>
        <h1>
          Todo App React Hooks{" "}
          <button
            onClick={() => {
              setThemeMode(
                themeMode.mode === "day" ? { mode: "night" } : { mode: "day" }
              );
            }}
          >
            {themeMode.mode === "day" ? "night" : "day"}
          </button>
        </h1>
        ...
    </TodoAppContext.Provider>
Enter fullscreen mode Exit fullscreen mode

We can import our context in other component like this:

import { TodoAppContext } from "./index";

export default function Todos(props) {
  ...
  const theme = useContext(TodoAppContext);

  return (
    <ul className={theme.mode}>
Enter fullscreen mode Exit fullscreen mode

Now we have built a Todo app with reactjs hooks, and know about hook basics.
Here's the full code of functional version of our app:
https://github.com/zeyadetman/todo-app-hooks

And this for our class version:
https://github.com/zeyadetman/todo-app-class

Top comments (0)

πŸ” If you came here from Google, why not take a look at some of our top posts?