Using Immer for React State Management

Avatar of Kingsley Silas
Kingsley Silas am

DigitalOcean bietet Cloud-Produkte für jede Phase Ihrer Reise. Starten Sie mit 200 $ kostenlosem Guthaben!

Wir nutzen State, um Anwendungsdaten zu verfolgen. States ändern sich, wenn Benutzer mit einer Anwendung interagieren. Wenn das passiert, müssen wir den State aktualisieren, der dem Benutzer angezeigt wird, und das tun wir mit Reacts setState.

Da States nicht direkt aktualisiert werden sollen (da Reacts State unveränderlich sein muss), kann es kompliziert werden, wenn States komplexer werden. Sie werden schwer zu verstehen und nachzuvollziehen.

Hier kommt Immer ins Spiel und das werden wir in diesem Beitrag betrachten. Mit Immer können States vereinfacht und viel einfacher nachvollziehbar gemacht werden. Immer verwendet etwas namens „Draft“, das Sie als Kopie Ihres States betrachten können, aber nicht als den State selbst. Es ist, als ob Immer mit CMD+C den State kopiert und ihn dann woanders mit cmd+V eingefügt hätte, wo er sicher betrachtet werden kann, ohne die ursprüngliche Kopie zu stören. Alle Updates, die Sie vornehmen müssen, geschehen auf dem Draft, und die Teile des aktuellen States, die sich im Draft ändern, werden aktualisiert.

Nehmen wir an, der State Ihrer Anwendung sieht so aus;

this.state = {
  name: 'Kunle',
  age: 30,
  city: 'Lagos,
  country: 'Nigeria'
}

Dieser Benutzer feiert seinen 31. Geburtstag, was bedeutet, dass wir den Alterswert aktualisieren müssen. Mit Immer, das im Hintergrund läuft, wird eine Replik dieses States erstellt.

Stellen Sie sich nun vor, die Replik wird erstellt und einem Boten übergeben, der die neu kopierte Version des States an Kunle weitergibt. Das bedeutet, es sind nun zwei Kopien verfügbar – der aktuelle State und die übergebene Draft-Kopie. Kunle ändert dann das Alter im Draft auf 31. Der Bote kehrt mit dem Draft zur Anwendung zurück, vergleicht beide Versionen und aktualisiert nur das Alter, da dies der einzige Teil des Draft ist, der sich geändert hat.

Dies bricht nicht die Idee eines unveränderlichen States, da der aktuelle State nicht direkt aktualisiert wird. Immer macht es im Grunde bequem, mit unveränderlichem State zu arbeiten.

Lassen Sie uns ein Beispiel dafür betrachten

Angenommen, Sie möchten eine Ampel für Ihre Gemeinde bauen, können Sie sie mit Immer für Ihre State-Updates ausprobieren.

Sehen Sie den Pen
Traffic Light Example with Reactjs
von CarterTsai (@CarterTsai)
auf CodePen.

Mit Immer sieht die Komponente so aus

const {produce} = immer

class App extends React.Component {
  state = {
    red: 'red', 
    yellow: 'black', 
    green: 'black',
    next: "yellow"
  }

  componentDidMount() {
    this.interval = setInterval(() => this.changeHandle(), 3000);
  }
  
  componentWillUnmount()  {
    clearInterval(this.interval);
  }

  handleRedLight = () => {
    this.setState(
      produce(draft => {
        draft.red = 'red';
        draft.yellow = 'black';
        draft.green = 'black';
        draft.next = 'yellow'
      })
    )
  }
  
  handleYellowLight = () => {
    this.setState(
      produce(draft => {
        draft.red = 'black';
        draft.yellow = 'yellow';
        draft.green = 'black';
        draft.next = 'green'
      })
    )
  }
  
  handleGreenLight = () => {
    this.setState(
      produce(draft => {
        draft.red = 'black';
        draft.yellow = 'black';
        draft.green = 'green';
        draft.next = 'red'
      })
    )
  }

  changeHandle = () => {
    if (this.state.next === 'yellow') {
      this.handleYellowLight()
    } else if (this.state.next === 'green') {
      this.handleGreenLight()
    } else {
      this.handleRedLight()
    }
    
  }

  render() {
    return (
      <div className="box">
        <div className="circle" style={{backgroundColor: this.state.red}}></div>
        <div className="circle" style={{backgroundColor: this.state.yellow}}></div>
        <div className="circle" style={{backgroundColor: this.state.green}}></div>
      </div>
  );
}
};

produce ist die Standardfunktion, die wir von Immer erhalten. Hier übergeben wir sie als Wert an die setState()-Methode. Die produce-Funktion nimmt eine Funktion entgegen, die draft als Argument akzeptiert. Innerhalb dieser Funktion können wir dann den Draft mit dem wir unseren State aktualisieren möchten, einstellen.

