https://nilshartmann.net / @nilshartmann
Freiberuflicher Software-Entwickler, Berater und Trainer aus Hamburg
function BlogPost() {
const [person, setPerson] = React.useState({});
function handlePersonChange(newCity) {
// verboten!
person.address.city = newCity;
// 🤨
setPerson(person);
}
}
// 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
}
// 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" }
}
// 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
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.
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);
}
}
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)
}
}
Beispiel: 05_reducer/01_reducer_complete
Eine reducer-Funktion...
reducer(old_state, action) => new_state
Reducer sind zentrales Konzept von Redux, kommen aber auch in anderen Bereichen vor
(z.B. Array.reduce
)
const addItemAction = {
type: "addItem",
itemName: "Book" // Action-spezifischer Payload (hier: Name des neuen Eintrags)
itemQuantity:
}
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 "...":
// ...
}
})
}
function App() {
const [items, dispatch] = React.useReducer(apiReducer, [{name: "book", quantity: 1}]);
function handleUpdateClick() {
dispatch({ type: "addItem", { itemName: "...", itemQuantity: 3 }});
}
// ...
}
👉 examples/context/ CounterContext und App implementieren!
unterscheiden: context für globalen zustand ODER context als ersatz für render property
React.createContext
erzeugt werden.
Dieses Objekt enthält die Provider- und die Consumer-Komponente
import react from "React";
const ThemeContext = React.createContext();
// erzeugt:
// ThemeContext.Provider
// ThemeContext.Consumer (irrelevant mit Hooks API)
createContext
erzeugte Provider Komponente erhält ein Objekt als
Properties, das sie allen unterhalb liegenden Komponenten (children
) zur
Verfügung stellt.
const ThemeContext = React.createContext();
function ThemeContextProvider(props) {
const [themeName, setThemeName] = React.useState();
return <ThemeContext.Provider value={{
currentTheme: themeName,
setCurrentTheme: setThemeName
}}>
{props.children}
</AuthContext.Provider>;
}
Zugriff auf die Werte aus dem Context
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>
}
// ThemeContext.tsx
export function useTheme() {
return React.useContext(ThemeContext);
}
// ThemeContext.tsx
function TextInput(props) {
const { themeName } = useTheme();
// ...
}
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>
}
}
Anwendung: 👉 20_redux/2-redux_complete
👉 2-redux-workspace (PostEditor und post-slice)
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) />
}
function setDraftTitle(newTitle) {
return {
type: "editor/setDraftTitle",
title
};
}
}
useSelector
Hook von Redux erwartet eine Callback-Funktion, die
aufgerufen wird, sobald sich irgendetwas im Store verändert hat
function PostEditor() {
const draftTitle = useSelector(state => state.editor.draftTitle);
// ...
}
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
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}`
)
function AppHeader() {
const fullname = useSelector(fullnameSelector);
return Good morning, {fullname}
}
Die Redux Dev Tools (Browser-Erweiterung) können in der Anwendung beim Konfiguration des Stores ein- oder ausgeschaltet werden
"The official, opinionated, batteries-included toolset for efficient Redux development"
👉👉 Beispiel: 20_redux_toolkit_workspace
👉👉 editor-slice konvertieren, PostEditor anpassen
createSlice
erzeugt
const hello = createSlice({
name: "hello",
initalState: { name: "world" },
reducers: {
greet(state, action) {
// ...
}
}
})
const dispatch = useDispatch();
dispatch(hello.actions.greet({
name: "World"
}))
const hello = createSlice({
// ...
reducers: {
greet(state, action) {
state.name = action.payload.name;
},
clear() {
return { name: "" };
}
}
})
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());
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)
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
Klassiker im neuen Gewand (Version 6 vom September 2020)
Hands-On 👉 advanced/exercices/7a-mobx
Fertig 👉 advanced/steps/7a-mobx
class BlogAppState = {
blogPosts:Array<BlogPost> = [];
constructor() {
// Diese Instanz "observieren", MobX verfolgt jetzt alle Änderungen
makeAutoObservable(this);
}
}
// Unser globaler Zustand
const blogAppState = new blogAppState();
const PostList = observer(function PostList() {
return <div>
{blogAppState.blogPosts.map( p => /* wie gewohnt */ )}
</div>
})
makeAutoObservable
erkannt,
oder ihr gebt sie explizit an
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() }, []);
// ...
}
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
observe
-Funktion übergeben, die eine eine observed
Komponente zurückliefert
const PostList = observer(function PostList() {
return <div>
{blogAppState.blogPosts.map( p => /* wie gewohnt */ )}
</div>
})
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={...});
})
const itemsState = atom({
key: 'shoppingListItems',
default: [
{ name: "Book",
quantitiy: 1
}
],
});
function App() {
const [items, setItems] = useRecoilState(itemsState);
// render list of items
}
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
}
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;
},
})
function BlogPostPage() {
const blogPost = useRecoilValue(blogPostSelector);
return ...;
}
function App() {
return <Suspense fallback={"Blog Post is loading"}>
<BlogPostPage />
</Suspense>
}
Wenn ihr noch Fragen habt, könnt ihr mich gerne kontaktieren:
Mail: nils@nilshartmann.net
Twitter: @nilshartmann