Wie wird eine ToDo-Liste mit React gecodet

28.05.2021 Ricky Elfner
Mobile React Open source JavaScript Frontend Framework Hands-on Tutorial How-to

Falls Sie unseren b-nova Blog verfolgen, haben Sie sicher bereits gesehen, dass es jeweils ein To-Do List Tutorial für Angular gibt und eines für Vue.js. Deshalb wollen wir Ihnen als Vergleichsmöglichkeit auch noch ein Tutorial mit React nahe legen. Wenn Sie bereits grundlegende Erfahrungen mit React haben, können Sie direkt starten, anderenfalls empfehlen wir Ihnen den Artikel Grundlagen mit React.

Als Setup für diese ToDo-Liste wird jenes verwendet, welches mittels create-react-app zur Verfügung gestellt wird. Sobald dies installiert ist, können sie wie folgt ihre App erstellen:

npx create-react-app b-nova-todo-list

Anschliessend können Sie mit code . VS Code öffnen und mit npm start den Server starten. Des Weiteren können Sie alle Files löschen, damit Sie nur noch die folgende Ordnerstruktur haben.

Ebenfalls können Sie den gesamten Inhalt render-Methode in dem App.js File entfernen. Auch ist es im index.html-File nur notwendig eine gültige HTML-Struktur für eine Seite zu haben, sowie ein div mit der Id root.

Die Liste

Als erste Komponente können Sie die ToDoList.jsx erstellen. Diese soll alle ToDoItems in einer Liste darstellen. Für all unsere Komponenten benutzen wir eine Functions Component.

import React from 'react';

const ToDoList = ({ toDoItems }) => {

    return (
        <ul>
            {toDoItems.map(({id, content, complete}) => (
                <Item id={id} content={content} complete={complete}/>
            ))}
        </ul>
    );
}

export default ToDoList;

Die Properties toDoItems sollen später den Items entsprechen, welche der Liste übergeben werden. Dabei soll ToDoItems eine bestimmte Struktur haben, diese wird mithilfe von PropTypes fest gelegen. Ein weiterer Vorteil ist, dass Sie dadurch bestimmen können, welchen Typ ein Attribut haben soll und ob dieses vorhanden sein muss oder nicht. Um PropTypes nutzen zu können, muss dieses Package über das Terminal installiert werden.

npm install --save-dev prop-types

Nach der Installation können Sie PropTypes innerhalb Ihrer Komponenten importieren. In diesem Beispiel wird dies zunächst innerhalb von ToDoList.jsx gemacht.

import PropTypes from 'prop-types';

Zwischen der letzten geschweiften Klammer und dem Export der Komponente fügen Sie die gewünschte Struktur hinzu.

ToDoList.propTypes = {
    toDoItems: PropTypes.arrayOf(
        PropTypes.shape({
          id: PropTypes.number.isRequired,
          content: PropTypes.string.isRequired,
          complete: PropTypes.bool.isRequired,
        })
      ).isRequired,
  };

Anhand dieser Property wird die nächste Komponente Item aufgerufen. Diese können Sie im nächsten Schritt erstellen.

Das Item

Für die Item-Komponente benötigen Sie zunächst einmal Properties für die Id, den Inhalt und einen boolean-Wert, ob dieses Item bereits erledigt ist oder noch nicht. Auch dabei ist es zu empfehlen, dies mittels PropTypes dies zu bestimmen.

import React from 'react';
import Item from './Item';
import PropTypes from 'prop-types';

const ToDoList = ({ toDoItems }) => {

    return (
        <ul>
            {toDoItems.map(({id, content, complete}) => (
                <Item id={id} content={content} complete={complete}/>
            ))}
        </ul>
    );
}

ToDoList.propTypes = {
    toDoItems: PropTypes.arrayOf(
        PropTypes.shape({
          id: PropTypes.number.isRequired,
          content: PropTypes.string.isRequired,
          complete: PropTypes.bool.isRequired,
        })
      ).isRequired,
  };

export default ToDoList;

Jetzt haben Sie zwar die notwendigen Komponenten erstellt, um etwas anzeigen zu können, jedoch sind die Komponenten noch nicht innerhalb der Applikation verwendet worden und haben auch noch keine Beispieldaten. Deshalb sollten Sie nun Daten für den initialen Zustand anlegen, welche die Attribute id, content und complete haben.

