React JS: Lab#RE03-1
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
- Basic app creation:
- 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 theuseReducer
- 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 tasktask
: Task descriptionasignee
: Name of the person in charge of the taskdate
: 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 aDelete
button to remove it from thetodoState
- Input: a
Create a new project and install dependencies
Start coding your app
Initial values
First off, we’ll define a hardcoded list of initial To-Do to work with:
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:
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.