Wenn das kompliziert aussieht, gibt es eine andere Möglichkeit, dies zu schreiben. Zuerst erstellen wir eine Funktion.

const handleLight = (state) => {
  return produce(state, (draft) => {
    draft.red = 'black';
    draft.yellow = 'black';
    draft.green = 'green';
    draft.next = 'red'
  });
}

Wir übergeben den aktuellen State der Anwendung und die Funktion, die draft als Argumente akzeptiert, an die produce-Funktion. Um dies innerhalb unserer Komponente zu nutzen, machen wir das;

handleGreenLight = () => {
  const nextState = handleLight(this.state)
  this.setState(nextState)
}

Ein weiteres Beispiel: Eine Einkaufsliste

Wenn Sie schon länger mit React arbeiten, dann ist Ihnen der Spread Operator nicht fremd. Mit Immer müssen Sie den Spread Operator nicht verwenden, insbesondere wenn Sie mit einem Array in Ihrem State arbeiten.

Lassen Sie uns das etwas weiter untersuchen, indem wir eine Einkaufslisten-Anwendung erstellen.

Sehen Sie den Pen
immer 2 – shopping list
von Kingsley Silas Chijioke (@kinsomicrote)
auf CodePen.

Hier ist die Komponente, mit der wir arbeiten

class App extends React.Component {
  constructor(props) {
      super(props)
      
      this.state = {
        item: "",
        price: 0,
        list: [
          { id: 1, name: "Cereals", price: 12 },
          { id: 2, name: "Rice", price: 10 }
        ]
      }
    }

    handleInputChange = e => {
      this.setState(
      produce(draft => {
        draft[event.target.name] = event.target.value
      }))
    }

    handleSubmit = (e) => {
      e.preventDefault()
      const newItem = {
        id: uuid.v4(),
        name: this.state.name,
        price: this.state.price
      }
      this.setState(
        produce(draft => {
          draft.list = draft.list.concat(newItem)
        })
      )
    };

  render() {
    return (
      <React.Fragment>
        <section className="section">
          <div className="box">
            <form onSubmit={this.handleSubmit}>
              <h2>Create your shopping list</h2>
              <div>
                <input
                  type="text"
                  placeholder="Item's Name"
                  onChange={this.handleInputChange}
                  name="name"
                  className="input"
                  />
              </div>
              <div>
                <input
                  type="number"
                  placeholder="Item's Price"
                  onChange={this.handleInputChange}
                  name="price"
                  className="input"
                  />
              </div>
              <button className="button is-grey">Submit</button>
            </form>
          </div>
          
          <div className="box">
            {
              this.state.list.length ? (
                this.state.list.map(item => (
                  <ul>
                    <li key={item.id}>
                      <p>{item.name}</p>
                      <p>${item.price}</p>
                    </li>
                    <hr />
                  </ul>
                ))
              ) : <p>Your list is empty</p>
            }
          </div>
        </section>
      </React.Fragment>
    )
  }
}

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

Wenn Elemente zur Liste hinzugefügt werden, müssen wir den State der Liste aktualisieren, um diese neuen Elemente widerzuspiegeln. Um den State von list mit setState() zu aktualisieren, müssten wir Folgendes tun:

handleSubmit = (e) => {
  e.preventDefault()
  const newItem = {
    id: uuid.v4(),
    name: this.state.name,
    price: this.state.price
  }
  this.setState({ list: [...this.state.list, newItem] })
};

Wenn Sie mehrere States in der Anwendung aktualisieren müssen, müssen Sie eine Menge Spreading betreiben, um einen neuen State zu erstellen, der den alten State und den zusätzlichen Wert verwendet. Das kann bei steigender Anzahl von Änderungen komplexer aussehen. Mit Immer wird das sehr einfach, wie wir im obigen Beispiel gezeigt haben.

Was ist, wenn wir eine Funktion hinzufügen möchten, die als Callback *nach* der State-Aktualisierung aufgerufen wird? In diesem Fall halten wir beispielsweise eine Zählung der Anzahl der Elemente in der Liste und den Gesamtpreis aller Elemente.

Sehen Sie den Pen
immer 3 – shopping list
von Kingsley Silas Chijioke (@kinsomicrote)
auf CodePen.

Wenn wir beispielsweise den Betrag berechnen möchten, der basierend auf dem Preis der Artikel in der Liste ausgegeben wird, kann die handleSubmit-Funktion wie folgt aussehen:

handleSubmit = (e) => {
  e.preventDefault()
  const newItem = {
    id: uuid.v4(),
    name: this.state.name,
    price: this.state.price
  }
  
  this.setState(
    produce(draft => {
      draft.list = draft.list.concat(newItem)
    }), () => {
      this.calculateAmount(this.state.list)
    }
  )
};