const items = [
  {
    id: 1,
    content: "Test Content",
    complete: false
  },
  {
    id: 2,
    content: "Test Content 2",
    complete: false
  }
]  

Im Anschluss müssen Sie innerhalb der App.js die Komponente ToDoList importieren.

import ToDoList from './components/ToDoList';

Dadurch ist es Ihnen nun möglich die Komponente zu verwenden und dieser auch die gewünschten Properties zu übergeben. In Ihrem Browser sollten nun Ihre Beispieldaten als Liste angezeigt werden.

  return (
    <div>
      <ToDoList toDoItems={items}/>
    </div>
  );

Eingabefeld und Button

Die letzte Komponente, die Sie nun noch benötigen, ist ein Input Feld und ein Button, um weitere Items hinzufügen zu können. In diesem Beispiel wird diese Komponente AddToDoForm.jsx genannt.

import React from 'react';

const AddToDoButton = () => {
   
    return (
        <div>
            <form> 
                <input 
                placeholder="Enter Task" />
                <button type="submit">Add</button>
            </form>
        </div>
    );
}

export default AddToDoButton;

Sobald die Komponente erstellt wurde, kann diese innerhalb der App.js importiert und verwendet werden.

...
import AddToDoForm from './components/AddToDoForm';
...
  return (
    <div>
      <AddToDoForm />
      <ToDoList toDoItems={items}/>
    </div>
  );

Dadurch haben Sie nun den grundlegenden Aufbau Ihrer ToDo-Liste erfolgreich abgeschlossen. Jetzt können Sie mit der Funktionalität beginnen.

Local Storage

Für diese Applikation wollen wir dieses Mal den LocalStorage nutzen, um die ToDo-Items abzuspeichern. Dadurch sind Ihre Items auch nach einem Seite-Reload weiterhin vorhanden. Um dies umzusetzen, empfehlen wir einen Service Ordner und eine Service.js Klasse zu erstellen. Dabei benötigen Sie zwei Methoden, einmal zum Auslesen und einmal zum Speichern der Items.

Auslesen

Zuerst erstellen Sie eine Methode die anhand des übergebenen Keys innerhalb des LocalStorage überprüft, ob solch ein Key-Value Paar vorhanden ist. Falls solch ein Key-Value-Paar vorhanden sein sollte, wird aus diesem JSON-Text ein JavaScript Objekt erstellt und zurückgegeben. Falls nicht, wird ein neues Array erstellt und leer zurückgegeben.

export const getToDoItemsFromLocalStorage = (storageKey) => {
    const storageValue = localStorage.getItem(storageKey);
  
    let todoItems = null;
  
    try {
      const storageValueJSON = JSON.parse(storageValue);
  
      if (Array.isArray(storageValueJSON)) {
        todoItems = storageValueJSON;
      }
    } catch(e) {
      todoItems = [];
    }
    
    return todoItems;
};

Speichern

Im Vergleich zur Methode zum Auslesen, ist diese Methode sehr kurz. Dabei nimmt diese den Key entgegen, sowie die Value welches passend zu dem Key abgespeichert werden soll.

export const saveTodoItemsToLocalStorage = (storageKey, storageValue) => {
    localStorage.setItem(storageKey, JSON.stringify(storageValue))
};

Diese beiden Service Methoden werden Sie im Verlauf dieses Tutorial innerhalb der App Komponente benötigen. Deshalb müssen Sie diese dort importieren. Zusätzlich wird es auch für den nächsten Schritt notwendig sein, useState zu importieren.

import React, { useState }  from 'react';
import { getToDoItemsFromLocalStorage, saveTodoItemsToLocalStorage } from './service/service';

Im Anschluss können Sie diese Methode verwenden. Dafür erstellen Sie eine Variable, sowie eine Setter-Methode, welche auf den State zurückgreift. Dabei wird dem State entweder das Array aus der getToDoItemsFromLocalStorage Methode übergeben oder ein leerer Array.

const [todoItems, setTodoItems] = useState(getToDoItemsFromLocalStorage('item') || []);

Diese Variabel kann nun auch der ToDoItems Komponente übergeben werden, anstatt des items-Array, welches Beispieldaten enthalten hatte.

<ToDoList toDoItems={todoItems}/>

Hinzufügen von Items

Da Sie nun keine Items mehr angezeigt bekommen, müssen Sie nun die Funktionalität implementieren, um neue und eigene Items hinzuzufügen.

