State Lifting
Das gezielte Verwalten von Zuständen ist eine der zentralen Herausforderungen in der Entwicklung mit React. Wenn mehrere Komponenten denselben Datenstand benötigen oder verändern sollen, stößt man schnell an die Grenzen lokaler Zustände. Hier setzt das Konzept des Lifting State an: ein Prinzip, das hilft, Zustände auf eine übergeordnete Ebene zu heben und so eine konsistente Datenbasis für mehrere Komponenten zu schaffen. Dieses Vorgehen verbessert nicht nur die Struktur und Wartbarkeit des Codes, sondern fördert auch ein klareres Verständnis der Datenflüsse innerhalb einer Anwendung.
Inhaltsverzeichnis
Problem
Es entsteht nicht selten die Notwendigkeit, ein Component A über die Änderung des Zustandes im Component B zu informieren.
Um das Problem besser beschreiben zu können, definieren wir zwei Components.
Component 1: SearchBar
import { useState } from 'react';
const SearchBar = () => {
const [searchTerm, setSearchTerm] = useState('');
const handleUpdateSearchTerm = (event) => {
setSearchTerm(currentSearchTerm => event.target.value);
};
return <input type="search" onChange={handleUpdateSearchTerm} />;
};
export default SearchBar;
Component 2: SearchOverview
const SearchOverview = () => {
return <p>Aktuelle Suchanfrage: {searchTerm}</p>
};
export default SearchOverview;
Zusammenführung (Verwendung) in App
Component.
import SearchBar from './components/SearchBar';
import SearchOverview from './components/SearchOverview';
class App extends React.Component {
render() {
return (
<>
<h1>State Sharing</h1>
<hr/>
<SearchBar />
<SearchOverview />
</>
);
}
}
export default App;
Wir haben hier das Problem, dass die Zustandsverwaltung im SearchBar
Component definiert ist. Aktuell weiß das Component SearchOverview
, in welchem wir ausgeben möchten, was in SearchBar
eingegeben wurde, nichts über irgendwelche Zustände im Component SearchBar
. Es besteht einfach keine Verbindung.
Man könnte sicherlich das Problem lösen, indem man beide Components zu einem Component zusammenführt. Dies ist allerdings nicht immer möglich bzw. manchmal gar nicht gewünscht, da man versuchen sollte die Component-Landschaft so granular wie möglich zu halten.
Lösung
Dieses Problem kann mit dem sogenannten State Lifting gelöst werden. Wenn State Lifting (das Anheben der Zustandsverwaltung) eingesetzt wird, wird der Zustand in keinem von den Beiden Components (in diesem Fall SearchBar
und SearchOverview
) verwaltet, sondern eine Ebene höher angesetzt.
Die Verwaltung des Zustandes verlagert sich zum Eltern-Component. Um genauer zu sein, zum nächstgelegenen Eltern-Component. Um das bildlich sich vorstellen zu können, schauen wir uns die folgende Grafik an.
In unserem Beispiel haben SearchBar
und SearchOverview
einen gemeinsamen Eltern-Component, nämlich App
. Warum? Weil wir sie dort einbinden. Würden wir sie in einem anderen Component, so wäre das andere Component eben das Parent-Component. In React sind Components in einer Art Baumstruktur aufgebaut, die bis zum Root-Component (in der Regel App
) geht.
Die anderen Components in der Grafik sind lediglich beispielhaft da. Wenn z.B. wir die Zustandsverwaltung zwischen unterschiedlichen Benutzer-Typen aufteilen müssten, wäre unser verwaltendes Component (oder das nächstgelegene Eltern-Component) das Users
Component.
Die geteilte Zustandsverwaltung wird durch den Einsatz von Props und das Auslagern in das Eltern-Component realisiert. Sie wird “angehoben”, was der Begriff State Lifting beschreibt.
Lösung - Beispiel Umbau
Nun bauen das oben gezeigte Beispiel so um, dass die Zustände zwischen den beiden Components ausgetauscht werden können.
Aus unserem SearchBar
Component entfernen wir die Zustandsverwaltung und platzieren eine Props-Eigenschaft onUpdateSearch
.
const SearchBar = ({ onUpdateSearch }) => {
return <input type="search" onChange={onUpdateSearch} />;
};
export default SearchBar;
Im Component SearchOverview
ergänzen wir die Prop-Eigenschaft searchTerm
.
const SearchOverview = ({ currentSearchTerm }) => {
return <p>Aktuelle Suchanfrage: {currentSearchTerm}</p>
};
export default SearchOverview;
Unser App
Component hat nun die Zustandsverwaltung übernommen.
import SearchBar from './components/SearchBar';
import SearchOverview from './components/SearchOverview';
const App = () => {
const [searchTerm, setSearchTerm] = useState('');
const handleUpdateSearchTerm = (event) => {
setSearchTerm(s => event.target.value);
};
return (
<>
<SearchBar onUpdateSearch={handleUpdateSearchTerm} />
<SearchOverview currentSearchTerm={searchTerm} />
</>
);
};
export default App;
Mit diesem Umbau haben wir den Zustand erreicht, dass wir im Component SearchOverview
nun über die Änderungen, in diesem Fall Input-Event, im Component SearchBar
über die Brücke, also das Eltern-Component App
, informiert werden.
Weil React nicht nur das Component, in dem die Zustandsverwaltung stattfindet, bei Änderung von überwachten Einheiten neurendert, sondern auch die Kind-Elemente, die Zustandswerte einsetzen/verwenden, werden sie ebenfalls von React aktualisiert.
Props und Schema
An dieser Stelle möchte ich etwas genauer auf die Props eingehen und das Schema etwas mehr erklären.
Im Component App
haben wir die Funktion handleUpdateSearchTerm
definiert. Diese aktualisiert einfach unseren Zustandswert für das Suchwort. Soweit ok. Nichts besonderes.
Diese Funktion schnappen wir und übergeben sie in das Component SearchBar
. Dabei binden wir die Referenz auf diese Funktion an den Namen onUpdateSearch
. Das könnte jeder anderer Name sein. Definiert wird dieser im Ziel-Component, in diesem Fall in SearchBar
als Prop-Eigenschaftsname.
Was bedeutet es? Es bedeutet, dass wir in unserem SearchBar
Component eigentlich eine Referenz auf die handleUpdateSearchTerm
Funktion haben. Entsprechend wird auch tatsächlich diese Funktion aufgerufen, wenn der registrierte Event getriggert wird.
Ok, lasst uns das Ganze anhand eines Beispiels verdeutlichen. Wir erstellen dafür möglichst einfache Components. Diese packe ich in eine Datei, sodass wir uns die Importe sparen können.
const ChildComp = ({ refFunction }) => {
return (
<button onClick={refFunction}>
Call function
</button>
);
};
const ParentComp = () => {
const originalFunc = (event) => {
console.log('Button in ChildComp clicked');
};
return <ChildComp refFunction={originalFunc} />
};
export default ParentComp;
Was haben wir hier? Wir haben hier ein ParentComp
, indem wir eine Funktion originalFunc
definieren. Diese Funktion macht in diesem Beispiel nichts. Uns reicht aus, dass wir ein Signal bekommen, dass genau diese Funktion aufgerufen wurde. Dazu dient unsere console.log
Anweisung.
In unserem ChildComp
definieren wird eine Prop-Eigenschaft namens refFunction
. Das ist ein Name, an den wir im ParentComp
die Funktion originalFunc
binden.
Zusätzlich haben wir im ChildComp
einen Button erstellt und diesem einen Klick-Event angehängt. Jedes Mal, wenn der Klick-Event durch einen Klick abgefeuert wird, wird die Funktion refFunction
aufgerufen. Aber in Wirklichkeit, weil wir die originalFunc
als Prop hier hineingegeben haben (als Referenz), wird die originalFunc
aufgerufen. Das können wir anhand dem Log in der Konsole im Browser sehen.