Webtechnologien
Wintersemester 2024
React State Management
♯
♫
React State Management
<section bg="react-state-management.jpg" id="react-state-management" class="slide cover"><div><h2>React State Management</h2> <p class="note">Quelle: <a href="https://unsplash.com/photos/leiE_1HQRkk">Andrés Gómez</a></p> </div></section> <section class="slide" id="react-router"><div><h2><a href="https://reactrouter.com">React Router</a></h2> <ul> <li>deklarative Zuordnung von Pfaden und Komponenten</li> <li>Extraktion von URL-Parametern</li> <li>Einfache Möglichkeit, URLs in SPAs zu nutzen um Application-State zu transportieren</li> <li>Übersichtliche API: <ul> <li><code class="language-plaintext highlighter-rouge">Router</code>, <code class="language-plaintext highlighter-rouge">Routes</code>, <code class="language-plaintext highlighter-rouge">Route</code> ,<code class="language-plaintext highlighter-rouge">Link</code> <small>(und ein paar andere)</small></li> <li><code class="language-plaintext highlighter-rouge">useParams</code>, <code class="language-plaintext highlighter-rouge">useLocation</code>, <code class="language-plaintext highlighter-rouge">useNavigation</code></li> </ul> </li> </ul> <pre class="highlight language-bash" data-lang="bash"><code>npm <span class="nb">install </span>react-router-dom </code></pre> <footer> <ul> <li>Für dieses Problem, eine App anhand der URL in den richtigen Zustand zu versetzen, hat sich React Router in den vergangenen Jahren als de facto Standard etabliert.</li> </ul> </footer> </div></section> <section class="slide shout" id="application-state"><div><h2>Application State</h2> <pre class="highlight language-jsx" data-lang="jsx"><code></code></pre> <footer> <ul> <li>Eben haben wir uns angesehen, wie der State, der implizit oder explizit in URLs enthalten ist, verarbeitet werden kann, um eine App in den richtigen Zustand zu bringen.</li> <li>Jetzt würde ich gerne eine andere Form von State ansehen, und zwar: App State</li> <li>Mit zunehmender Größe einer Anwendung wachsen meist auch deren Komplexität und die Daten, die mit der Anwendung verwaltet werden sollen. Also anders gesagt: Der <strong>App State</strong> wird schwieriger und unübersichtlicher zu verwalten. Wann gebe ich wie welcher Komponente welche Props hinein? Wie wirken sich diese Props auf den State meiner Komponente aus und was passiert wenn ich den State in einer Komponente modifiziere?</li> <li>Um dieses Problem zu lösen, gibt es einige externe Tools für <strong>globales State Management</strong>, die sich im Ökosystem von React gebildet haben. Das populärste ist wohl Redux.</li> </ul> </footer> </div></section> <section class="slide" id="prop-drilling"><div><h2>Prop Drilling</h2> <p>Übergabe von State als Props an Komponenten, die diesen nur weiterreichen, ohne ihn selbst zu verwenden.</p> </div></section> <section class="slide" id="redux"><div><h2><a href="https://redux.js.org/">Redux</a></h2> <ul> <li><q>A Predictable State Container for JS Apps</q></li> <li>Zentraler Speicher für State</li> <li>Unidirektionaler Datenfluss</li> </ul> <pre class="highlight language-bash" data-lang="bash"><code>npm <span class="nb">install </span>redux react-redux </code></pre> <footer> <ul> <li>Redux bezeichnet sich selber als <q>vorhersehbaren State Container</q>. Um zu verstehen, was das bdeutet, müssen wir uns das Grundprinzip von Redux anschauen: <q>Unidirektionaler Datenfluss</q></li> <li>Das kennen wir schon von React: <ul> <li>Eine <strong>Aktion</strong> (bspw. ausgelöst durch einen Button-Klick) ändert den <strong>State</strong> einer Komponente, die <strong>State-Änderung</strong> löst ein <strong>Rerendering</strong> aus und erlaubt es dann weitere Aktionen auszuführen</li> <li>genau das macht auch Redux, nur dass die Änderung nach ganz oben, an eine globale Instanz gegeben wird und die erzeugte State-Änderung an alle daran interessierten Komponenten weitergegeben wird, egal wo sie sich im Baum befinden.</li> </ul> </li> <li>Wir installieren daher 2 Pakete: <code class="language-plaintext highlighter-rouge">redux</code> und <code class="language-plaintext highlighter-rouge">react-redux</code></li> <li>Theoretisch wäre auch die Verwendung von <strong>Redux</strong> alleine möglich, allerdings müssten wir uns dann selbst darum kümmern zu schauen, wann Komponenten neu gerendert werden und darum, wie Daten aus einer Komponente in den State Container rein und wieder raus kommen. Da wir das nicht wollen, weil sich andere das bereits gemacht haben, nutzen wir eben zusätzlich <code class="language-plaintext highlighter-rouge">react-redux</code>, was uns eine schöne API gibt, um mit Redux zu arbeiten.</li> </ul> </footer> </div></section> <section class="slide" id="store-actions-und-reducer"><div><h2>Store, Actions und Reducer</h2> <p><img src="https://redux.js.org/assets/images/ReduxDataFlowDiagram-49fa8c3968371d9ef6f2a1486bd40a26.gif" alt="Redux Data Flow" class="right" width="370" /></p> <ul> <li><strong>Store</strong> speichert alle Daten und ermöglicht Zugriff (<code class="language-plaintext highlighter-rouge">getState</code>) und Änderung (<code class="language-plaintext highlighter-rouge">dispatch</code>)</li> <li><strong>Action</strong> Nachricht über eine Änderung mit <code class="language-plaintext highlighter-rouge">type</code> und ggf. <code class="language-plaintext highlighter-rouge">payload</code></li> <li><strong>Reducer</strong> berechnet aus aktuellem State und einer Action den neuen State (<code class="language-plaintext highlighter-rouge">newState = reducer(currentState, action)</code>)</li> </ul> <footer> <ul> <li>Es gibt in Redux drei wichtige Begriffe: Store, Actions und Reducer. Gehen wir sie nacheinander durch.</li> <li>Alle Daten in <strong>Redux</strong> befinden sich in einem sogenannten <strong>Store</strong>, der sich um die Verwaltung des globalen <strong>States</strong> kümmert. Theoretisch kann eine Anwendung auch mehrere Stores haben, in <strong>Redux</strong> ist das jedoch, um Komplexität zu reduzieren eher unüblich und so beschränken sich React-Anwendungen, die <strong>Redux</strong> einsetzen, meist auch auf lediglich einen <em>einzigen</em> <strong>Store</strong> als <strong>Single Source of Truth</strong>, also als <em>die</em> einzige wahre Quelle für alle Daten. Der Store stellt Methoden bereit, um die sich in ihm befindlichen Daten zu verändern (<code class="language-plaintext highlighter-rouge">dispatch</code>), zu lesen (<code class="language-plaintext highlighter-rouge">getState</code>), und auf Änderungen zu reagieren (<code class="language-plaintext highlighter-rouge">subscribe</code>).</li> <li>Die einzige Möglichkeit um Daten in einem <strong>Store</strong> zu verändern, ist dabei das Auslösen (<em>„dispatchen<q></em>) von <strong>Actions</strong>. Sie bestehen aus einem simplen JavaScript-Objekt, das eine bestimmte Form haben muss, und zwar muss es einen <code class="language-plaintext highlighter-rouge">type</code> haben, und zusätzlich kann es Daten wie <code class="language-plaintext highlighter-rouge">payload</code>, <code class="language-plaintext highlighter-rouge">meta</code> und <code class="language-plaintext highlighter-rouge">error</code> haben.</li> <li><code class="language-plaintext highlighter-rouge">type</code> lässt sich wie ein Funktionsame betrachten und <code class="language-plaintext highlighter-rouge">payload</code> wie die Parameter.</li> <li>Die <strong>Payload</strong> stellt sozusagen den <em>Inhalt</em> einer <strong>Action</strong> dar und kann alles beinhalten, was serialisierbar ist, also Boolean, String, Number und Arrays und Objekte, solange sie keine Funktioen enthalten, also quasi alles, was sich in JSON ausdrücken ließe.</li> <li>Wird eine <strong>Action</strong> durch die vom <strong>Store</strong> bereitgestellte <code class="language-plaintext highlighter-rouge">dispatch</code>-Methode ausgelöst, wird der zum Zeitpunkt des Aufrufs aktuelle <strong>State</strong> zusammen mit der ausgelösten <strong>Action</strong> an die <strong>Reducer</strong> übergeben.</li> <li>Ein <strong>Reducer</strong> ist eine <strong>pure Function</strong>, und dient dazu, aus dem <strong>aktuellen State</strong> und der jeweiligen Action mit ihren <code class="language-plaintext highlighter-rouge">type</code>- und <code class="language-plaintext highlighter-rouge">payload</code>-Eigenschaften einen <strong>neuen State</strong> zu erzeugen.</li> <li> <p>Eine <strong>pure Function</strong> erzeugt stets <strong>dieselbe Ausgabe</strong> bei <strong>gleichen Eingabeparametern</strong>, egal wie oft diese aufgerufen wird. Dieses Verhalten ist es, das sie vorhersehbar und dadurch auch gleichzeitig einfach testbar macht.</p> </li> <li>Prinzipien <ul> <li>Single Source of Truth</li> <li>State is read-only</li> </ul> </li> </ul> </footer> </div></section> <section class="slide" id="beispiel-action"><div><h2>Beispiel-Action</h2> <pre class="highlight language-jsx" data-lang="jsx"><code><span class="kd">const</span> <span class="nx">remove</span> <span class="o">=</span> <span class="p">(</span><span class="nx">id</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span> <span class="kd">const</span> <span class="nx">action</span> <span class="o">=</span> <span class="p">{</span> <span class="na">type</span><span class="p">:</span> <span class="dl">'</span><span class="s1">REMOVE</span><span class="dl">'</span><span class="p">,</span> <span class="na">payload</span><span class="p">:</span> <span class="nx">id</span><span class="p">,</span> <span class="p">};</span> <span class="k">return</span> <span class="nx">action</span><span class="p">;</span> <span class="p">}</span> <span class="kd">const</span> <span class="nx">onClick</span> <span class="o">=</span> <span class="p">{</span> <span class="kd">const</span> <span class="nx">action</span> <span class="o">=</span> <span class="nf">remove</span><span class="p">(</span><span class="mi">4</span><span class="p">);</span> <span class="nx">store</span><span class="p">.</span><span class="nf">dispatch</span><span class="p">(</span><span class="nx">action</span><span class="p">);</span> <span class="p">}</span> <span class="p"><</span><span class="nt">button</span> <span class="na">onClick</span><span class="p">=</span><span class="si">{</span><span class="nx">onClick</span><span class="si">}</span><span class="p">></span>Remove<span class="p"></</span><span class="nt">button</span><span class="p">></span> </code></pre> <footer> <ul> <li>Für unser Favourites-Beispiel könnte eine Action nach Klick auf dem Herz-Button so aussehen</li> <li>als kurzer Einschub: Wer schon mit Redux in Berührung gekommen ist, ist vielleicht auf die Begriffe <strong>Action</strong> und <strong>Action Creator</strong> gestoßen. <ul> <li>Action: serialisierbares Object, das eine State-Änderung beschreibt</li> <li>Action Creator: Funktion, die eine Action zurück gibt (quasi eine Action Factory). Werde meist verwendet, um Logik zu kapseln</li> </ul> </li> </ul> </footer> </div></section> <section class="slide" id="beispiel-reducer"><div><h2>Beispiel-Reducer</h2> <pre class="highlight language-js" data-lang="js"><code><span class="kd">const</span> <span class="nx">initialState</span> <span class="o">=</span> <span class="p">[];</span> <span class="kd">const</span> <span class="nx">favouritesReducer</span> <span class="o">=</span> <span class="p">(</span><span class="nx">state</span> <span class="o">=</span> <span class="nx">initialState</span><span class="p">,</span> <span class="nx">action</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span> <span class="k">switch </span><span class="p">(</span><span class="nx">action</span><span class="p">.</span><span class="nx">type</span><span class="p">)</span> <span class="p">{</span> <span class="k">case</span> <span class="dl">'</span><span class="s1">ADD</span><span class="dl">'</span><span class="p">:</span> <span class="p">{</span> <span class="k">return</span> <span class="p">[...</span><span class="nx">state</span><span class="p">,</span> <span class="nx">action</span><span class="p">.</span><span class="nx">id</span><span class="p">];</span> <span class="p">}</span> <span class="k">case</span> <span class="dl">'</span><span class="s1">REMOVE</span><span class="dl">'</span><span class="p">:</span> <span class="p">{</span> <span class="k">return</span> <span class="nx">state</span><span class="p">.</span><span class="nf">filter</span><span class="p">(</span><span class="nx">id</span> <span class="o">=></span> <span class="nx">id</span> <span class="o">!==</span> <span class="nx">action</span><span class="p">.</span><span class="nx">id</span><span class="p">);</span> <span class="p">}</span> <span class="nl">default</span><span class="p">:</span> <span class="p">{</span> <span class="k">return</span> <span class="nx">state</span><span class="p">;</span> <span class="p">}</span> <span class="p">}</span> <span class="p">};</span> </code></pre> <footer> <ul> <li>Der Reducer bekommt den kompletten aktuellen State und die Action und erzeugt basierend darauf den neuen State</li> <li>Dieser muss immutable sein, es darf also icht einfach der initiale State verändert werden</li> <li>Reducer müssen synchron sein.</li> <li>In der Praxis würde man die Types in eine eigene Datei auslagern und nur mit den Variablen-Namen arbeiten, um Tippfehler zu vermeiden.</li> </ul> </footer> </div></section> <section class="slide" id="beispiel-store"><div><h2>Beispiel-Store</h2> <pre class="highlight language-js" data-lang="js"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">createStore</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">redux</span><span class="dl">'</span><span class="p">;</span> <span class="kd">const</span> <span class="nx">store</span> <span class="o">=</span> <span class="nf">createStore</span><span class="p">(</span><span class="nx">favouritesReducer</span><span class="p">);</span> </code></pre> <footer> <ul> <li>der Store stellt dann die <code class="language-plaintext highlighter-rouge">dispatch</code>-Methode aus dem ersten Beispiel bereit.</li> </ul> </footer> </div></section> <section class="slide" id="react--redux"><div><h2>React + Redux</h2> <ol> <li>State bereitstellen</li> </ol> <pre class="highlight language-jsx" data-lang="jsx"><code><span class="p"><</span><span class="nc">Provider</span> <span class="na">store</span><span class="p">=</span><span class="si">{</span><span class="nx">store</span><span class="si">}</span><span class="p">></span> <span class="p"><</span><span class="nc">App</span> <span class="p">/></span> <span class="p"></</span><span class="nc">Provider</span><span class="p">></span> </code></pre> <ol> <li>Komponenten an State anbinden</li> </ol> <pre class="highlight language-jsx" data-lang="jsx"><code><span class="c1">// App-State zu Props umformen</span> <span class="kd">const</span> <span class="nx">mapStateToProps</span> <span class="o">=</span> <span class="nx">state</span> <span class="o">=></span> <span class="p">({</span> <span class="na">isLoggedIn</span><span class="p">:</span> <span class="nx">state</span><span class="p">.</span><span class="nx">user</span><span class="p">.</span><span class="nx">isLoggedIn</span> <span class="p">});</span> <span class="c1">// State-Änderungen zu Props umformen</span> <span class="kd">const</span> <span class="nx">mapDispatchToProps</span> <span class="o">=</span> <span class="nx">dispatch</span> <span class="o">=></span> <span class="p">({</span> <span class="na">login</span><span class="p">:</span> <span class="p">()</span> <span class="o">=></span> <span class="nf">dispatch</span><span class="p">({</span> <span class="na">type</span><span class="p">:</span> <span class="dl">'</span><span class="s1">LOGIN_USER</span><span class="dl">'</span> <span class="p">})</span> <span class="p">});</span> <span class="nf">connect</span><span class="p">(</span><span class="nx">mapStateToProps</span><span class="p">,</span> <span class="nx">mapDispatchToProps</span><span class="p">)(</span><span class="nx">Komponente</span><span class="p">)</span> </code></pre> </div></section> <section class="slide" id="redux-hooks"><div><h2>Redux Hooks</h2> <ul> <li><q>Lesen</q>: <code class="language-plaintext highlighter-rouge">useSelector</code> (statt <code class="language-plaintext highlighter-rouge">mapStateToProps</code>)</li> <li><q>Schreiben</q>: <code class="language-plaintext highlighter-rouge">useDispatch</code> (statt <code class="language-plaintext highlighter-rouge">mapDispatchToProps</code>)</li> </ul> <pre class="highlight language-jsx" data-lang="jsx"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">useSelector</span><span class="p">,</span> <span class="nx">useDispatch</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">react-redux</span><span class="dl">'</span><span class="p">;</span> <span class="kd">const</span> <span class="nx">isLoggedIn</span> <span class="o">=</span> <span class="nf">useSelector</span><span class="p">(</span><span class="nx">state</span> <span class="o">=></span> <span class="nx">state</span><span class="p">.</span><span class="nx">user</span><span class="p">.</span><span class="nx">isLoggedIn</span><span class="p">);</span> <span class="kd">const</span> <span class="nx">dispatch</span> <span class="o">=</span> <span class="nf">useDispatch</span><span class="p">();</span> <span class="kd">const</span> <span class="nx">login</span> <span class="o">=</span> <span class="p">()</span> <span class="o">=></span> <span class="nf">dispatch</span><span class="p">({</span> <span class="na">type</span><span class="p">:</span> <span class="dl">'</span><span class="s1">LOGIN_USER</span><span class="dl">'</span> <span class="p">});</span> </code></pre> <footer> <ul> <li>der Store stellt dann die <code class="language-plaintext highlighter-rouge">dispatch</code>-Methode aus dem ersten Beispiel bereit.</li> </ul> </footer> </div></section> <section class="slide" id="async-actions"><div><h2>Async Actions</h2> <pre class="highlight language-sh" data-lang="sh"><code>npm <span class="nb">install </span>redux-thunk </code></pre> <pre class="highlight language-js" data-lang="js"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">applyMiddleware</span><span class="p">,</span> <span class="nx">createStore</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">redux</span><span class="dl">'</span><span class="p">;</span> <span class="k">import</span> <span class="nx">thunk</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">redux-thunk</span><span class="dl">'</span><span class="p">;</span> <span class="kd">const</span> <span class="nx">store</span> <span class="o">=</span> <span class="nf">createStore</span><span class="p">(</span><span class="nx">reducer</span><span class="p">,</span> <span class="nf">applyMiddleware</span><span class="p">(</span><span class="nx">thunk</span><span class="p">));</span> </code></pre> <p><img src="https://redux.js.org/assets/images/ReduxAsyncDataFlowDiagram-d97ff38a0f4da0f327163170ccc13e80.gif" alt="Redux Thunk Data Flow" class="center" width="370" /></p> <footer> <ul> <li>Reducer imm synchron, aber viele Aktionen sind asynchron (fetch -> pending, suscess, error)</li> <li>Enhancer legt sich um <code class="language-plaintext highlighter-rouge">dispatch</code> und kann Veränderungen vornehmen</li> </ul> </footer> </div></section> <section class="slide" id="react-query"><div><h2><a href="https://react-query.tanstack.com">react-query</a></h2> <ul> <li>Server-Kommunikation und Caching</li> </ul> </div></section> <section class="slide" id="react-context--reducer-mit-hooks"><div><h2><a href="https://reactjs.org/docs/context.html">React Context</a> & Reducer mit Hooks</h2> <ul> <li><a href="https://reactjs.org/docs/hooks-reference.html#usecontext">Context Hook</a> <code class="language-plaintext highlighter-rouge">useContext</code></li> <li><a href="https://reactjs.org/docs/hooks-reference.html#usereducer">Reducer Hook</a> <code class="language-plaintext highlighter-rouge">useReducer</code></li> </ul> </div></section>