Deshalb erstellen Sie nun in der App.js eine Funktion die dies übernimmt. Dabei setzen Sie auf die Hook useCallback. Dadurch ist es ihnen möglich, diese Funktion auch weiter unten in ihrer Komponente zu verwenden.

const handleAddToDo = useCallback(item => {
    console.log("addToDo - App2")

    const items = [
      {
        id: item.id,
        content: item.content,
        complete: item.complete
      },
      ...todoItems,
    ]
  setTodoItems(items);
  saveTodoItemsToLocalStorage('item', items)
}, [todoItems])

Diese Funktion müssen Sie nun der Komponente AddToDoForm übergeben.

<AddToDoForm onAddToDo={handleAddToDo}/>

Damit Sie auch sichergehen können, dass Ihre Komponente die richtigen Properties übergeben bekommt, bestimmen Sie wieder mittels PropTypes, dass diese Property eine Funktion sein muss,

AddToDoButton.propTypes = {
    onAddToDo: PropTypes.func.isRequired,
}

Innerhalb des return-Wertes müssen Sie nun noch einige Sachen implementieren, welche Sie hier definiert haben.

return (
        <div>
            <form onSubmit={addToDoItem}> 
                    <input 
                    onChange={handleChange}
                    value={inputValue}
                    placeholder="Enter Task" />
                    <button type="submit">Add</button>
                </form>
        </div>
    );

Angefangen mit der Variable für das Input-Feld. Dieses soll nämlich den Text beinhalten für ihr ToDo-List-Item. Deshalb müssen Sie dieses mittels useState und einer Setter-Methode hinzufügen.

const [ inputValue, setInputValue ] = useState("");

Die onChange-Funktion soll jedes Mal ausgeführt werden, sobald ein onChange-Event durch das Input-Feld getriggert wird. Also wenn sich der Inhalt dieses Feld ändert. Dabei wird die zuvor erstellte Variable inputValue mittels der Setter-Methode anhand des Inhalts des Event-Targets geändert.

const handleChange = (e) => {
    setInputValue(e.currentTarget.value)
};

Da es Ihnen nun möglich ist, den Wert des Eingabefelds zu ändern, können Sie im nächsten Schritt die eigentliche Funktion für das Hinzufügen implementieren. Diese Methode wird anhand des onSubmit-Events aufgerufen, also sobald der Button geklickt wird. Dabei wird zunächst das standardmässige Verhalten von Submit unterbunden und ein Item Objekt wird erstellt. Die Id wird durch den Import von uuid möglich.

import { v4 as uuidv4 } from 'uuid';

Sollten Sie dies noch nicht installiert haben können Sie dies über das Terminal schnell erledigen.

npm install uuid

Der Inhalt wird anhand der Variable inputValue festgelegt. Für den Standardzustand eines Items, legen Sie sinnvollerweise fest, dass dieses noch nicht ausgeführt wurde.

const addToDoItem = e => {
        e.preventDefault();

        const item = {
            id: uuidv4(),
            content: inputValue,
            complete: false
        }
        
        onAddToDo(item)
        setInputValue("");
   };

Löschen von Items

Eine weitere wichtige Funktion ist, auch Items von der Liste löschen zu können. Deshalb erstellen wir, wie zuvor in der App.js eine handleOnDelete Funktion. Auch diese nutzt wieder die useCallback-Hook. Das Löschen wird anhand der Id vorgenommen, dafür wird mittels filter ein neues Array erstellt mit allen Item-Ids, welche ungleich dem Parameter sind. Anschliessend wird der State mit dem neuen Array aktualisiert. Zusätzlich mit der Service Methode wird der LocalStorage ebenfalls aktualisiert.

const handleOnDelete = useCallback(id => {
  const newTodoItems = todoItems.filter(item => item.id !== id)
  setTodoItems(newTodoItems)
  saveTodoItemsToLocalStorage('item', newTodoItems)
}, [todoItems]);

Diese Methode muss nun ausgeführt werden, wenn das Event onDeleteToDo getriggert wird.

<ToDoList toDoItems={todoItems} onDeleteToDo={handleOnDelete}/>

Wie vorhin gesehen muss auch diese in den PropTypes von ToDoList hinzugefügt und definiert werden.

ToDoList.propTypes = {
    ...
    onDeleteToDo: PropTypes.func.isRequired,
};

