Nils Hartmann

"Modernes" Statemanagement

für React

Web Developer Conference, 28. April 2021 | DevSession

E-Mail: nils@nilshartmann.net | Twitter: @nilshartmann

Nils Hartmann

https://nilshartmann.net / @nilshartmann

Freiberuflicher Software-Entwickler, Berater und Trainer aus Hamburg

Java

JavaScript, TypeScript

React

Single-Page-Applications

GraphQL

Schulungen und Workshops


https://reactbuch.de

Agenda

Slides

https://react.schule/wdc21-state

Beispiele

https://github.com/nilshartmann/react-state-workshop

Arbeiten mit immutable Objekten

  • Zustand in React ist grundsätzlich immutable
  • Objekte (oder Arrays) im Zustand müsst ihr also kopieren und dann die Kopie bearbeiten
  • Das gilt sowohl für useState, useReducer, Redux, ...

            function BlogPost() {
              const [person, setPerson] = React.useState({});

              function handlePersonChange(newCity) {
                // verboten!
                person.address.city = newCity;

                // 🤨
                setPerson(person);
              }

            }
          

Beispiel: Arbeiten mit immutable Objekten (ES6)


        // Objekt-Spread-Operator zum Kopieren und verändern

        const newObject = {
          ...oldObject, // altes Objekt hier "einfügen"
          updatedProperty: "new value" // (ausgewählte) Werte in NEUEM Objekt verändern
        }
                    

Object-Spread-Operator kopiert nur "flach"



// Beispiel mit Verschachtelung:
const oldPerson = { name: "Klaus", address: { city: "Berlin", country: "Germany"} };                      

const newPerson = {
  ...oldPerson, // altes Objekt hier "einfügen"

  // address muss nun auch kopiert werden:
  address: { ...newPerson.address, city: "Hamburg" } 
}
            

Ändern von Arrays mit Array-Spread-Operator


            // Verändern von Arrays:

            const newArray = oldArray.map(oldEntry => {
              if (oldEntry.id === "user-1") { // dieses Element soll verändert werden
                return { ...oldEntry, name: "Klaus" }
              }
              return oldEntry; // unverändert
            });
    
            // newArray ist eine Kopie, mit ggf. veränderten Einträgen

      

Fazit: Arbeiten mit immutable Objekten (ES6)

😱 😰 😨

Immer: produce-Funktion


            import produce from "immer";

            const person = { name: "Klaus", address: { city: "Berlin", country: "Germany" }, hobbies: ["Musik"] };

            const newPerson = produce(person, draft => {
              draft.name = "Susi";
              draft.address.city = "Hamburg";
              draft.hobbies.push("Programmieren")
            })

            newPerson; // Neues Objekt!
            newPerson.name; // Susi
            newPerson.address // Neues Objekt!
            newPerson.address.city; // Hamburg
            newPerson.hobbies // Neues Array! (Musik, Programmieren)
          

Die produce-Funktion bekommt ein Objekt übergeben, sowie eine zweite Funktion, die von immer mit einem Draft aufgerufen wird.

Dieses Draft kann verändert werden

Der Rückgabe-Typ der produce-Funktion ist dann eine Kopie des alten Objektes mit den Änderungen, die auf dem Draft vorgenommen wurden.

Beispiel: Immer mit useState


            function PersonEditor() {
              const [person, setPerson] = React.useState({});
  
              function handlePersonChange(newCity) {
                const newPerson = produce(person, draft => {
                  // draft kann verändert werden, als ob es ein mutable Objekt sei
                  // und zwar auf allen Ebenen des Objekt-Graphen
                  draft.address.city = newCity;
                })
  
                // 😊
                setPerson(newPerson);
              }
            }
              
          

Beispiel: Immer mit useImmer

  • useImmer kann als Ersatz für useState verwendet werden
  • Statt einer set-Funktion wird eine update-Funktion zurückgeliefert

            function PersonEditor() {
              // useImmer statt useState
              const [person, updatePerson] = useImmer({});
  
              function handlePersonChange(newCity) {
                // updatePerson liefert ein draft für den aktuellen Zustand
                // und setzt den draft dann als neuen Zustand
                updatePerson(draft => draft.address.city = newCity)
              }
            }
          

