React JS: Lab#RE03-1

React
Router
Hooks
API
axios
semantic
Creating a TO-DO app
Author

ProtossGP32

Published

May 9, 2023

Introduction

We’ll create a To-Do app that uses almost all of the React hooks that we worked with on previous labs.

We’ll be using:

  • react-router-dom: used for bindings when useing React Router in web applications
  • Semantinc React: library to paing some CSS:
  • HighCharts: library for data representation
  • Hooks:
    • Basic app creation: useReducer, useContext
    • State value persistence between renders: useEffect, useRef
    • States or variables management: useState
  • Axios, REST API and Database storage:
    • Once the basic app is done, we’ll add API communication and Database storage
  • Local storage: we can use the device local storage to store the data between renders

Getting started

User-story and mock-up

A basic implementation of the To-Do app manages data and has a basic business logic to manage this data (create entries, update entries, delete entries, etc…).

  • Our to-do data will be stored in the state from the useReducer
  • When dealing with render-cycle complexity, we’ll use useRef to keep track of our variables state
  • When additional components are included as children, then we’ll use useContext to deal with components communication. Remember the steps:
    • Define your context
    • Choose your provider (upmost component)
    • Define its consumers (selected children)

Our architecture (pseudo-code) should look something like this:

Architecture pseudo-code
export default function TodoList() {
    // Define an initial hardcoded ToDo state, just for testing purposes
    const initialTodoState = [
        {
            id: 0,
            task: "task 1",
            assignee: "User 1",
            date: "2023/05/09",
            completed: false
        },
        {
            id: 1,
            task: "task 2",
            assignee: "User 2",
            date: "2023/05/09",
            completed: true
        }
    ];
    // Define a useReduce to deal with the TODO data
    const [ todoState, todoDispatch ] = useReducer(todoReducer, initialTodoState);

    // Render the component
    return (
        <>
        <h2>To-Do table</h2>
        {/* Iterate over the todoState and create a formatted Item for each available entry */}
        {todoState.map((todoEntry) => {
            // Call the Item component with the 'entry' as props (pending to define)
            <Item todoEntry/>
        })}
        </>
    );
}

Once defined the pseudo-code, we can make a list of the required components to design and hooks to use:

  • <Item />: returns the rendered todo item
    • Input: a todoEntry object that contains the following fields:
      • id: ID of the task
      • task: Task description
      • asignee: Name of the person in charge of the task
      • date: Some date field (creation? due?)
      • completion: boolean value that defines the task completion
    • Output: an HTML rendered table entry of the input todoEntry along with a Delete button to remove it from the todoState

Create a new project and install dependencies

Create project and install packages
# Create the new project
npx create-react-app lab-re03-1
# Install Router, Axios and Semantic
npm install axios semantic-ui-react semantic-ui-css react-router-dom

Start coding your app

Initial values

First off, we’ll define a hardcoded list of initial To-Do to work with:

const initialTodoState = [
        {
            id: 0,
            task: "task 1",
            assignee: "User 1",
            date: "2023/05/09"
            completed: false
        },
        {
            id: 1,
            task: "task 2",
            assignee: "User 2",
            date: "2023/05/09"
            completed: true
        }
    ];

Expected return

Then, we define the return of our TodoFake:

return (
    <>
        <h1>To-Do Fake - hardcoded list of tasks</h1>
        <TodoList receivedTodoState={initialTodoState} />
    </>
);

where: - TodoList would be the component that handles the To-Do list rendering based on an initial todo state. Params: - receivedTodoState: the initial To-Do state values

TodoList component

Let’s develop the TodoList component:

export default function TodoList({receivedTodoState}) {
    const [todoState, todoDispatch] = useReducer(todoReducer, receivedTodoState);
    return (
        <>
            <button onClick={() => {
                todoDispatch({ type: 'add' })
            }}>New To-Do entry</button>
            <List>
                {todoState.map((todoEntry) => {
                    return(
                        <TodoItem todoEntry={todoEntry} />
                    );
                })}    
            </List>
        </>
    );
}

Things that we’ve introduced here: - The useReducer hook to deal with our To-Do entries - A button called New To-do entry that calls the useReducer dispatch (todoDispatch) so a new entry is added to the todoState - A new component called TodoItem that manages the formatting of each To-Do entry

useReducer hook

Let’s continue with the useReducer, then:

  • Its reducer function should be something like this:

    Reducer function for To-do app
    function todoReducer(todoState, todoAction) {
        switch(todoAction.type) {
            case "add": {
                return [
                    ...todoState,
                    // Add a new task at the end of the array
                    newTask()
                ];
            }
            case "delete": {
                // TODO: return the todoState without the current entry
                // Use the todoAction.payload to identify which entry to delete from the todoState
                return todoState;
            }
            default: {
                return todoState;    
            }
        };
    }
  • Here we introduce an auxiliar newTask function that simply returns a JSON object with empty values for the expected To-Do entry keys:

    Auxiliar newTask function
    function newTask() {
        return {
            id: Date.now(),
            task: "",
            assignee: "",
            date: Date.now(),
            completed: false
        }
    }

TodoItem component

The TodoItem should return a CSS-formatted entry based on the provided todoEntry. It should allow the user to modify the tasks fields as well as delete it from the todoState:

Tentative todoItem component
function TodoItem({todoEntry}) {
    return (
        <List.Item>
            {/** TODO: Add images to each entry */}
            <Image avatar src='https://react.semantic-ui.com/images/avatar/small/rachel.png'/>
            <List.Content>
                <List.Header as='a'><b>Task: </b><Input placeholder="Enter task" value={todoEntry.task} /></List.Header>
                <List.Description>
                    <b>Assignee: </b><Input placeholder="Enter assignee" value={todoEntry.assignee} />
                    <br/>
                    <b>Date: </b><Input type="date" value={todoEntry.date} />
                    <br/>
                    <Checkbox label="Completed" value={todoEntry.completed} />
                </List.Description>
            </List.Content>
            <Button onClick={() => {
                // ERROR: TodoItem doesn't have visibility of the todoDispatch!!
                // We provide a 'payload' key with the ID value to delete that entry from the todoState
                todoDispatch({ type: 'delete', payload: todoEntry.id })
            }} icon labelPosition="right">
                Delete
                <Icon name="trash" />
            </Button>
        </List.Item>
    );
}

But here we have a problem: todoDispatch called by the Delete button isn’t on the same scope/context as the TodoItem component!!

We solve this issue by creating a context where we’ll pass the todoDispatch

Final result:

TodoList.jsx
import { React, useContext, useReducer, createContext } from "react";
import { List, Image, Input, Checkbox, Button, Icon } from "semantic-ui-react";

// Define a Context Provider to deal with initial states and reducer dispatch functions
const TodoContext = createContext();

// Define a function that handle fields update
function handleFieldUpdate(todoDispatch, itemId, itemField, itemValue) {
    // This function shall call the reducer dispatches
    todoDispatch({
        type: "updateField",
        payload: {
            "id": itemId,
            "field": itemField,
            "value": itemValue 
        }
    });

}

// Define the function to generate list items
function TodoItem({todoEntry}) {
    const todoDispatch = useContext(TodoContext)
    return (
        <List.Item>
            {/** TODO: Add images to each entry */}
            <Image avatar src='https://react.semantic-ui.com/images/avatar/small/rachel.png'/>
            <List.Content>
                <List.Header as='a'><b>Task: </b><Input placeholder="Enter task" onChange={(event) => handleFieldUpdate(todoDispatch, todoEntry.id, "task", event.target.value)} value={todoEntry.task} /></List.Header>
                <List.Description>
                    <b>Assignee: </b><Input placeholder="Enter assignee" onChange={(event) => handleFieldUpdate(todoDispatch, todoEntry.id, "assignee", event.target.value)} value={todoEntry.assignee} />
                    <b>Date: </b><Input type="date" onChange={(event) => handleFieldUpdate(todoDispatch, todoEntry.id, "date", event.target.value)} value={todoEntry.date} />
                    <Checkbox label="Completed" onChange={() => todoDispatch({ type: "completed", payload: todoEntry.id })} checked={todoEntry.completed} />
                    <Button onClick={() => {
                        todoDispatch({ type: 'delete', payload: todoEntry.id })
                        return;
                    }} icon labelPosition="right">
                        Delete
                        <Icon name="trash" />
                    </Button>
                </List.Description>
            </List.Content>

        </List.Item>
    );
}

function newTask() {
    return {
        id: Date.now(),
        task: "",
        assignee: "",
        date: Intl.DateTimeFormat("az", {
            day: "2-digit",
            month: "2-digit",
            year: "numeric"
        }).format((Date.now())),
        completed: false
    }
}

// Define the reducer function
function todoReducer(todoState, todoAction) {
    switch(todoAction.type) {
        case "add": {
            return [
                ...todoState,
                // Add a new task at the end of the array
                newTask()
            ];
        }
        case "delete": {
            // Filter the todoState map by the provided todoAction.payload
            return todoState.filter((todoEntry) => todoEntry.id !== todoAction.payload);
        }
        case "completed": {
            // TODO: change the completed value of the provided ID item from true to false
            return todoState.map((item) => {
                // If the item ID matches the payload, change its completed value and return it
                if (item.id === todoAction.payload) {
                    return {
                        ...item,
                        completed: !item.completed
                    }
                }
                // Else, return the item without changes
                return item;
            });
        }
        case "updateField": {
            // Distructure the payload into the expected values
            const { id, field, value } = todoAction.payload;
            // Return all of the todoState items
            return todoState.map((item) => {
                // If the item ID matches the received ID in the payload, change its defined value and return it
                if (item.id === id) {
                    return {
                        ...item,
                        //https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Object_initializer#computed_property_names
                        [field]: value
                    };
                }
                // Else, return the item without changes
                return item;
            });
            
        }
        case "reset": {
            // TODO: reset the state to the initially provided To-Do
            return [
                // Empty string
            ];
        }
        // TODO: add the text fields update cases (task, assignee, date, etc...)
        default: {
            return todoState;    
        }
    };
}

export default function TodoList({receivedTodoState}) {
    // Define the useReducer to manage the entries of our To-Do list
    const [todoState, todoDispatch] = useReducer(todoReducer, receivedTodoState);

    return (
        <TodoContext.Provider value={todoDispatch} >
            {/* Create an "New To-Do entry" to add entries to the todoState */}
            <button onClick={() => { todoDispatch({ type: 'add' }) }}>New To-Do entry</button>
            {/* Create an "New To-Do entry" to add entries to the todoState */}
            <button onClick={() => { todoDispatch({ type: 'reset' }) }}>Reset To-Do list</button>
            {/* Use the Semantic List component to compose our To-Do list*/}
            <List animated verticalAlign='middle'>
                {todoState.map((todoEntry) => {
                    return(
                        // Delegate the entries rendering to the TodoItem component
                        // Important!! Each unique item in a list must have its own 'key'
                        <TodoItem  key={todoEntry.id} todoEntry={todoEntry} />
                    );
                })}    
            </List>
        </TodoContext.Provider>
    );
}

To-Do list with data fetched from an API

Follow-up on Lab#RE03-3.