React Advanced

Topics

  • Reducer Hook und State Management mit Reducern
  • Immutability-Hilfslibraries
  • Benutzerauthentifizierung mit einem Identity Provider
  • Progressive Web Apps
  • React Native
  • Refs
  • "Wrapper" für bestehende Elemente
  • HOCs
  • Internationalisierung

State Management mit Reducern

State Management

In komplexeren Anwendungen oder Komponenten macht es Sinn, den Anwendungszustand (model) von der Ansicht (view) zu trennen.

Oft wird der gesamte Anwendungszustand durch ein Datenmodell repräsentiert. Jede Änderung am Anwendungszustand läuft über das Datenmodell.

State Management Tools

  • reducer Hook (in React beinhaltet, sehr ähnlich zu Redux)
  • Redux (basiert auf Reducern, oft mit React verwendet)
  • MobX (oft mit React verwendet, einfacher als Reducer)
  • recoil (basiert auf React Hooks, 2020 von facebook veröffentlicht)
  • ngrx (mit Angular verwendet)
  • vuex (mit Vue.js verwendet)

State Management Tools

Redux devtools showing the state of the airbnb website
Redux Devtools, die den komplexen State der airbnb-Website anzeigen

State Management mit Actions

Beim Reducer Hook, Redux, ngrx und vuex wird jede State-Änderung durch eine Action ausgelöst, die durch ein JavaScript-Objekt repräsentiert wird

Anmerkung: vuex verwendet den Begriff Mutation

State Management mit Actions

In Redux / Reducer Hook:

  • Actions werden durch JavaScript-Objekte repräsentiert
  • Actions haben immer eine type-Property
  • Actions haben meist auch eine payload-Property

State Management mit Actions

Beispiele für Actions:

{
  "type": "addTodo",
  "payload": "learn React"
}
{
  "type": "deleteTodo",
  "payload": 1
}
{
  "type": "deleteCompletedTodos"
}

State Management mit Reducern

Konzept von Redux und Reacts Reducer Hook:

  • eine Stateänderung erfolgt über eine Reducer-Funktion
  • die Reducer-Funktion erhält den aktuellen State und eine Action
  • die Reducer-Funktion gibt den neuen State zurück (ohne den alten State zu verändern)

Reducer Diagramm

state action reducer

Beispiel: Todos State Management

Manuelle Verwendung eines Reducers:

const state1 = [
  { id: 1, title: 'groceries', completed: false },
  { id: 2, title: 'taxes', completed: true },
];
const actionA = { type: 'addTodo', payload: 'gardening' };
const state2 = todosReducer(state1, actionA);
const actionB = { type: 'deleteTodo', payload: 1 };
const state3 = todosReducer(state2, actionB);
console.log(state3);
/* [{ id: 2, title: 'taxes', completed: true },
    { id: 3, title: 'gardening', completed: false },] */

Beispiel: Todos State Management

Implementierung eines Reducers:

const todosReducer = (oldState, action) => {
  switch (action.type) {
    case 'addTodo':
      return [
        ...oldState,
        {
          title: action.payload,
          completed: false,
          id: Math.max(0, ...oldState.map((t) => t.id)) + 1,
        },
      ];
    case 'deleteTodo':
      return oldState.filter(
        (todo) => todo.id !== action.payload
      );
    default:
      throw new Error('unknown action type');
  }
};

Beispiel: Todos State Management

Verwendung mit TypeScript:

type TodosState = Array<Todo>;

type TodosAction =
  | { type: 'addTodo'; payload: string }
  | { type: 'deleteTodo'; payload: number };

const todosReducer = (
  state: TodosState,
  action: TodosAction
): TodosState => {
  // ...
};

Reducer kombinieren

Reducer können einfach kombiniert / aufgesplittet werden um komplexen / verschachtelten State zu verwalten

Beispiel für State:

{
  "todoData": {
    "status": "loading",
    "todos": []
  },
  "uiData": {
    "newTitle": "re",
    "filterText": ""
  }
}

Reducer kombinieren

Reducer-Implementierung:

const rootReducer = (rootState, action) => ({
  todoData: todoDataReducer(rootState.todoData, action),
  uiData: uiDataReducer(rootState.uiData, action),
});

const uiDataReducer = (uiData, action) => ({
  newTitle: newTitleReducer(uiData.newTitle, action),
  filterText: filterTextReducer(uiData.filterText, action),
});

const newTitleReducer = (newTitle, action) => {
  if (action.type === 'setNewTitle') {
    return action.payload;
  } else if (action.type === 'addTodo') {
    return '';
  } else {
    return newTitle;
  }
};

Reducer kombinieren

Bei kombinierten Reducern verwaltet ein einzelner Reducer nur einen Teil des States; aber jeder Reducer erhält jede Action und kann darauf reagieren

Reducer Hook

Reducer Hook

Zum State Management mit Hooks können wir das bekannte useState oder nun auch useReducer verwenden:

const [state, dispatch] = useReducer(reducer, initialState);

Konkretes Beispiel count:

const [count, countDispatch] = useReducer(countReducer, 0);

Reducer Hook

Aufruf von useReducer gibt ein Array mit zwei Einträgen zurück:

  • aktueller State
  • eine dispatch-Funktion, mit der Actions ausgelöst werden können

Reducer Hook

const TodoApp = () => {
  const [todos, dispatch] = useReducer(
    todosReducer,
    initialTodos
  );

  return (
    <div>
      ...
      <button
        onClick={() => dispatch({ type: 'deleteAll' })}
      >
        delete all todos
      </button>
    </div>
  );
};

Reducer Hook

Die mächtigen Redux devtools können mit dem Reducer Hook verwendet werden: https://github.com/troch/reinspect (benötigt etwas Konfiguration, manuelles Dispatchen von Actions ist nicht möglich)

Immutability-Hilfslibraries

Immutability-Hilfslibraries

direktes Arbeiten mit unveränderlichem State kann kompliziert sein

Hilfslibraries:

  • immer.js (commonly used with Redux)
  • immutable.js

immer.js

import produce from 'immer';

const todos = [
  // ...
];
const newTodos = produce(todos, (todosDraft) => {
  todosDraft[0].completed = true;
  todosDraft.push({ title: 'study', completed: false });
});

GraphQL and Apollo

GraphQL and Apollo

https://www.apollographql.com/docs/react/

GraphQL and Apollo

Gründe für die Verwendung:

  • Automatisches Senden von Queries über das Netzwerk
  • Automatisches Caching
  • Automatische Einbindung in das (Re)rendering von React

Installation

Benötigte npm-Pakete:

  • graphql
  • graphql-tag
  • apollo-client
  • apollo-cache-inmemory
  • apollo-link-http
  • react-apollo (für Verwendung mit React)

Setup

import { ApolloClient } from 'apollo-client';
import { InMemoryCache } from 'apollo-cache-inmemory';
import { HttpLink } from 'apollo-link-http';
import gql from 'graphql-tag';

const client = new ApolloClient({
  cache: new InMemoryCache(),
  link: new HttpLink({
    uri: 'https://api.spacex.land/graphql/',
  }),
});

Beispiel für eine Abfrage

// via a tagged template string
const LAUNCHES_QUERY = gql`
  query recentLaunches {
    launchesPast(limit: 10) {
      mission_name
    }
  }
`;

client
  .query({ query: LAUNCHES_QUERY })
  .then((result) => console.log(result));

Lokale Daten

Apollo kann auch lokale Daten / lokalen State verwalten

Setzen von lokalem State:

  • via client.writeData für einfache Fälle
  • mittels @client-Direktive in Mutationen, und lokalen Resolvern

Auslesen von lokalem State:

  • mittels @client-Direktive in Queries

Lokale Daten

Einfaches direktes Setzen von lokalem State (ähnlich wie Reacts setState):

const client = useApolloClient();

client.writeData({ data: { inputText: '' } });

Lokale Daten

lokale Resolver für Mutationen:

https://www.apollographql.com/docs/react/data/local-state/#local-resolvers

Lokale Daten

Auslesen von lokalem State (via @client):