useReducer-Hook für komplexen Zustand

Beispiel: 05_reducer/01_reducer_complete

useReducer-Hook

Eine reducer-Funktion...

  • ...erhält einen (vorherigen) Zustand und eine Action als Parameter übergeben
  • Actions sind anwendungsspezifische, beliebige JavaScript-Objekte
  • ...verarbeitet die Action
  • ...liefert dann den neuen, aktualisierten Zustand zurück
  • ...muss Seiteneffekt frei sein ("pure function")

          reducer(old_state, action) => new_state
        

Reducer sind zentrales Konzept von Redux, kommen aber auch in anderen Bereichen vor (z.B. Array.reduce)

Actions

  • Pure JavaScript-Objekte
  • Haben üblicherweise type und payload
  • Über das type-Property können sie identifiziert werden
  • Der Payload enthält Action-spezifische Daten

        const addItemAction = {
          type: "addItem",
          itemName: "Book"  // Action-spezifischer Payload (hier: Name des neuen Eintrags)
          itemQuantity: 
        }
      

Die reducer-Funktion

  • Bekommt den vorherigen Zustand übergeben und liefert neuen Zustand zurück (oder den unveränderten alten)
  • Zustand ist immutable! Kann mit immer kombiniert werden

function apiReducer(state, action) {
  return produce(state, newState => { // immer!
    switch (action.type) {
      case "addItem": 
        let currentItem = newState.find(item => item.name === action.itemName);
        if (currentItem) {
          currentItem.quantity += parseInt(action.itemQuantity);
        } else {
          // ...
          newState.push(currentItem);
        }
        case "...":
        // ...
      }
    })
  }
      

useReducer-Hook

  • Mit dem useReducer-Hook wird die reducer-Funktion in der Komponente registriert
  • Der Hook bekommt die reducer-Funktion übergeben und den initialen Zustand (ähnlich wie useState)
  • Der Hook liefert ein Array zurück mit zwei Einträgen: dem aktuellen Zustand und der dispatch-Funktion. Auch hier ähnlich wie bei useState: Zustand und Funktion zum Ändern des Zustandes
  • Mit der dispatch-Funktion können Actions an den Reducer gesendet werden
  • Der reducer aktualisiert verarbeitet die Action, liefert neuen Zustand zurück, Komponente wird neu gerendert (wie bei useState)

        function App() {
          const [items, dispatch] = React.useReducer(apiReducer, [{name: "book", quantity: 1}]);

          function handleUpdateClick() {
            dispatch({ type: "addItem", { itemName: "...", itemQuantity: 3 }});
          }

          // ...

        }
      

Beispiel

👉 examples/context/ CounterContext und App implementieren!

unterscheiden: context für globalen zustand ODER context als ersatz für render property

Context: Factory

  • Der Provider stellt ein beliebiges Objekt zur Verfügung
  • Dafür muss ein Context-Objekt mit React.createContext erzeugt werden. Dieses Objekt enthält die Provider- und die Consumer-Komponente
  • (Consumer-Komponente ist mit Hooks API irrelevant)

        import react from "React";

        const ThemeContext = React.createContext();

        // erzeugt:
        // ThemeContext.Provider 
        // ThemeContext.Consumer (irrelevant mit Hooks API)
                

Context: Provider

  • Die mit createContext erzeugte Provider Komponente erhält ein Objekt als Properties, das sie allen unterhalb liegenden Komponenten (children) zur Verfügung stellt.
  • Die Verwendung ist wie eine "normale" Komponente
  • Üblicherweise baut man sich eine Komponente darum, die dann auch die Daten hält, die über den Context zur Verfügung gestellt werden sollen
  • Auch hierbei handelt es sich um eine "normale" Komponente:

        const ThemeContext = React.createContext();

        function ThemeContextProvider(props) {
          const [themeName, setThemeName] = React.useState();


          return <ThemeContext.Provider value={{
            currentTheme: themeName,
            setCurrentTheme: setThemeName 
          }}>
          {props.children}
        </AuthContext.Provider>;
      }