Ebenfalls muss diese Funktion innerhalb der Parameter übernommen werden, da diese Funktion noch einmal eine Komponente darunter, also dem Item, übergeben werden soll.

const ToDoList = ({ toDoItems, onDeleteToDo }) => {

    return (
        <ul>
            {toDoItems.map(({id, content, complete}) => (
                <Item id={id} content={content} complete={complete} onDeleteToDo={onDeleteToDo}/>
            ))}
        </ul>
    );
}

Da nun die Item Komponente die Funktion onDeleteToDo als Property hat, muss auch diese innerhalb dieser Komponente als PropType übernommen werden.

Item.propTypes = {
    ...
    onDeleteToDo: PropTypes.func.isRequired,
};

Für die Funktionsweise, dass ein Item gelöscht werden kann, benötigt das Item noch einen zusätzlichen Button, der dies übernimmt. Dieser Button soll eine handleOnDelete Methode innerhalb der Item-Komponente aufrufen.

<button onClick={handleOnDelete}>X</button>

Diese handleOnDelete Funktion nutzt wiederum die useCallback-Hook. Selbstverständlich muss diese zuvor wieder für diese Komponente importiert werden. Dabei ruft diese Funktion wiederum die onDeleteToDo-Funktion auf, welche über die Properties zur Verfügung gestellt wird. Dabei ist der Parameter für diese Funktion die Item-Id.

const handleOnDelete = useCallback(() => 
        onDeleteToDo(id), [id, onDeleteToDo]
    );

Dadurch ist es ihnen nun möglich Hinzugefügt-Items wieder zu löschen.

Items abhaken

Wenn Sie eine ToDo-Liste nutzen, wollen Sie auch Ihren Fortschritt sehen, die Dinge, die Sie schon erledigt haben. Auch mit dieser Funktionsweise beginnen wir in der obersten Komponente App.js.

In dieser Funktion wird nicht filter verwendet, sondern Sie suchen in diesem Fall ein ganz bestimmtes Item, um den Boolean-Wert durch einen Klick auf den umgekehrten Wert zu setzen. Dabei wird das Item mittels der Id gesucht. Auch hier muss zum Schluss der lokale State, sowie der LocalStorage geupdatet werden.

const handleOnToggleComplete = useCallback(id => {
  const item = todoItems.find(item => item.id === id)
  item.complete = !item.complete
  setTodoItems([...todoItems])
  saveTodoItemsToLocalStorage('item', todoItems)

}, [todoItems]);

Sobald diese Funktion geschrieben ist, können Sie diese der Komponente ToDoList übergeben.

<ToDoList 
  toDoItems={todoItems} 
  onDeleteToDo={handleOnDelete} 
  onToggle={handleOnToggleComplete}/>

Innerhalb von ToDoList müssen Sie nun noch drei Kleinigkeiten anpassen:

  • onToggle als Parameter übernehmen, und .
const ToDoList = ({ toDoItems, onDeleteToDo, onToggle }) => {...}
  • dem Item, ebenfalls diese Funktion weiterübergeben
<Item 
  id={id}
  content={content}
  complete={complete}
  onDeleteToDo={onDeleteToDo}
  onToggle={onToggle}/>
  • die PropTypes um onToggle erweitern
ToDoList.propTypes = {
    ...
    onToggle: PropTypes.func.isRequired,
};

Nun kommen wir zur letzten Komponente, welche Sie für diese Funktionalität erweitern müssen. Dabei wird dem p-Element textDecoration hinzugefügt, je nachdem welchen Wert complete hat. Des Weiteren wird ein onClick-Event hinzugefügt. Dieses Event ruft die Funktion handleOnToggle auf.

<p style={{textDecoration: complete ? "line-through" : ""}} 
  onClick={handleOnToggle}>
  {content}
</p>

Da Sie onToggle verwenden möchten, wird auch diese Funktion zu der Parameterliste hinzugefügt. Anschliessend können Sie nun auch die zuvor bestimmte handleOnToggle Funktion implementieren. Sobald diese aufgerufen wird, wird wiederum onToggle aufgerufen, welche bis nach oben in die App.js gereicht wird.

