Side Effects

Side Effects

Wenn sich Komponenten-Props bzw State ändern oder die Komponente zum ersten Mal eingebunden wird:

"Main Effect": Komponente wir mit aktuellen Daten (neu) gerendert

mögliche "Side Effects": Auslösen von API-Abfragen, Speichern von Daten, Explizite Änderungen am Dokument, Starten von Timern, ...

Side Effects

typische Fälle von Side Effects:

  • Auslösen von API-Anfragen
    • wenn eine Komponente zum ersten Mal eingebunden wird
    • wenn sich bestimmte Daten (State / Props) geändert haben (z.B. wenn der Benutzer ein bestimmtes Element auswählt, um dessen Details zu sehen)
  • Speichern von Daten in localStorage, wenn sie sich geändert haben
  • Explizite Änderungen am Dokument (DOM)
  • Starten von Timern
  • ...

Side Effects

manche Side Effects müssen später "aufgeräumt" werden:

  • Abbrechen von API-Anfragen, wenn sie nicht mehr benötigt werden (z.B. wenn sich ein Suchbegriff erneut geändert hat)
  • Stoppen von Timern

Side Effects

Implementierung:

in Funktionskomponenten: mittels des Effect Hooks

in Klassenkomponenten: mit Hilfe von Lifecycle-Methoden

Side Effects in Klassenkomponenten

Side Effects in Klassenkomponenten

Drei wichtige Methoden zum Abfragen von Ereignissen im Lebenszyklus einer Komponente:

  • componentDidMount
  • componentDidUpdate
  • componentWillUnmount

Side Effects in Klassenkomponenten

componentDidUpdate bekommt die vorhergehenden Props als Parameter übergeben:

class MyComponent extends Component {
  // ...
  componentDidUpdate(prevProps, prevState) {
    if (this.state.foo !== prevState.foo) {
      doSomething();
    }
  }
}

Side Effects in Klassenkomponenten

Beispiel: Komponente, die Wechselkurse zwischen zwei Währungen lädt:

class ExchangeRate extends Component {
  // ...
  componentDidMount() {
    this.loadExchangeRate();
  }
  componentDidUpdate(prevProps, prevState) {
    if (
      this.state.from !== prevState.from ||
      this.state.to !== prevState.to
    ) {
      this.loadExchangeRate();
    }
  }
}

Side Effects in Klassenkomponenten

Erweitertes Beispiel: Abbrechen veralteter API-Anfragen

class ExchangeRate extends Component {
  // ...
  componentDidMount() {
    this.loadExchangeRate();
  }
  componentDidUpdate(prevProps, prevState) {
    if (
      this.state.from !== prevState.from ||
      this.state.to !== prevState.to
    ) {
      this.cancelPreviousQuery();
      this.loadExchangeRate();
    }
  }
  componentWillUnmount() {
    this.cancelPreviousQuery();
  }
}

Effect Hook

Effect Hook

Der Effect Hook kann verwendet werden, um Aktionen zu setzen, wenn eine Komponente zum ersten Mal eingebunden wird, oder wenn sich deren Props / State geändert haben

useEffect(
  effect, // what should happen
  dependencies // array of values to watch
);

Die Effekt-Funktion nach dem (Re-)Rendering einer Komponente ausgeführt, falls sich eine der Abhängigkeiten geändert hat

Effect Hook

Beispiel: Laden von Umrechnungskursen, wenn die Komponente zum ersten Mal eingebunden wird oder wenn sich eine Währung ändert:

const [from, setFrom] = useState('USD');
const [to, setTo] = useState('EUR');
const [rate, setRate] = useState<number | null>(null);
async function loadRate(from: number, to: number) {
  // ...
}
useEffect(() => {
  loadRate(from, to);
}, [from, to]);

Effect Hook

Beispiel: Laden von Todos, wenn die Komponente zum ersten Mal eingebunden wird:

const [todos, setTodos] = useState([]);
async function loadTodos() {
  // ...
}
useEffect(() => {
  loadTodos();
}, []);

Effect nach jedem Rendering

Wenn kein zweiter Parameter übergeben wird, wird die Funktion nach jedem Rendering ausgeführt; dies kann eventuelle Probleme mit veralteten Daten verhindern

useEffect(() => {
  ensureExchangeRateIsLoaded();
});

Effect Hook: Cleanup

Cleanup-Funktionen

Eine Effect-Funktion kann eine "Cleanup-Funktion" zurückgeben

Diese Funktion wird ausgeführt, bevor der Effect zum nächsten Mal läuft oder bevor die Komponente entfernt wird

Cleanup-Funktionen

Beispiel: Komponente, die veraltete Queries abbricht:

function ExchangeRate() {
  // ...
  useEffect(() => {
    async function loadExchangeRate() {
      // ... (initiate a new query)
    }
    function cancel() {
      // ... (cancel this query)
    }
    loadExchangeRate();
    return cancel;
  }, [from, to]);
  // ...
}

Asynchrone Effects und Cleanup-Funktionen

Eine Effect-Funktion darf keine asynchrone Funktion sein

Hintergrund:

ein eventueller Rückgabewert einer Effect-Funktion wird immer als Cleanup-Funktion interpretiert

asynchrone Funktionen geben immer Promises zurück

Asynchrone Effects und Cleanup-Funktionen

ungültig:

useEffect(loadSearchResultsAsync, [query]);

gültig:

useEffect(() => {
  loadSearchResultsAsync();
}, [query]);

Beispiele und Ãœbungen: APIs

Beispiel: Laden von Wechselkursen

Beispiel: Laden von Wechselkursen von einem API, wenn sich die ausgewählten Währungen ändern:

function ExchangeRate() {
  const [from, setFrom] = useState('USD');
  const [to, setTo] = useState('EUR');
  const [rate, setRate] = useState(null);
  async function loadRate(from: string, to: string) {
    setRate(null);
    const rate = await fetchExchangeRate(from, to);
    setRate(rate);
  }
  useEffect(() => {
    loadRate(from, to);
  }, [from, to]);
  // render two dropdowns for selecting currencies
  // and show the exchange rate
}

Beispiel: Laden von Wechselkursen

Funktion, die Daten lädt:

async function fetchExchangeRate(
  from: string,
  to: string
): Promise<number> {
  const res = await fetch(
    `https://api.exchangerate.host/convert?from=${from}&to=${to}`
  );
  const data = await res.json();
  return data.result;
}

Beispiel: Laden von Wechselkursen

vollständiger Code (Klassenkomponenten und Funktionskomponenten, inklusive "cleanup"):

https://codesandbox.io/s/side-effects-exchange-rate-2z42d

Ãœbungen

Beispiele für abfragbare APIs:

Ãœbungen

  • Lade Todos, wenn der Benutzer die Todolist-Anwendung öffnet
  • Zeige Daten zu einem bestimmten SpaceX-Start basierend auf der Startnummer
  • Zeige Daten zu einem bestimmten Pokémon basierend auf der Nummer
  • Zeige Hacker News Artikel basierend auf einem Suchbegriff

Beispiele und Ãœbungen: andere

Beispiele und Ãœbungen

Beispiele und Ãœbungen, die nicht mit APIs interagieren

  • Clock
  • Counter, der in localStorage speichert
  • Komponente, die den Dokumententitel "rendert"
  • Logout Timer

Beispiel: Clock-Komponente

function Clock() {
  const [time, setTime] = useState(new Date());
  useEffect(() => {
    const intervalId = setInterval(() => {
      setTime(new Date());
    }, 1000);
    return () => clearInterval(intervalId);
  }, []);
  return <div>{time.toLocaleTimeString()}</div>;
}

Beispiel: Counter

Counter, der seinen Wert in localStorage speichert, wenn sich dieser ändert:

function PersistentCounter() {
  const [count, setCount] = useState(null);
  function loadCount() {
    const lsCount = localStorage.getItem('count');
    if (lsCount !== null) {
      setCount(Number(lsCount));
    } else {
      setCount(0);
    }
  }
  function saveCount() {
    if (count !== null) {
      localStorage.setItem('count', count);
    }
  }
  useEffect(loadCount, []);
  useEffect(saveCount, [count]);
  return (
    <button onClick={() => setCount(count + 1)}>
      {count}
    </button>
  );
}

Ãœbungen

Ãœbung: Speichere den state einer der vorigen Anwendungen, (z.B. slideshow) in localStorage

Beispiel: DocumentTitle-Komponente

Wir erstellen eine Komponente, die den Dokumenttitel dynamisch setzen kann:

<DocumentTitle value="my custom title" />

Diese Komponente kann irgendwo in der React-Anwendung vorkommen.

Beispiel: DocumentTitle-Komponente

const DocumentTitle = (props) => {
  useEffect(() => {
    document.title = props.value;
  }, [props.value]);
  return null;
};

Beispiel: Logout-timer

Benutzer wird nach 10 Sekunden Inaktivität automatisch ausgeloggt:

const App = () => {
  const [loggedIn, setLoggedIn] = useState(true);
  const [lastInteraction, setLastInteraction] = useState(
    new Date()
  );
  // restart the logout timer on user interaction
  useEffect(() => {
    const logout = () => setLoggedIn(false);
    const timeoutId = setTimeout(logout, 10000);
    return () => clearTimeout(timeoutId);
  }, [lastInteraction]);
  return (
    <button onClick={() => setLastInteraction(new Date())}>
      {loggedIn
        ? 'click to stay logged in'
        : 'logged out automatically'}
    </button>
  );
};