const INPUT_TEXT_QUERY = gql`
  query {
    inputText @client
  }
`;

client
  .query({ query: INPUT_TEXT_QUERY })
  .then((result) => console.log(result));

Apollo Client Developer Tools

Erweiterung für Chrome

Laut Bewertungen unzuverlässig (3.2 / 5 Sternen)

Funktionen:

  • Betrachten des aktuellen Caches
  • Inspizieren der Struktur von Queries / Mutationen
  • Ausführen von Queries (und Mutationen)

React with GraphQL and Apollo

React with GraphQL and Apollo

https://www.apollographql.com/docs/react/data/queries/

React mit einem Apollo Client verbinden

Eine Anwendung kommuniziert meist mit einem einzigen API

import { ApolloProvider } from 'react-apollo';
<ApolloProvider client={client}>
  <App />
</ApolloProvider>

Definition einer Query

const LAUNCHES_QUERY = gql`
  query recentLaunches {
    launchesPast(limit: 10) {
      mission_name
    }
  }
`;

useQuery

function RecentLaunches() {
  const { data, loading, error } = useQuery(LAUNCHES_QUERY);
  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error!</div>;

  return (
    <div>
      <h1>Launches</h1>
      {data.launchesPast.map((launch) => (
        <div>{launch.mission_name}</div>
      ))}
    </div>
  );
}

useQuery: Parameter

const LAUNCHES_QUERY = gql`
  query recentLaunches($numLaunches: Int!) {
    launchesPast(limit: $numLaunches) {
      mission_name
    }
  }
`;

function RecentLaunches({ numLaunches }) {
  const { data, loading, error } = useQuery(
    LAUNCHES_QUERY,
    { variables: { numLaunches } }
  );
  ...
}

useQuery: Polling & Refetching

Daten alle 5 Sekunden aktualisieren:

const { data, loading, error } = useQuery(LAUNCHES_QUERY, {
  pollInterval: 5000,
});

Funktion, deren Aufruf ein neues Laden der Daten bewirkt:

const { data, loading, error, refetch } = useQuery(
  LAUNCHES_QUERY
);
...
refetch()

useMutation

Beispielfall Todo:

const SET_COMPLETED = gql`
  mutation setCompleted($id: ID!, $completed: Boolean!) {
    updateTodo(id: $id, input: { completed: $completed }) {
      id
      completed
    }
  }
`;

useMutation

Gundlegende Verwendung:

const [setCompleted] = useMutation(SET_COMPLETED);

Ausführliche Form (vgl. useState):

const [
  setCompleted,
  { data, loading, error },
] = useMutation(SET_COMPLETED);

Der State wird am Server und danach auch lokal entsprechend abgeändert

useMutation

Update des lokalen Caches:

  • automatisch, falls ein zuvor existierendes Objekt geändert wurde
  • manuell, falls Objekte in einem Array hinzugefügt / entfernt wurden

useMutation: manuelles Update des Caches

Zugriff auf cache und API-Antwort in der update-Funktion:

const [addTodo] = useMutation(ADD_TODO, {
  update: (cache, reply) => {
    // cache: local cache
    // reply: reply from the API
    console.log(cache);
    console.log(reply);
    // TODO: update the local cache based on the reply
  },
});

useMutation: manuelles Update des Caches

const [addTodo] = useMutation(ADD_TODO, {
  update: (cache, reply) => {
    // get old todos from cache
    const oldTodos = cache.readQuery({ query: GET_TODOS })
      .todos;
    // build newTodos array based on the server response
    const newTodos = [...oldTodos, reply.data.createTodo];
    // TODO: update the local cache with the newTodos array
  },
});

useMutation: manuelles Update des Caches

const [addTodo] = useMutation(ADD_TODO, {
  update: (cache, reply) => {
    const oldTodos = cache.readQuery({ query: GET_TODOS })
      .todos;
    const newTodos = [...oldTodos, reply.data.createTodo];
    cache.writeQuery({
      query: GET_TODOS,
      data: { todos: newTodos },
    });
  },
});

Benutzer-Authentifizierung mit einem Identity Provider

Identity Provider

Ein Identity Provider kann die Identität eines Benutzers überprüfen (kann den Benutzer authentifizieren)

Beispiele:

der aktuelle Endnutzer ist auf dieser Domain als Benutzer "foo" eingeloggt

der aktuelle Endnutzer ist al Benutzer "x" bei Google / als Benutzer "y" bei Facebook authentifiziert

Identity Provider

Mechanismus für den Benutzer:

Benutzer klickt auf login, wird zu einer Login-Seite weitergeleitet und nach erfolgreichem Login zur ursprünglichen Seite zurückgeleitet

im Hintergrund erhält der Benutzer ein Identity Token, einen kleinen Datensatz, der die Identität des Benutzers im Zusammenspiel mit dem Identity Provider belegen kann

Identity Provider

Standards:

  • Authentifizierung via OpenID Connect
  • Authorisierung via OAuth2

Auth0

Auth0 (auth-zero) ist ein weit verbreiteter Identity Provider

unterstützt Authentifizierung mittels "interner" Acccounts oder externer Identity Provider (z.B. Google, Facebook, Apple, ...)

Auth0: Registrierung und Einrichtung

  • registriere dich für einen Auth0-Account unter https://auth0.com
  • in der Sidebar, wähle "Applications"
  • wähle die "default application" oder erstelle eine neue "Single Page Web Application"; der gewählte Name wird Benutzern bei der Authentifizierung angezeigt

Auth0: Registrierung und Einrichtung

Application Settings:

  • erlaubte Redirect-Ziele nach dem Login: Allowed Callback URLs
  • erlaubte Redirect-Ziele nach dem Logout: Allowed Logout URLs
  • um Authentifizierungstokens zu erneuern: Allowed Web Origins

für die lokale Entwicklung, setze alle drei Werte auf http://localhost:3000

Auth0: Registrierung und Einrichtung

unter Settings:

jeder Auth0-Klient hat zumindest eine domain (z.B. dev-xxxxxxxx.eu.auth0.com)

jede App hat eine bestimmte client ID (z.B. jA0EAwMCxamDRMfOGV5gyZPnyX1BBPOQ)

auth0-react

Library: auth0-react

npm-Paket: @auth0/auth0-react

Einbinden eines Providers, der einen Context verwaltet:

<Auth0Provider
  domain="YOUR_AUTH0_DOMAIN"
  clientId="YOUR_AUTH0_CLIENT_ID"
  redirectUri={window.location.origin}
>
  <App />
</Auth0Provider>

(siehe nächste Slide für Implementierung in next.js)

auth0-react

bei Verwendung von next.js:

// pages/_app.js
export default function App({ Component, pageProps }) {
  return (
    <Auth0Provider
      domain="YOUR_AUTH0_DOMAIN"
      clientId="YOUR_AUTH0_CLIENT_ID"
      redirectUri="YOUR_URL"
    >
      <Component {...pageProps} />
    </Auth0Provider>
  );
}

auth0-react

const auth = useAuth0();

if (auth.isLoading) {
  return <div>...</div>;
} else if (auth.error) {
  return <div>...</div>;
} else if (!auth.isAuthenticated) {
  return (
    <button onClick={auth.loginWithRedirect}>Log in</button>
  );
} else {
  return (
    <div>
      main content
      <button onClick={auth.logout}>log out</button>
    </div>
  );
}

auth0-react

Einträge im Rückgabewert von useAuth0:

  • isLoading
  • error
  • isAuthenticated
  • loginWithRedirect
  • logout
  • user (user.sub, user.email, user.username, user.name, ...)

auth0-react

Aufgabe: Erstelle eine React-Anwendung, bei nur authentifizierte Benutzer auf den Inhalt zugreifen können; Name / e-mail des Benutzers sollen wenn vorhanden angezeigt werden

auth0-react

Authentifizierung kann mithilfe von Access Tokens verifiziert werden (in diesem Fall sind dies keine JWT Tokens)

weitere Funktionalität von useAuth0:

  • getAccessTokenSilently
  • getAccessTokenWithPopup

auth0-react

Ausführen eines Requests mit dem Access Token:

async function makeRequestSilently() {
  const token = await auth.getAccessTokenSilently();
  console.log(`make API request with token ${token}`);
}

auth0-react

Verifizieren des Auth0-Tokens auf Seite des APIs:

const auth0Domain = 'dev-xxxxxxxx.eu.auth0.com';

try {
  const res = await fetch(
    `https://${auth0Domain}/userinfo`,
    {
      headers: {
        'Content-Type': 'application/json',
        Authorization: `Bearer ${token}`,
      },
    }
  );
  const userInfo = await res.json();
  console.log(`authenticated as ${userInfo.sub}`);
} catch {
  console.log('error');
}

Bemerkung: das reine Senden der Information (z.B. Benutzer-ID) vom Client ist nicht sicher, da dies vom API nicht verifiziert werden kann

Ressourcen

PWAs

Progressive Web Apps mit React

PWAs

Progressive Web Apps: Möglichkeit, Anwendungen für Mobilgeräte und PCs mit HTML, CSS und JavaScript zu schreiben

PWAs

create-react-app kann Projekte mit PWA-Unterstützung erstellen:

npx create-react-app myapp --template cra-template-pwa
npx create-react-app myapp --template cra-template-pwa-typescript

Codesandbox beinhaltet grundlegende Unterstützung für PWAs

PWAs

PWA-Grundlagen in create-react-app-Projekten:

  • Konfiguration in public/manifest.json
  • PWA-Boilerplate in src/serviceWorker.js

PWAs: Aktivierung

in index.js / index.tsx:

serviceWorker.register();

PWAs: Konfiguration

Via public/manifest.json

PWA: add to homescreen

Prozess in Chrome:

  • warten, bis eine Installationsdialog angezeigt werden darf
  • anzeigen eines Buttons o.ä., der die Installation anbietet
  • beim Anklicken des Buttons via Chrome den Installationsdialog anzeigen

siehe auch: https://developers.google.com/web/fundamentals/app-install-banners/

PWA: add to homescreen

TypeScript Implementierung:

const [canInstall, setCanInstall] = useState(false);
const installPromptEventRef = useRef<Event>();

const getInstallPermission = () => {
  window.addEventListener(
    'beforeinstallprompt',
    (ipEvent) => {
      ipEvent.preventDefault();
      installPromptEventRef.current = ipEvent;
      setCanInstall(true);
    }
  );
};
useEffect(getInstallPermission, []);

PWA: add to homescreen

TypeScript Impementierung:

<button
  disabled={!canInstall}
  onClick={() => {
    (installPromptEventRef.current as any).prompt();
  }}
>
  install
</button>

PWA: Deployment

React Native

React Native

Mit React Native können React Anwendungen für iOS- und Android-Geräte erstellt werden

Möglichkeiten zur Entwicklung

  • Expo: einfache Option, schneller Einstieg
  • React Native CLI: ermöglicht Integration nativer Module (Java / Objective-C)

Expo Tools

  • Expo Snack: online Editor
  • Expo CLI: lokale Entwicklung
  • Expo App: Emulator für live-Testen auf Android / iOS (erhältlich in App Stores)

Expo Snack

https://snack.expo.io

Optionen:

  • Web-Version ausführen
  • Android / iOS online emulieren (begrenzte Kapazität)
  • am lokalen Gerät ausführen (via Expo App)

Expo CLI

Installation:

npm install -g expo-cli

Erstellen eines neuen Projekts:

expo init myproject

Ausführen eines Projektes (öffnet Dashboard auf localhost:19002):

npm run start

Expo CLI

Ausfürhen auf einem Gerät:

  • Auswählen von tunnel
  • warten
  • Scannen des QR Codes mit der Expo App

React Native Komponenten

  • View (=div)
  • Text
  • Image
  • Button
  • TextInput
  • ScrollView

ausführlichere Liste

React Native Komponenten

Beispiele:

<Button title="press me" onPress={handlePress} />
<TextInput value={myText} onChangeText={setMyText} />

Styling

In React Native geschieht Styling über die style-Property:

const TodoItem = ({ title, completed }) => (
  <View style={{ margin: 5, padding: 8 }}>
    <Text>{title}</Text>
  </View>
);

Styling

Die style-Property kann auch ein Array von Objekten erhalten (Einträge, die falsy sind, werden ignoriert)

const TodoItem = ({ title, completed }) => (
  <View
    style={[
      { padding: 8, backgroundColor: 'lightcoral' },
      completed && { backgroundColor: 'lightgrey' },
    ]}
  >
    <Text>{title}</Text>
  </View>
);

Styling

Erstellen von stylesheets, die mehrere gruppierte Stile definieren:

import { StyleSheet } from 'react-native';

const styles = StyleSheet.create({
  todoItem: {
    padding: 8,
    backgroundColor: 'lightcoral',
  },
  completedView: {
    backgroundColor: 'lightgrey',
  },
  completedText: {
    textDecoration: 'line-through',
  },
});

Styling

Verwendung von Stylesheets:

const TodoItem = ({ title, completed, onToggle }) => (
  <View
    style={[
      styles.todoItem,
      completed && styles.completedView,
    ]}
  >
    <Text style={[completed && styles.completedText]}>
      {completed ? 'DONE: ' : 'TODO: '}
      {title}
    </Text>
  </View>
);

Plattformspezifischer Code

Möglichkeit 1 (einfache Fälle):

import { Platform } from 'react-native';
if (Platform.OS === 'web') {
  // 'web' / 'ios' / 'android'
}

Möglichkeit 2 (Plattform-spezifische Komponenten):

  • AddTodo.web.js
  • AddTodo.ios.js
  • AddTodo.android.js

"Wrapper" für bestehende Elemente

"Wrapper" für bestehende Elemente

Beispiel: eine Button-Komponente, die einen button mit zusätzlichem Styling rendert

Die Button-Komponente soll die gleichen Properties haben wie das button-Element

"Wrapper" für bestehende Elemente

<Button type="submit" disabled={true}>
  foo
</Button>

sollte rendern:

<button type="submit" disabled={true} className="Button">
  foo
</button>

"Wrapper" für bestehende Elemente

Implementierung:

function Button(props: ComponentProps<'button'>) {
  // return a "button" element with one extra CSS class
  return <button {...props} className="Button" />;
}

Zusätzliche Properites

Beispiel: Komponente mit einer zusätzlichen Property

type Props = ComponentProps<'input'> & {
  label: string;
};

function InputWithLabel({ label, ...rest }: Props) {
  return (
    <label>
      {label}: <input {...rest} />
    </label>
  );
}

Weiterleiten der ref-Property

zusätzlicher Wunsch: Die ref-Property der Button-Komponente soll auf des gerenderte button-Element verweisen

Weiterleiten der ref-Property

const Button = forwardRef<
  HTMLButtonElement,
  ComponentProps<'button'>
>((props, ref) => {
  return <button {...props} ref={ref} className="Button" />;
});

Higher-order components

Funktionen, die eine Komponentendefinition verändern

Higher-order components

verwirrende Terminologie:

Eine higher-order component (HOC) ist keine Komponente 😲

Eine HOC ist eine Funktion, die eine Komponentendefinition verändert / erweitert (ein "Komponenten-Decorator")

Higher-order components

Beispiel:

Reacts memo ist eine HOC

Es erhält eine Komponente und gibt eine memoisierte Komponente zurück:

const MemoizedRating = memo(Rating);

Higher-order components

Beispiel:

connect aus react-redux gibt eine HOC zurück:

// connector is a HOC
const connector = connect(
  mapStateToProps,
  mapDispatchToProps
);

Die entstehende HOC erhält eine normale Komponente und gibt eine Komponente zurück, die mit dem Redux Store verbunden ist:

const RatingContainer = connector(Rating);

Render props

Render props

render props: Entwurfsmuster, das es einem Elternelement erlaubt, zusätzliche Daten bereitzustellen, wenn ein Unterelement gerendert wird

das Elternelement erhält eine "Anleitung", um die Daten darzustellen - diese Anleitung wird als Funktion übergeben

Render props

Wunschdenken - wenn das doch funktionieren würde:

<DataLoader resource="https://jsonplaceholder.typicode.com/todos">
  <DataViewer />
</DataLoader>

eine Möglichkeit, es umzusetzen:

<DataLoader
  resource="https://jsonplaceholder.typicode.com/todos"
  render={(data) => <DataViewer data={data} />}
/>

Render props

vollständiges Beispiel: DataLoader

<DataLoader
  resource="https://jsonplaceholder.typicode.com/todos"
  renderLoading={() => <div>loading</div>}
  renderError={(error) => <div>Error: ...</div>}
  render={(data) => <DataViewer data={data} />}
/>

Render props

Beispiel: DataTable

<DataTable
  data={todos}
  filter={(todo) => !todo.completed}
  renderRow={(todo) => <tr>{todo.title}</tr>}
/>

Render props

Beispiel: formik-Library

<Formik
  initialValues={/*...*/}
  onSubmit={/*...*/}
  validate={/*...*/}
  children={(props) => <Form>...</Form>}
/>

Internationalisierung

Internationalisierung

Libraries:

  • react-i18next (basiert auf i18next)
  • react-intl

i18next - Grundlagen

// src/i18n.ts

import i18next from 'i18next';
import { initReactI18next } from 'react-i18next';

i18next - Grundlagen

Setup mit Inline-Daten:

// src/i18n.ts

i18next.use(initReactI18next).init({
  debug: true,
  lng: 'en',
  resources: {
    en: {
      translation: { hello: 'Hello!', sign_in: 'Sign in' },
    },
    de: {
      translation: { hello: 'Hallo!', sign_in: 'Anmelden' },
    },
  },
});

i18next - Grundlagen

Einbinden in das Projekt in src/index.tsx:

// ...
import './i18n';
// ...

Ausprobieren der Funktionalität in src/index.tsx:

import i18next from 'i18next';

console.log(i18next.t('hello'));
console.log(i18next.t('sign_in', { lng: 'de' }));

i18next - Grundlagen

Zugriff auf die Ãœbersetzungsfunktion mit einem Hook:

function SignInButton() {
  const { t } = useTranslation();

  return <button>{t('sign_in')}</button>;
}

i18next - Vertiefende Themen

  • Ãœbersetzungsdateien und Hosting am Server
    • Laden via suspense
  • Namespaces
  • Interpolation
  • Mehrzahl
  • Ändern der Sprache

Ressourcen:

Portale

Portale

Portale: erlauben es, HTML-Elemente zu rendern, die "außerhalb" der rendernden Komponente liegen

Portals

Beispiel: Dialog-Komponente

Ein Dialog kann aus einer beliebigen Komponente gerendert werden - das Rendering wird ein Unterelement des HTML Body-Elements werden

function Dialog(props: { children: React.ReactNode }) {
  return ReactDOM.createPortal(
    <div style={dialogStyle}>{props.children}</div>,
    document.querySelector('body') as HTMLBodyElement
  );
}

Error Boundaries

Error Boundaries

Ziel:

Abfangen von Laufzeitfehlern in einer Anwendung, um stattdessen "schöne" Fehlermeldungen für Benutzer zu zeigen

Error Boundaries

Beispiel: Abfangen von Laufzeitfehlern für die ganze Anwendung

<MyErrorBoundary>
  <App />
</MyErrorBoundary>

Error Boundaries

Error Boundary Komponenten können nur als Klassenkomponenten implementiert werden

Error Boundaries fangen folgende Fehler in Unterkomponenten ab:

  • Fehler im Rendering-Code / JSX
  • Fehler in Lifecycle-Methoden / Effect-Hooks

Error Boundaries

Implementierung einer ErrorBoundary-Komponente:

type Props = { children: React.ReactNode };
type State = { hasError: boolean };
class ErrorBoundary extends Component<Props, State> {
  constructor(props: Props) {
    super(props);
    this.state = { hasError: false };
  }
  static getDerivedStateFromError(error: Error): State {
    return { hasError: true };
  }
  render() {
    if (this.state.hasError) {
      return <div>Something went wrong ...</div>;
    }
    return this.props.children;
  }
}