Context: Consumer

Zugriff auf die Werte aus dem Context

  • In allen Komponenten unterhalb der Provider Komponente, kann mit useContext auf das bereitgestellte Objekt zugegriffen werden

function TextInput() {
const { themeName } = React.useContext(ThemeContext);

return <div className={`theme-${themeName}`}><input ... /></div>
}            
    

Wenn der Kontext sich ändert, werden alle Konsumer automatisch neu gerendert


function ThemeChooser() {
  const { setThemeName } = React.useContext(themeContext);
  return <div>
    <button onClick={() => setThemeName("dark")}>Dark Theme</button>
    <button onClick={() => setThemeName("light")}>Light Theme</button>
    </div>
}                
    

Custom Hook für Context-Zugriff

  • Du kannst einen Custom Hook für den Zugriff auf deinen Kontext bauen
  • Dann ist die Technologie (Context) gekapselt und Du hast eine "fachliche" API

            // ThemeContext.tsx
            export function useTheme() {
              return React.useContext(ThemeContext);
            }
          

            // ThemeContext.tsx
            function TextInput(props) {
              const { themeName } = useTheme();
              
              // ...
            }
          

Query Libraries

Für Daten vom Server (Caching)

  • Ein "Speziallfall" von globalen Daten
  • Daten, die vom Server gelesen werden und in der Anwednung z.B. gecached werden sollen
  • Möglicherweise werden diese Daten sogar nur zur Darstellung benötigt...
  • ...oder werden nur durch Server-Aufruf geändert, aber nicht direkt am Client

DataLibs: Examplarische Verwendung

APIs sehen bei SWR, React-Query und Apollo ähnlich aus


            // konzeptionell
            function User() {
              // Rückgabe-Wert enthält Informationen über den Request-Status
              // Parameter für useXyz ist ein Cache-Key (kann z.B. die URL sein)
              // es gibt - je nach Lib - diverse Optionen zur Konfiguration!
              const { data, loading, error } = useSWR("/api/user");

              if (loading) {
                return <h1>User is loading </h1>;
              }

              if (error) {
                return <h1>User could not be loaded</h1>
              }

              if (data) {
                return <div>...</div>
              }
  
            }

          

Redux

External Statemanagement

Beispiel: Redux

Anwendung: 👉 20_redux/2-redux_complete

Beispiel: Redux

  • Der "Klassiker" unter den State Libs
  • Mit Sicherheit höchste Verbreitung, meistes Wissen etc.
  • Exzellentes Developer Tooling
  • (Vor-)Urteile
    • viel Boilerplate-Code nötig
    • verwendetes Konzept von Higher-Order-Components schwer verständlich
    • kein Support für Data Fetching
  • Hier hat sich aber einiges getan!
  • Hooks API, Redux Toolkit

Wiederholung

Render Cycle in Pure React

Redux extrahiert die Verantwortlichkeiten

Redux im Code

👉 2-redux-workspace (PostEditor und post-slice)

Redux: Store, Reducer und Actions

  • Der Store, d.h. der globale Zustand, wird ausschließlich über reducer-Funktionen verwaltet
  • Jede reducer-Funktion verwaltet einen "Teil-Zustand" des globalen Zustandes. Die reducer können sich untereinander nicht sehen und nicht auf die anderen Teile des gloablen Zustands zugreifen. Ein Teil-Zustand wird auch als "Slice" bezeichnet (also ein Anwendungsteil)
  • Wenn Du den Store mit einer Datenbank vergleichst, wäre ein solcher Teilzustand so etwas wie eine Tabelle
  • Die reducer werden beim Starten der Anwendung in Redux registriert
  • Dadurch ist eine gute Entkopplung möglich: ein Teil der Anwendung informiert über eine Aktion ("User hat sich eingeloggt", "Theme wurde verändert") und alle interessierten Anwendungsteile können darauf reagieren

Redux: Actions auslösen

  • Actions werden an alle Reducer-Funktionen verteilt
  • Zum Auslösen einer Action gibt auch von Redux eine dispatch-Funktion, an die Du mit dem useDispatch Hook von Redux gelangst.

            function PostEditor() {
              const dispatch = useDispatch();

              function handleTitleChange(newTitle) {
                dispatch({ type: "editor/setDraftTitle", title });
              }

              // ...

              <input onChange={(e) => handleTitleChange(e.target.value) />
            }
          

Action Creator...

  • ...sind "Factory-Funktionen", die Action-Objekte erzeugen
  • ...sind optional, werden aber nahezu immer verwendet

            function setDraftTitle(newTitle) {
              return { 
                type: "editor/setDraftTitle", 
                title 
              };
            }
            }
          

Redux: Zugriff auf den globalen Zustand

  • Komponenten können aus dem globalen Zustand (Store) die Daten auswählen, die sie benötigen
  • Die Hierarchie-Ebene spielt dabei keine Rolle, weil der Zustand außerhalb, "neben" den UI Komponenten liegt
  • Nur wenn sich die ausgewählten Daten in einer Kompoente ändern, wird die Komponente neu gerendert
  • Der useSelector Hook von Redux erwartet eine Callback-Funktion, die aufgerufen wird, sobald sich irgendetwas im Store verändert hat
  • Dieser Callback-Funktion wird der komplette Store übergeben.
  • Aus dem Store wählt die Komponente die für sie relevanten Daten aus und liefert sie zurück
  • Nur wenn sich diese zurückgelieferten Daten verändert haben, wird die Komponente neu gerendert

              function PostEditor() {
                const draftTitle = useSelector(state => state.editor.draftTitle);

                //  ...
              }
            

Abgeleitete Daten

  • Berechnungen etc.
  • Du kannst in useSelector auch "abgeleitete" Daten zurückliefern, Redux vergleicht nur den von dir zurückgelieferten Wert, unabhängig davon, ob er "direkt" aus dem Store kommt oder basierend auf dem Store "berechnet" wurde

            function AppHeader() {
              // "abgeleiteter" Zustand
              const hasDraftPost = useSelector(
                state => state.editor.draftTitle !== "" || state.editor.draftBody !== ""
              );
            }

            // übergebene Selector-Callback-Funktion 
            //    wird bei JEDER Änderung des Stores ausgeführt
            // AppHeader wird nur neu gerendert, 
            //    wenn sich deren RÜCKGABEWERT ändert
          
  • reselect bietet Caching für berechnete Daten

            import { createSelector } from 'reselect'
            
            const fullnameSelector = createSelector(
              state => state.person.firstname,  // input selector 1
              state => state.person.lastname, // input selector 2
              (firstname, lastname) => `${firstname} ${lastname}`
            )
            
          
  • Nur wenn sich einer der "Input Selectors" ändert, wird die letzte Funktion ausgeführt
  • Der letzen Funktion werden die Rückgabewerte aller Selektoren übergeben
  • Die letzte Funktion berechnet dann den eigentlichen Wert

            function AppHeader() {
              const fullname = useSelector(fullnameSelector);

              return 

Good morning, {fullname}

}

Redux Dev Tools

Die Redux Dev Tools (Browser-Erweiterung) können in der Anwendung beim Konfiguration des Stores ein- oder ausgeschaltet werden

Redux Toolkit

"The official, opinionated, batteries-included toolset for efficient Redux development"

  • https://redux-toolkit.js.org/
  • Vorkonfiguriertes Setup (inklusive immer für Reducer und Thunk Middleware)
  • Vereinfacht erheblich das Arbeiten mit Reducer, generiert z.B. Action Creator zur Laufzeit
  • Vereinfacht typische Anwendungsfälle wie API Calls
  • Outstanding TypeScript support! Ihr wollt das nicht ohne TypeScript verwenden 😉
  • Neu: RTK Query: Data Fetching und Caching für Redux