const Item = ({ id, content, complete, onDeleteToDo, onToggle }) => {

    const handleOnToggle = useCallback(() => 
        onToggle(id),[id, onToggle]
    );

Zum Schluss sollten Sie nicht vergessen die PropTypes wieder zu erweitern.

Item.propTypes = {
    ...
    onToggle: PropTypes.func.isRequired,
};

Das Styling

Zum Schluss wollen Sie bestimmt auch noch einige Style Anpassungen vornehmen, damit ihre ToDo-Liste auch nach etwas aussieht. Dafür benutzen wir die CSS-in-JS Variante. Dafür ist es notwendig, dass Sie styled-components installiert haben.

npm install --save-dev styled-components

In jeder Komponente in der styled-componentes verwendet werden soll, müssen Sie erst ein import durchführen.

import styled from 'styled-components';

Das Item

Innerhalb der Komponente müssen Sie nun die üblichen HTML-Tags durch ihre eigenen gestylten Tags ersetzen. Zuerst muss dafür einen Variablennamen bestimmt werden. Nach dem Gleichheitszeichen geben sie mithilfe des Imports an, was für eine Art Ihr HTML-Element sein soll. Innerhalb von einfachen Anführungszeichen bestimmen Sie nun Ihr gewünschtes Design. Dafür können Sie die üblichen CSS-Attribute verwenden.

const ItemArea = styled.div`
    display: flex;
    justify-content: flex-start;
    background: #f4f7fa;
    border: 2px solid #24272b;%;
    color: black;
    font-size: 22px;
    padding: 8px;
    margin:2px;
`;

const ListItem = styled.div`
    width: 90%;
`;

const DeleteButton = styled.button`
    background: #44bba4;
    border: 1px solid #44bba4;
    color: white;
    width: 10%;
    padding: 10px;
    margin-left: 15px;  
    border-radius: 3px;
`;

Da Sie nun ihrer eigenen HTML-Elemente erstellt haben, können Sie diese innerhalb des Return-Wertes verwenden

    <ItemArea>
        <ListItem key={id}>
            <p style={{textDecoration: complete ? "line-through" : ""}} onClick={handleOnToggle}>{content}</p>     
        </ListItem>
        <DeleteButton onClick={handleOnDelete}>X</DeleteButton>
    </ItemArea>

Die Liste

Ebenso gehen Sie bei der Liste wie oben beschrieben vor. Zuerst das import-Statement und anschliessend können Sie Ihr Design bestimmen.

const ListArea = styled.div`
display: flex;
justify-content: flex-start;
flex-direction: column;
`;

Und weiter unten wieder das neue Element für das div-Element ersetzen.

<ListArea>
    {toDoItems.map(({id, content, complete}) => (
        <Item 
          id={id}
          content={content}
          complete={complete}
          onDeleteToDo={onDeleteToDo}
          onToggle={onToggle}/>
    ))}
  </ListArea>

Das Formular

Und zu guter letzt müssen Sie nur noch das Formular samt Button Stylen.

const ItemInput = styled.input` 
    width: 100%;
    font-size: 22px;
    padding: 12px 20px;
    display: inline-block;
    border: 1px solid #ccc;
    border-radius: 4px;
    box-sizing: border-box;
`;

const AddButton = styled.button` 
    width: 100%;
    background-color: #44bba4;
    color: white;
    font-size: 22px;
    padding: 12px 20px;
    margin: 8px 0;
    border: none;
    border-radius: 4px;
    cursor: pointer;
`;  

const FormArea = styled.div` 
    display: flex;
    justify-content: center;
    background-color: #24272b;
    padding: 30px;
    margin: 10px
    color: white;
    text-align: center;
`;

const Form = styled.form` 
    width: 100%;
`;

Und selbstverständlich noch die letzten HTML-Elemente durch die neuen ersetzen.

<FormArea>
    <Form onSubmit={addToDoItem}> 
            <ItemInput 
              onChange={handleChange}
              value={inputValue}
              placeholder="Enter Task" />
            <AddButton type="submit">Add</AddButton>
        </Form>
</FormArea>

Herzlichen Glückwunsch, Sie haben so eben Ihre eigene ToDo-Liste mit React implementiert! Nun hatten Sie bei uns die Möglichkeit, mit drei verschiedenen Frameworks eine ToDo-Liste zu erstellen.

Ricky Elfner – Denker, Überlebenskünstler, Gadget-Sammler. Dabei ist er immer auf der Suche nach neuen Innovationen, sowie Tech News, um immer über aktuelle Themen schreiben zu können.