Zuerst erstellen wir ein Objekt mit den vom Benutzer eingegebenen Daten, das wir dann newItem zuweisen. Um den State unserer Anwendung zu aktualisieren, verwenden wir .concat(), das ein neues Array zurückgibt, das aus den vorherigen Elementen und dem neuen Element besteht. Diese aktualisierte Kopie wird nun als Wert von draft.list gesetzt, die dann von Immer verwendet werden kann, um den State der Anwendung zu aktualisieren.

Die Callback-Funktion wird nach der State-Aktualisierung aufgerufen. Es ist wichtig zu beachten, dass sie den aktualisierten State verwendet.

Die Funktion, die wir aufrufen möchten, sieht so aus:

calculateAmount = (list) => {
  let total = 0;
    for (let i = 0; i < list.length; i++) {
      total += parseInt(list[i].price, 10)
    }
  this.setState(
    produce(draft => {
      draft.totalAmount = total
    })
  )
}

Schauen wir uns Immer Hooks an

use-immer ist ein Hook, der es Ihnen ermöglicht, State in Ihrer React-Anwendung zu verwalten. Sehen wir das in Aktion anhand eines klassischen Zähler-Beispiels.

import React from "react";
import {useImmer} from "use-immer";

const Counter = () => {
  const [count, updateCounter] = useImmer({
    value: 0
  });

  function increment() {
    updateCounter(draft => {
      draft.value = draft.value +1;
    });
  }

  return (
    <div>
      <h1>
        Counter {count.value}
      </h1>
      <br />
      <button onClick={increment}>Increment</button>
    </div>
  );
}

export default Counter;

useImmer ist ähnlich wie useState. Die Funktion gibt den State und eine Updater-Funktion zurück. Wenn die Komponente zum ersten Mal geladen wird, ist der Wert des States (der in diesem Beispiel count ist) derselbe wie der Wert, der an useImmer übergeben wurde. Mit der zurückgegebenen Updater-Funktion können wir dann eine increment-Funktion erstellen, um den Wert des Zählers zu erhöhen.

Es gibt auch einen useReducer-ähnlichen Hook für Immer.

import React, { useRef } from "react";
import {useImmerReducer } from "use-immer";
import uuidv4 from "uuid/v4"
const initialState = [];
const reducer = (draft, action) => {
  switch (action.type) {
    case "ADD_ITEM":
      draft.push(action.item);
      return;
    case "CLEAR_LIST":
      return initialState;
    default:
      return draft;
  }
}
const Todo = () => {
  const inputEl = useRef(null);
  const [state, dispatch] = useImmerReducer(reducer, initialState);
  
  const handleSubmit = (e) => {
    e.preventDefault()
    const newItem = {
      id: uuidv4(),
      text: inputEl.current.value
    };
    dispatch({ type: "ADD_ITEM", item: newItem });
    inputEl.current.value = "";
    inputEl.current.focus();
  }
  
  const handleClear = () => {
    dispatch({ type: 'CLEAR_LIST' })
  }
  
  return (
    <div className='App'>
      <header className='App-header'>
        <ul>
          {state.map(todo => {
            return <li key={todo.id}>{todo.text}</li>;
          })}
        </ul>
        <form onSubmit={handleSubmit}>
          <input type='text' ref={inputEl} />
          <button
            type='submit'
          >
            Add Todo
          </button>
        </form>
        <button
          onClick={handleClear}
        >
          Clear Todos
        </button>
      </header>
    </div>
  );
}
export default Todo;

useImmerReducer nimmt eine Reducer-Funktion und den initialen State entgegen und gibt sowohl den State als auch die Dispatch-Funktion zurück. Wir können dann durch den State iterieren, um die Elemente anzuzeigen, die wir haben. Wir rufen dispatch eine Aktion auf, wenn ein Todo-Element übermittelt und die Liste gelöscht wird. Die aufgerufene Aktion hat einen Typ, den wir verwenden, um zu entscheiden, was in der Reducer-Funktion zu tun ist.
In der Reducer-Funktion verwenden wir draft, wie wir es zuvor getan haben, anstelle von state. Damit haben wir eine bequeme Möglichkeit, den State unserer Anwendung zu manipulieren.

Den im obigen Beispiel verwendeten Code finden Sie auf GitHub.

Das war ein Blick auf Immer!

In Zukunft können Sie Immer in Ihrem nächsten Projekt verwenden oder es langsam in das Projekt integrieren, an dem Sie gerade arbeiten. Es hat sich als hilfreich erwiesen, um State-Management bequem zu gestalten.