Redux Toolkit Beispiel

👉👉 Beispiel: 20_redux_toolkit_workspace

👉👉 editor-slice konvertieren, PostEditor anpassen

createSlice

  • Ein "Slice" repräsentiert einen fachnlichen "Schnitt" in Eurer Anwendung
  • Ein Slice wird mit der Funktion createSlice erzeugt
  • Diese Funktion erwartet ein Konfigurationsobjekt
  • Über dieses Objekt können reducer und der initiale Zustand festgelegt werden
  • Für jede reducer-Funktion wird per Default ein action-creator automatisch generiert

            const hello = createSlice({
              name: "hello",
              initalState: { name: "world" },
              reducers: {
                greet(state, action) {
                  // ...
                }
              }
            })
          

            const dispatch = useDispatch();

            dispatch(hello.actions.greet({
              name: "World"
            }))
          

reducer

  • Wie gesehen, bekommen reducer den slice-State und die Action übergeben
  • Die reducer-Funktionen werden mit immer umschlossen, so dass der State dort mutable ist (bzw so aussieht, als ob er mutable ist)
  • Der State kann deshalb direkt bearbeitet werden oder es kann ein komplett neues Objekt zurückgegeben werden

            const hello = createSlice({
              // ...
              reducers: {
                greet(state, action) {
                  state.name = action.payload.name;
                },
                clear() {
                  return { name: "" };
                }
              }
            })
          

reducer und Action Creator

  • Zu jeder reducer-Funktion wird eine Action-Creator-Funktion erzeugt
  • Diese Funktion erwartet einen Parameter: den Action-Payload (oder gar nichts)
  • Die Funktionen liegen unterhalb von sliceName.actions und können von dort exportiert werden, so dass die Anwendung darauf Zugriff hat

            const hello = createSlice({
              // ...
              reducers: {
                
                greet(state, action) { ... },
                clear() { ... }
                }
              }
            })   
          
            export const { greet, clear } = hello.actions;
          
          

            import { greet } from "./hello-slice";

            // in der Komponente
            dispatch(greet({name: "Klaus"}));

            // ohne payload
            dispatch(clear());
          

Was nehme ich denn jetzt?

Redux vs Context

Redux hat Dev Tools, Time Travelling, Middlewares, globale Actions und Reducers/State

Redux ist sehr optimiert für Performance (häufige Updates)

Context von der API her einfacher (aber auch nicht so mächtig)

Redux lässt feingranularere Auswahl aus dem globalen Zustand zu (verhindert unnötige Renderings)

Redux vs Context

Wie entscheiden wir uns?

Entscheidung 1: Mischform: Redux und Context? Oder: Redux oder Context

Entscheidung 2: Was kommt wohin?

Auth-State (eingeloggter Benutzer)

Api-State: Netzwerk Request(s) laufen gerade

Draft-Post: editierters, neues, Post

MobX

Klassiker im neuen Gewand (Version 6 vom September 2020)

MobX

  • https://mobx.js.org/README.html
  • Claim: Simple, scalable state management.
  • Aktuelle Version und Library für React: mobx-react-lite
  • Achtung #1: mobx-react braucht ihr nicht (wenn ihr Hooks API verwendet)
  • Achtung #2: Doku teilweise veraltet, die nicht direkt auf mobx.js.org liegt!
  • Basiert auf der Idee von "observable State", der von Euren Komponenten "observiert" wird

MobX Schritt-für-Schritt

Hands-On 👉 advanced/exercices/7a-mobx

Fertig 👉 advanced/steps/7a-mobx

MobX, Beispiel #1

  • Der Zustand in MobX wird entweder in JavaScript-Objekten oder ES6-Klassen gehalten
  • Ihr könnt beides verwenden, ES6-Klassen weiter verbreitet und in der Doku auch empfohlen
  • Eine Klasse müsst ihr zum "Observer" machen

class BlogAppState = {
  blogPosts:Array<BlogPost> = [];

  constructor() {
    // Diese Instanz "observieren", MobX verfolgt jetzt alle Änderungen
    makeAutoObservable(this);
  }
}        

// Unser globaler Zustand
const blogAppState = new blogAppState();  
        
  • Eine Komponente kann zum Observer werden, und rendert sich dann bei allen Änderungen an der beobachteten Klasse:

const PostList = observer(function PostList() {
  return <div>
    {blogAppState.blogPosts.map( p => /* wie gewohnt */ )}
  </div>  
})
        

MobX: Actions

  • Der Zustand in MobX wird über Actions verändert. Dabei handelt es sich um Methoden an einer Klasse.
  • Wenn der Zustand außerhalb einer Action verändert wird, gibt es einen Fehler. Auf diese Weise erzwingt MobX eine gewisse Architektur
  • Die Action-Methoden werden per Konvention von makeAutoObservable erkannt, oder ihr gebt sie explizit an
  • Das Laden von Daten könnte z.B. eine Action sein:

class BlogAppState = {
  blogPosts:Array<BlogPost> = [];
            
  constructor() { makeAutoObservable(this); }

  async loadPosts() {
    const await response = fetch("...");
    const posts = await response.json();
    this.blogPosts = posts;
  }
}
          

// Auslösen:
function App() {
  React.useEffect(  () => { blogState.loadPosts() }, []);

  // ...
}            
          

Berechnete Werte: Computed Properties

  • MobX kann berechnete Werte cachen. Berechnete Werte basieren auf observierten Daten.
  • Beispiel: Auf Basis der Blog-Post-Liste (beobachtet) und eines Sortierkriteriums (beobachtet) wird eine sortierte Blog-Post-Liste erzeugt. Die sortierte Liste ist ein "berechneter" Wert.
  • Der Wert wird dann nur neuberechnet, wenn sich einer der darin verwendeten Werte ändert (Liste oder Kriterium).
  • Ein Computed Property ist in der Regel eine getter-Methode an einer Klasse

Berechnete Werte #2

Beispiel


class BlogAppState = {
  blogPosts:Array<BlogPost> = [];
  orderBy:string = "";
  currentUser: string = "";
            
  constructor() { makeAutoObservable(this); }

  get orderedPosts loadPosts() {
    const posts = [...blogPosts];
    posts.sort(orderBy); // vereinfacht

    return posts;
  }
}              
            

blogAppState.orderedPosts; // Liste wird initial berechnet
blogAppState.orderedPosts; // Liste wird NICHT neu berechnet (Cache!)

blogAppState.orderBy = "date";              
blogAppState.orderedPosts; // Liste wird neu berechnet

blogAppState.currentUser = "Klaus";
blogAppState.orderedPosts; // Liste wird NICHT berechnet
            

Observed Components

  • Eine Komponente, die mit dem State arbeitet, muss ein Observer werden
  • Dazu wird sie der observe-Funktion übergeben, die eine eine observed Komponente zurückliefert
  • Das ist für den Aufrufer vollständig transparent
  • Die observed Komponente wird immer dann neu gerendert, wenn sich einer der Werte verändert, auf die sie zugreift
  • Die observed Komponente rendert sich daher auch nicht neu, wenn ihre Parent-Komponente sich neu rendert
  • Ausnahme: die Parent-Kompoenten übergibt veränderte Properties

            const PostList = observer(function PostList() {
              return <div>
                {blogAppState.blogPosts.map( p => /* wie gewohnt */ )}
              </div>  
            })  
          

Bereitstellen des Zustands

  • MobX ist es egal, wo der Zustand herkommt; es ist ein beliebiges Observable
  • Typischerweise wird der Zustand in React-Anwendungen über genau einen Context in die Anwendung gereicht
  • Das Context-Objekt wird sich nie ändern, da sich nur die Werte in der Klasse verändern.
  • Deswegen wird eine konsumierende Komponente auch nie neu gerendert, weil sich der Context nicht ändert
  • Deswegen muss die Komponente ein Observer sein; wenn sich ein Wert aus dem Context ändert, rendert MobX neu

Bereitstellen des Zustands - Beispiel


const context = {
  blog: new BlogAppState(),
  auth: new AuthenticationState()
}
function StoreProvider({ children }) {
  return (
    <StoreContext.Provider value={context}>
      {children}
    </StoreContext.Provider>
  );
}

export function useStore() {
  return React.useContext(StoreContext);
}
          

const PostList = observe(function PostList() {
  const { blog } = useStore();

  // PostList wird nur neu gerendert, wenn sich BlogPost-Liste ändert
  return blog.posts.map(<BlogPost id={...});
})           
          

MobX vs. Redux

  • Redux eher funktionale Programmierung, MobX eher OO
  • Viel Freiheiten mit MobX...
  • ...aber auch viel "Magie"
  • In großen Modellen manchmal schwer zu verstehen, was eigentlich wo und wie "observed" wird
  • Observer-Komponenten eher untypisch für React (noch)
  • Developer Tooling nicht so gut wie Redux
  • Doku, StackOverflow etc. deutlich weniger umfänglich als Redux
  • Arbeiten mit asynchronen Daten in MobX "natürlicher", Thunk Actions sehr ungewöhnlich
  • Computed Properties in MobX sehr einfach, re-select gewöhnungsbedürftig
  • 👉 Beide Bibliotheken gute Wahl und können verwendet werden
  • 👉 "Für Redux ist noch niemand entlassen worden"

Recoil


          const itemsState = atom({
            key: 'shoppingListItems',
            default: [
              { name: "Book", 
                quantitiy: 1
              }
            ],
          });

          function App() {
            const [items, setItems] = useRecoilState(itemsState);

            // render list of items
          }
        

Recoil: Selektoren

  • Berechnete Daten werden über Selektoren zur Verfügung gestellt
  • Ein Selektor kann Atome bestimmten, von denen er abhängi ist. Wenn eines der Atome sich ändert, wird der Wert des Selektors neu bestimmt:

            const itemsState = atom({ /* ... */ }); // wie gesehen
            const oderByState = atom({ key: "orderBy", default: "desc" });

            const orderedItemsSelector = selector({
              key: 'orderedItems',
              get: ({get}) => {
                const allItems = get(itemsState);
                const orderBy = get(orderByState);
               
                return ... /* allItems basierend auf orderBy sortieren */
              }
            });

            function OrderedList() {
              const orderedItems = useRecoilValue(orderedItemsSelector);

              // sortierte Liste rendern
            }
                        
          

Recoil: asynchrone Daten

  • Atome und Selektoren können asynchron sein
  • Beispiel: ein asynchroner Selektor, der Daten lädt, sobald sich eine Id ändert

            const blogPostIdState = atom({ key: "blogPostId", default: null }); 
            const blogPostSelector = selector({
              key: "blogPost",
              get: async ({get}) => {
                const postId =  get(blogPostIdState)  // wenn sich Id ändert, wird Selector neu ausgeführt
                const data = await fetch(`/api/posts/${postId}).json(); // vereinfacht
                return data;
              },
            })
          
  • Recoil arbeitet bereits mit React Suspense zusammen:

            function BlogPostPage() {
              const blogPost = useRecoilValue(blogPostSelector);

              return ...;
            }

            function App() {
              return   <Suspense fallback={"Blog Post is loading"}>
                <BlogPostPage />
              </Suspense>
            }
          

Geschafft! 😊

Vielen Dank für Eure Teilnahme!

Happy Statemanagement 🌻

Wenn ihr noch Fragen habt, könnt ihr mich gerne kontaktieren:

Mail: nils@nilshartmann.net

Web: https://nilshartmann.net

Twitter: @nilshartmann