Before the journey begins. (Part 4)

Before the journey begins. (Part 4)

A "simple" TODO list.

Having added, in part 3 , the ability to have a list of items and to insert new ones, let's now move on to the rest of the operations provided for this app.

Let's start with the ability to modify one of the elements. By directly using the input we used in the EntryItem component to display the text, we can take advantage of the onChange property to save the new value. We then write the function we will need for the change in the App component.

const editListElement = (newText, elementPosition) => {
    const newList = [...list];

    newList.splice(elementPosition, 1, {
        ...list[elementPosition],
        title: newText
    });

    setList(newList);
}

The function will accept two arguments, the new text and the position of the element to be modified and via setList will modify the element in the list.

This will be passed via a new "handleEdit" property to the EntriesList component.

This in turn will pass a function as a property to each instance of the EntryItem component, which will invoke handleEdit by receiving the text and adding the required item location.

Later, when we introduce react-redux, we'll see how to avoid this "cascading" effect of function passes as properties by making the code "cleaner".

All that remains is to assign to the onChange property of the EntryItem input a function that will invoke handleEdit by passing the text. Before proceeding let's reason on the fact that onChange is an event that is triggered at each modification, thus starting the whole cycle of calls that culminates in the modification of the list and its new display. This is not at all optimal and could also lead to several problems if we were to link other events to the list modification. We solve this little inconvenience by "postponing" the invocation of the function; in this lodash comes to our aid with the "debounce" function.

This accepts the function to be invoked and the time in milliseconds to wait before invoking it. This way as long as onChange is invoked within the set time, sending the change will be delayed. Let's establish for now that half a second (500) is an acceptable value. We will have to make some small changes to the EntryItem component to make debounce do its job. Let's create an internal state that will handle the input

const [titleValue, setTitleValue] = useState(title);

We use the "useMemo" hook to create a function that takes advantage of debounce, otherwise at each new render the debounce will be "recreated" losing its initial purpose.

const debouncedHandleTitle = useMemo(() => debounce(handleEdit, 500), [handleEdit]);

After that we pass to the onChange event of the input, a function that will immediately set the change to the internal state of the component and will invoke the change of the element in the list only when no changes will be made to the text for at least half a second (the "acceptable" time established)

const handleChange = event => {
    setTitleValue(event.target.value)
    debouncedHandleTitle(event.target.value);
};
<FormControl
    disabled={completed}
    value={titleValue}
    className={completed ? 'text-decoration-line-through' : ''}
    onChange={handleChange}
/>

We can go ahead and move on to the delete and mark operation for the single element. For both of them we're going to create respectively the function "removeListElement" and "markListElement". The first one will set the "archived" index of the chosen element to true, the second one will set the "taken" index of the element denying the previous status.

const removeListElement = (elementPosition) => {
    const newList = [...list];

    newList[elementPosition].archived = true;

    setList(newList);
}

const markListElement = (elementPosition) => {
    const newList = [...list];
    newList[elementPosition].taken = !newList[elementPosition].taken;
    setList(newList);
}

We pass the functions to the EntriesList component which, as with editListElement, will pass them to each individual EntryItem instance.

<EntriesList
    list={list}
    handleEdit={editListElement}
    handleRemove={removeListElement}
    handleMark={markListElement}
/>

With that done as well, all that remains is to implement the filter. This will be based on the "taken" property.

To achieve this we will implement two states, one to keep track of the applied filter (initialized with null) and the other to record which keys are obtained by the filter (initialized with an empty array).

const [takenFilter, setTakenFilter] = useState(null);
const [filteredList, setFilteredList] = useState([]);

We introduce at this point another hook "useEffect" that will be extremely useful to manage these two new states in a non-invasive way for the involved components.

To keep the list of filtered keys up to date we should keep in mind not only the filter status but every operation that happens on the list (inserting a new item, marking an item in the list) and at every new operation inserted in our app we should "remember" to manage the filter. Since laziness is one of my greatest assets, we're going to use a "do it once and forget you did it" method.

The "useEffect" hook allows us to "observe" variables that we consider to be "dependent" and invoke a predefined function whenever they change their state. So it is that through this powerful hook we will observe both the list and the filter, and should either of them change for any of the operations we have implemented or will implement in the future, a new filtered list will be created.

useEffect(() => {
    if (typeof takenFilter === 'boolean') {
        setFilteredList(keys(list).filter(index => list[index].taken === takenFilter));
    } else {
        setFilteredList(keys(list));
    }
}, [takenFilter, list])

We'll pass the filtered list to the EntriesList component so that it can display only the filtered items by mapping their indexes instead of the entire list.

map(filtered,
    (index) => {
        const {archived, taken, title} = list[index];
        return (
            !archived &&
            <EntryItem
                key={index}
                title={title}
                completed={taken}
                handleEdit={(text) => handleEdit(text, index)}
                handleRemove={() => handleRemove(index)}
                handleMark={() => handleMark(index)}
            />
        );
    }
)

Thus, we have completed the "basic" operations necessary for our TODO list.

In the last part we'll make the list "persistent", do some code and UI improvements, and finally I'll be able to write the list of tools to take on my journey!

As always, you can find all the code at github repo.

git clone -b v3.0 https://github.com/MaxLau88/fsd-todo-app.git

See you next time!