BBV logo

Making a beautiful Todo App using React Hooks + Material UI

How to start?

First of all, we’ll start by creating a new CodeSandbox.

We’ll decompose the Todo App into 4 components:

  • Layout
  • AddTodo
  • TodoListItem
  • TodoList

Because we don’t want all these components to re-render unless it’s really necessary we’ll wrap them using memo helper from React.


Let’s develop the Layout

Our Layout component will only render a Toolbar with our App name and will be able to receive children components.

import React, { memo } from 'react';
import { AppBar, Toolbar, Typography, Paper } from '@material-ui/core';

const Layout = memo(props => (
  <Paper
    elevation={0}
    style={{ padding: 0, margin: 0, backgroundColor: '#fafafa' }}
  >
    <AppBar color="primary" position="static" style={{ height: 64 }}>
      <Toolbar style={{ height: 64 }}>
        <Typography color="inherit">TODO APP</Typography>
      </Toolbar>
    </AppBar>
    {props.children}
  </Paper>
));

export default Layout;

Let’s develop the AddTodo

Our AddTodo component will render a Paper component wrapping an input and a button, both of them will receive some event handlers passed as props from a Parent component.

import React, { memo } from 'react';
import { TextField, Paper, Button, Grid } from '@material-ui/core';

const AddTodo = memo(props => (
  <Paper style={{ margin: 16, padding: 16 }}>
    <Grid container>
      <Grid xs={10} md={11} item style={{ paddingRight: 16 }}>
        <TextField
          placeholder="Add Todo here"
          value={props.inputValue}
          onChange={props.onInputChange}
          onKeyPress={props.onInputKeyPress}
          fullWidth
        />
      </Grid>
      <Grid xs={2} md={1} item>
        <Button
          fullWidth
          color="secondary"
          variant="outlined"
          onClick={props.onButtonClick}
        >
          Add
        </Button>
      </Grid>
    </Grid>
  </Paper>
));

export default AddTodo;

Let’s develop the TodoListItem

Our TodoListItem component will render a ListItem wrapping a checkbox, a text container and a button as children elements.

import React, { memo } from 'react';

import {
  List,
  ListItem,
  Checkbox,
  IconButton,
  ListItemText,
  ListItemSecondaryAction,
} from '@material-ui/core';
import DeleteOutlined from '@material-ui/icons/DeleteOutlined';

const TodoListItem = memo(props => (
  <ListItem divider={props.divider}>
    <Checkbox
      onClick={props.onCheckBoxToggle}
      checked={props.checked}
      disableRipple
    />
    <ListItemText primary={props.text} />
    <ListItemSecondaryAction>
      <IconButton aria-label="Delete Todo" onClick={props.onButtonClick}>
        <DeleteOutlined />
      </IconButton>
    </ListItemSecondaryAction>
  </ListItem>
));

export default TodoListItem;

Let’s develop the TodoList

Our TodoList component will render only when there’s an item added to the list. It will be in charge of rendering TodoListItem components.

import React, { memo } from 'react';
import { List, Paper, Grid } from '@material-ui/core';

import TodoListItem from './TodoListItem';

const TodoList = memo(props => (
  <>
    {props.items.length > 0 && (
      <Paper style={{ margin: 16 }}>
        <List style={{ overflow: 'scroll' }}>
          {props.items.map((todo, idx) => (
            <TodoListItem
              {...todo}
              key={`TodoItem.${idx}`}
              divider={idx !== props.items.length - 1}
              onButtonClick={() => props.onItemRemove(idx)}
              onCheckBoxToggle={() => props.onItemCheck(idx)}
            />
          ))}
        </List>
      </Paper>
    )}
  </>
));

export default TodoList;

Let’s move to the logic

So, dude, we’ve all the components done. It’s time for us to think about the logic and here, we’ll make all the logic using custom React Hooks (Hell yeah!).

We’ll decompose the Todo App logic into 2 variants:

  • Input state management
  • Todos state management

Let’s develop the Input State logic

The input state will have a single value that will be a string, our custom hook will return that value, and also, a set of custom functions to handle change, reset, and input events.

import { useState } from 'react';

export const useInputValue = (initialValue = '') => {
  const [inputValue, setInputValue] = useState(initialValue);

  return {
    inputValue,
    changeInput: event => setInputValue(event.target.value),
    clearInput: () => setInputValue(''),
    keyInput: (event, callback) => {
      if (event.which === 13 || event.keyCode === 13) {
        callback(inputValue);
        return true;
      }

      return false;
    },
  };
};

Let’s develop the Todos State logic

The todos state will have a single value that will be an array, our custom hook will return that value, and also, a set of custom functions to handle adding, checking, and removing todos.

import { useState } from 'react';

export const useTodos = (initialValue = []) => {
  const [todos, setTodos] = useState(initialValue);

  return {
    todos,
    addTodo: text => {
      if (text !== '') {
        setTodos(
          todos.concat({
            text,
            checked: false,
          })
        );
      }
    },
    checkTodo: idx => {
      setTodos(
        todos.map((todo, index) => {
          if (idx === index) {
            todo.checked = !todo.checked;
          }

          return todo;
        })
      );
    },
    removeTodo: idx => {
      setTodos(todos.filter((todo, index) => idx !== index));
    },
  };
};

Connecting all pieces together

We’ve the components, and also the logic. Let’s mix them to make our App work:

import './styles.css';

import React, { memo } from 'react';
import ReactDOM from 'react-dom';

import { useInputValue, useTodos } from './custom-hooks';

import Layout from './components/Layout';

import AddTodo from './components/AddTodo';
import TodoList from './components/TodoList';

const TodoApp = memo(props => {
  const { inputValue, changeInput, clearInput, keyInput } = useInputValue();
  const { todos, addTodo, checkTodo, removeTodo } = useTodos();

  const clearInputAndAddTodo = _ => {
    clearInput();
    addTodo(inputValue);
  };

  return (
    <Layout>
      <AddTodo
        inputValue={inputValue}
        onInputChange={changeInput}
        onButtonClick={clearInputAndAddTodo}
        onInputKeyPress={event => keyInput(event, clearInputAndAddTodo)}
      />
      <TodoList
        items={todos}
        onItemCheck={idx => checkTodo(idx)}
        onItemRemove={idx => removeTodo(idx)}
      />
    </Layout>
  );
});

ReactDOM.render(<TodoApp />, document.getElementById('root'));

Check the results

Let’s see our Todo App up and running: