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.
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
In Redux / Reducer Hook:
Beispiele für Actions:
{
"type": "addTodo",
"payload": "learn React"
}
{
"type": "deleteTodo",
"payload": 1
}
{
"type": "deleteCompletedTodos"
}
Konzept von Redux und Reacts Reducer Hook:
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 },] */
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');
}
};
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 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-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;
}
};
Bei kombinierten Reducern verwaltet ein einzelner Reducer nur einen Teil des States; aber jeder Reducer erhält jede Action und kann darauf reagieren
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);
Aufruf von useReducer
gibt ein Array mit zwei Einträgen zurück:
const TodoApp = () => {
const [todos, dispatch] = useReducer(
todosReducer,
initialTodos
);
return (
<div>
...
<button
onClick={() => dispatch({ type: 'deleteAll' })}
>
delete all todos
</button>
</div>
);
};
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)
direktes Arbeiten mit unveränderlichem State kann kompliziert sein
Hilfslibraries:
import produce from 'immer';
const todos = [
// ...
];
const newTodos = produce(todos, (todosDraft) => {
todosDraft[0].completed = true;
todosDraft.push({ title: 'study', completed: false });
});
Gründe für die Verwendung:
Benötigte npm-Pakete:
graphql
graphql-tag
apollo-client
apollo-cache-inmemory
apollo-link-http
react-apollo
(für Verwendung mit React)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/',
}),
});
// 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));
Apollo kann auch lokale Daten / lokalen State verwalten
Setzen von lokalem State:
client.writeData
für einfache Fälle@client
-Direktive in Mutationen, und lokalen ResolvernAuslesen von lokalem State:
@client
-Direktive in QueriesEinfaches direktes Setzen von lokalem State (ähnlich wie Reacts setState
):
const client = useApolloClient();
client.writeData({ data: { inputText: '' } });
lokale Resolver für Mutationen:
https://www.apollographql.com/docs/react/data/local-state/#local-resolvers
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));
Erweiterung für Chrome
Laut Bewertungen unzuverlässig (3.2 / 5 Sternen)
Funktionen:
Eine Anwendung kommuniziert meist mit einem einzigen API
import { ApolloProvider } from 'react-apollo';
<ApolloProvider client={client}>
<App />
</ApolloProvider>
const LAUNCHES_QUERY = gql`
query recentLaunches {
launchesPast(limit: 10) {
mission_name
}
}
`;
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>
);
}
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 } }
);
...
}
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()
Beispielfall Todo:
const SET_COMPLETED = gql`
mutation setCompleted($id: ID!, $completed: Boolean!) {
updateTodo(id: $id, input: { completed: $completed }) {
id
completed
}
}
`;
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
Update des lokalen 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
},
});
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
},
});
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 },
});
},
});
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
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
Standards:
Auth0 (auth-zero) ist ein weit verbreiteter Identity Provider
unterstützt Authentifizierung mittels "interner" Acccounts oder externer Identity Provider (z.B. Google, Facebook, Apple, ...)
Application Settings:
für die lokale Entwicklung, setze alle drei Werte auf http://localhost:3000
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)
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)
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>
);
}
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>
);
}
Einträge im Rückgabewert von useAuth0
:
isLoading
error
isAuthenticated
loginWithRedirect
logout
user
(user.sub
, user.email
, user.username
, user.name
, ...)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
Authentifizierung kann mithilfe von Access Tokens verifiziert werden (in diesem Fall sind dies keine JWT Tokens)
weitere Funktionalität von useAuth0
:
getAccessTokenSilently
getAccessTokenWithPopup
Ausführen eines Requests mit dem Access Token:
async function makeRequestSilently() {
const token = await auth.getAccessTokenSilently();
console.log(`make API request with token ${token}`);
}
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
Progressive Web Apps mit React
Progressive Web Apps: Möglichkeit, Anwendungen für Mobilgeräte und PCs mit HTML, CSS und JavaScript zu schreiben
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
PWA-Grundlagen in create-react-app-Projekten:
public/manifest.json
src/serviceWorker.js
in index.js
/ index.tsx
:
serviceWorker.register();
Via public/manifest.json
Prozess in Chrome:
siehe auch: https://developers.google.com/web/fundamentals/app-install-banners/
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, []);
TypeScript Impementierung:
<button
disabled={!canInstall}
onClick={() => {
(installPromptEventRef.current as any).prompt();
}}
>
install
</button>
npm run build
Mit React Native können React Anwendungen für iOS- und Android-Geräte erstellt werden
Optionen:
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
Ausfürhen auf einem Gerät:
Beispiele:
<Button title="press me" onPress={handlePress} />
<TextInput value={myText} onChangeText={setMyText} />
In React Native geschieht Styling über die style-Property:
const TodoItem = ({ title, completed }) => (
<View style={{ margin: 5, padding: 8 }}>
<Text>{title}</Text>
</View>
);
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>
);
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',
},
});
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>
);
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
Beispiel: eine Button
-Komponente, die einen button
mit zusätzlichem Styling rendert
Die Button
-Komponente soll die gleichen Properties haben wie das button
-Element
<Button type="submit" disabled={true}>
foo
</Button>
sollte rendern:
<button type="submit" disabled={true} className="Button">
foo
</button>
Implementierung:
function Button(props: ComponentProps<'button'>) {
// return a "button" element with one extra CSS class
return <button {...props} className="Button" />;
}
Beispiel: Komponente mit einer zusätzlichen Property
type Props = ComponentProps<'input'> & {
label: string;
};
function InputWithLabel({ label, ...rest }: Props) {
return (
<label>
{label}: <input {...rest} />
</label>
);
}
zusätzlicher Wunsch: Die ref-Property der Button
-Komponente soll auf des gerenderte button
-Element verweisen
const Button = forwardRef<
HTMLButtonElement,
ComponentProps<'button'>
>((props, ref) => {
return <button {...props} ref={ref} className="Button" />;
});
verwirrende Terminologie:
Eine higher-order component (HOC) ist keine Komponente 😲
Eine HOC ist eine Funktion, die eine Komponentendefinition verändert / erweitert (ein "Komponenten-Decorator")
Beispiel:
Reacts memo
ist eine HOC
Es erhält eine Komponente und gibt eine memoisierte Komponente zurück:
const MemoizedRating = memo(Rating);
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: 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
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} />}
/>
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} />}
/>
Beispiel: DataTable
<DataTable
data={todos}
filter={(todo) => !todo.completed}
renderRow={(todo) => <tr>{todo.title}</tr>}
/>
Beispiel: formik-Library
<Formik
initialValues={/*...*/}
onSubmit={/*...*/}
validate={/*...*/}
children={(props) => <Form>...</Form>}
/>
Libraries:
// src/i18n.ts
import i18next from 'i18next';
import { initReactI18next } from 'react-i18next';
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' },
},
},
});
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' }));
Zugriff auf die Ãœbersetzungsfunktion mit einem Hook:
function SignInButton() {
const { t } = useTranslation();
return <button>{t('sign_in')}</button>;
}
Ressourcen:
Portale: erlauben es, HTML-Elemente zu rendern, die "außerhalb" der rendernden Komponente liegen
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
);
}
Ziel:
Abfangen von Laufzeitfehlern in einer Anwendung, um stattdessen "schöne" Fehlermeldungen für Benutzer zu zeigen
Beispiel: Abfangen von Laufzeitfehlern für die ganze Anwendung
<MyErrorBoundary>
<App />
</MyErrorBoundary>
Error Boundary Komponenten können nur als Klassenkomponenten implementiert werden
Error Boundaries fangen folgende Fehler in Unterkomponenten ab:
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;
}
}