Slide 1

Slide 1 text

Gianluca Costa ️ Introduction to Redux with TypeScript Latest update: 2023-02-14

Slide 2

Slide 2 text

Foreword Elegance always matters - especially when crafting software - and patterns exist precisely as inspiring principles that lead us to methodically tackling complexity and creating clean architectures. State management is one of the most essential and nuanced problems in the IT domain - and Redux, inspired by Elm and Flux, elegantly mixes beautiful concepts such as events and reducers to provide a simple and effective solution via a functional approach. 2

Slide 3

Slide 3 text

About this presentation Our journey stems from my passion for the concepts at the heart of Redux: we are going to discover, with no claim of completeness, how Redux can be a brilliant tool for state management - especially when combined with the static type checking provided by TypeScript. I have also created a few minimalist code projects - available in the companion GitHub repository - to showcase different aspects of the Redux ecosystem. This work is designed to be a concise reference: should you need a more gradual approach, live coding sessions or real-life projects, please consider online courses. For the latest and most complete version of the documentation, please refer to Redux's official website. 3

Slide 4

Slide 4 text

Overview The main parts of this presentation can be briefly summarized as follows: 1. Enter Redux: introductory considerations before sailing 2. Essential building blocks: core aspects of Redux, packaged as redux on NPM 3. Redux Toolkit: simplified development with the @reduxjs/toolkit package 4. React Redux: to elegantly combine React and Redux 5. Conclusion: parting thoughts 4

Slide 5

Slide 5 text

Part 1 Enter Redux

Slide 6

Slide 6 text

What is Redux? « Redux is a pattern and library for managing and updating application state » Minimalist library providing just a core implementation Redux does not depend on a specific major library or framework, such as React or Angular: it can be referenced by any JavaScript project, including backend projects and potentially even command-line applications The ecosystem provides a wide variety of extensions and developer tools 6

Slide 7

Slide 7 text

Global state in one store The core idea is a single, centralized store containing the global state: Any modification to the state can only be performed indirectly, by sending messages to the store Each message is processed according to specific business rules and creates a new state for the store 7

Slide 8

Slide 8 text

Why use Redux? Single source of truth - the store Trackable modifications Expressive code Scalable, message-based architecture Simplified data access in complex UIs Time-travel debugging 8

Slide 9

Slide 9 text

Time-travel debugging 9

Slide 10

Slide 10 text

Vibrant ecosystem The very heart of Redux consists of the redux package, designed to be extensible Redux Toolkit, packaged as @reduxjs/toolkit , exports sensible defaults and minimalist constructs React Redux provides seamless integration with React Redux DevTools: time-travelling debugger and other tools ...and much more! Redux is designed to be extensible! 10

Slide 11

Slide 11 text

Not every app needs Redux Every architectural choice has drawbacks; in the case of Redux: boilerplate code to support the architecture - especially without Redux Toolkit additional layers of indirection developers must choose what part of the state is global - typically in React apps the learning curve, which could be initially demanding 11

Slide 12

Slide 12 text

Best scenarios for Redux The more of the following factors are present in your application, the more effective Redux will be: Multiple, heterogeneous sources triggering state updates Chaotic state fragmentation at different levels Complex, strongly domain-related update logic Message-/Event-driven architecture Vast, shared codebase 12

Slide 13

Slide 13 text

Part 2 Essential building blocks

Slide 14

Slide 14 text

The architecture at a glance 14

Slide 15

Slide 15 text

Section 2.1 Store, actions, reducers

Slide 16

Slide 16 text

The store « The store is an object that holds the application's state tree. There should only be one single store in a Redux app » The store provides just a basic set of methods - in particular: getState() : returns the current state dispatch(action) : sends an action (a message) to the store, always triggering the computation of the next state. There's no other way to change the state in the store subscribe(listener) : adds a listener that gets notified right after the computed next state becomes the store state. Returns a function to cancel the subscription 16

Slide 17

Slide 17 text

The state must be serializable The state contained in the store can range from a primitive value up to a complex object graph having arbitrary depth. One should store only serializable objects as well as primitive values - to ensure portability in a variety of situations such as persistence and debugging. In particular, the state should definitely not contain: class instances functions promises It is usually recommended that entities - i.e., domain objects having an ID, be stored in a dedicated state branch and referenced by ID from within other branches 17

Slide 18

Slide 18 text

Store creators « A store creator is a function that creates a Redux store » The essential store creator is createStore() , provided by Redux and supporting 3 args: reducer : the reducer (also known as root reducer) used by the store - a function returning the new state whenever an action is dispatched initial_state (optional): if missing or set to undefined, the reducer will use the default value of its state parameter enhancer (optional): higher-order function that takes a store creator and returns a modified store creator; the most common enhancer is applyMiddleware() , to apply middleware extensions 18

Slide 19

Slide 19 text

Deprecation warning for createStore() createStore() is a basic store creator - effective for introductory examples. It is nowadays deprecated, as the recommended store creator has become configureStore() - provided by Redux Toolkit and described in the related section of this presentation. Existing code should still compile until future major versions - but it is definitely advisable to start adopting the new approach 19

Slide 20

Slide 20 text

Just a single store? « It is possible to create multiple distinct Redux stores, but the intended pattern is to have only a single store » Considering that the Redux ecosystem revolves around the one-store principle, it is generally advisable to adhere as much as possible to this guideline 20

Slide 21

Slide 21 text

Actions « An action is a plain object that represents an intention to change the state » Actions can be arbitrary JavaScript objects, with just one requirement: they must have a type field, that should be a specific string (not a symbol), such as in: const ADD_BEAR = "bears/add"; interface AddBearAction { type: typeof ADD_BEAR; name: string; } As shown above, type constants often has / characters to conventionally organize the actions into logical trees; however, Redux code is usually based on mere string equality 21

Slide 22

Slide 22 text

Action creators « An action creator is a function creating a specific action type » An action creator is conventionally the only way to create an action - as it ensures that the action's fields are correctly initialized. This is a typical pattern for creating the action seen in the previous slide: export function addBear(name: string): AddBearAction { return { type: ADD_BEAR, name }; } 22

Slide 23

Slide 23 text

Feature-related action type It is recommended to define a dedicated type unifying all the interfaces related to the same set of actions in the domain; for example: type BearAction = AddBearAction | ClearBearsAction; 23

Slide 24

Slide 24 text

Reducers « A reducer is a pure function that receives the current store state and an action, and must return the new state » Reducers are pure functions - they cannot have side effects. In particular: reducers cannot modify their arguments - especially the state reducers cannot depend on the context - e.g, reading/writing global variables reducers cannot perform side-effects (deleting files, calling impure functions, ...) reducers cannot contain async logic - such as REST API calls reducers must be deterministic they cannot rely on any external environment or even random number generators or Date.now() - use action creators instead 24

Slide 25

Slide 25 text

Writing a basic reducer export function bearReducer( state: ReadonlyArray = [], //The default arg is the initial state action: BearAction //This is why we have defined the BearAction type ): ReadonlyArray { //A switch is frequent, but not mandatory switch (action.type) { case ADD_BEAR: return [...state, { name: action.name }]; case CLEAR_BEARS: return []; default: //You must return the current state by default return state; } } 25

Slide 26

Slide 26 text

The principle of state immutability « Redux expects that all state updates are done immutably » Never perform - in plain Redux and plain TS - something like this within a reducer: state.myField = 90; Instead, to change one or more fields, you must create a new object holding the state - for example, via the spread syntax State immutability provides a wide range of benefits, such as: code safety: no unexpected state modifications performances: Redux is based on shallow equality checking 26

Slide 27

Slide 27 text

Errors in reducers « By default, errors thrown within a reducer bubble up the call stack, thus terminating the computation induced by dispatch() » Should reducers contain try/catch ? Most probably no, because: syntax errors - especially in vanilla JS/ES - should emerge as quickly as possible domain errors should be expressed via dedicated state fields in lieu of throwing Finally, the default behaviour can be arbitrarily customized by introducing middleware - a topic we are going to discuss in the next section 27

Slide 28

Slide 28 text

Redux basics in action Please, refer to the GitHub project 28

Slide 29

Slide 29 text

Combining reducers The store demands exactly one reducer - named root reducer; however, dealing with a huge, monolithic state within a single function would be rather inconvenient « You split the root reducer into multiple, independent sub- reducers - each operating on different parts of the state object » This can be achieved in many ways - and mainly via the combineReducers() function: const rootReducer = combineReducers({ bears: bearReducer, rangers: rangerReducer }); 29

Slide 30

Slide 30 text

Understanding reducer combination Whenever a compound reducer is called, the following logical steps occur: 1. it receives the current state and the dispatched action 2. it creates a new object having the same fields as the object passed to combineReducers() : i. each sub-reducer is called with the value of the related field in the compound state - but also with the action passed to the compound reducer ii. the result of each sub-reducer is assigned to its related field in the new object 3. The return value of the compound reducer will be: the new object - if at least one sub-reducer returns a different reference the current state otherwise 30

Slide 31

Slide 31 text

One action, potentially multiple effects In the previous example: bearReducer() computes the value for the bears field of the state rangerReducer() computes the value for the rangers field of the state Despite this output differentiation, each reducer receives every single dispatched action; consequently: « Different reducers can react to the same action type » For example, different parts of the domain model could react differently to the same global event 31

Slide 32

Slide 32 text

Where should the business logic reside? In action creators: +: traditionally easier approach for beginners -: actions become thick objects with duplicated business logic -: the effects of action creators are not affected by time-travel debugging In reducers (recommended): +: single source of truth for computations +: pure functions foster code robustness 32

Slide 33

Slide 33 text

Setters and events In Redux, one dispatches actions - that are, technically speaking, messages. In semantic terms, however, messages can be interpreted as: setters - like bears/add - that describe commands to be executed by the system events - like bears/added - which denote something that already happened and that might trigger business logic within the system Redux is technically unopinionated, and different architectural styles might have different visions - not to mention a hybrid approach: what matters is to be aware of the semantic distinction 33

Slide 34

Slide 34 text

Selectors « Selectors are pure functions that know how to extract specific pieces of information from the store state » function getBearNames(state: ReadonlyArray): ReadonlyArray { return state.bears.map(bear => bear.name); } Selectors are essential in terms of minimalist design, as they: prevent duplicated domain logic in different parts of the system can express calculated properties, thus preventing redundant data in the store Memoization libraries like reselect can remarkably enhance selector performances 34

Slide 35

Slide 35 text

Section 2.2 Middleware

Slide 36

Slide 36 text

Middleware alters the dispatch process « Middleware provides a third-party extension point between dispatching an action, and the moment it reaches the reducer » In particular, middleware: exists for the lifetime of the store can see all dispatched actions and dispatch actions by itself can customize the dispatch process 36

Slide 37

Slide 37 text

Async actions Async actions are action instances that are not serializable - and would not be accepted by a reducer: for example, a function, or a Promise . « Async action can be dispatched - but only serializable actions should enter the reducer » More precisely, it is the middleware chain that detects async actions and dispatches actions accordingly 37

Slide 38

Slide 38 text

Middleware in detail « A middleware is a higher-order function that composes a dispatch function to return a new dispatch function » The redux package exports two fundamental type definitions: type MiddlewareAPI = { dispatch: Dispatch; getState: () => State }; represents a minimalist view of the store - with just dispatch() and getState() type Middleware = (api: MiddlewareAPI) => (next: Dispatch) => Dispatch; Middleware needs to be plugged into the store: by design, you can only do so when creating the store - by passing the list of middlewares to applyMiddleware() 38

Slide 39

Slide 39 text

Applying middleware One of the first problems with Redux is logging: in particular, store subscribers can only access the store state, not the actions. The solution is to apply middleware when creating the store - e.g. via redux-logger : import { createStore, applyMiddleware } from "redux" import { createLogger } from "redux-logger" const logger = createLogger({...}) //One can pass a variety of options const reducer = ... export const createCustomStore = () => createStore( reducer, applyMiddleware(logger) ) 39

Slide 40

Slide 40 text

Redux core - extended example Please, refer to the GitHub project 40

Slide 41

Slide 41 text

Middleware in the ecosystem Advanced reducers: reduce-reducers, redux-ignore, reduxr-scoped-reducer Sophisticated listening: redux-watch, redux-subscribe, ... Asynchronous actions: Redux Thunk, Redux-Saga, ... Action batching: redux-batched-actions, redux-batch Subscription: redux-batched-subscribe Logging: redux-logger, redux-log-slow-reducers Debugging: Redux DevTools 41

Slide 42

Slide 42 text

Creating middleware Creating one's custom middleware via a factory method is actually fairly straightforward - especially when using curried notation for lambdas: import { MiddlewareAPI, Dispatch, Action, Middleware } from "redux"; export function createCustomMiddleware< TAction extends Action >(/*Custom params used to create the middleware*/): Middleware { //This series of chained lambdas represents the middleware return (store: MiddlewareAPI) => (next: Dispatch) => (action: TAction) => { //TODO: Add here the actual code of the middleware //Call next(action) to pass the action to the next mw in the chain. //You can also call store.dispatch() with any new action. }; } 42

Slide 43

Slide 43 text

Custom middleware in action Please, refer to the GitHub project 43

Slide 44

Slide 44 text

Part 3 Redux Toolkit

Slide 45

Slide 45 text

Effective minimalism « Redux Toolkit makes it easier to write good Redux applications and speeds up development, by baking in our recommended best practices, providing good default behaviors, catching mistakes, and allowing you to write simpler code » It is available as a package via NPM: @reduxjs/toolkit 45

Slide 46

Slide 46 text

Section 3.1 Enhanced store and middleware

Slide 47

Slide 47 text

Battery-included store Redux Toolkit provides an enhanced store creator - configureStore() , taking an options object having the following core fields: reducer : can be either a reducer or the object that would be passed to combineReducers() preloadedState : optional initial state devTools - enables/disables/configures Redux DevTools in development only: if true (the default), Redux DevTools will have a predefined configuration if a DevToolsOptions instance, it will be used to configure Redux DevTools 47

Slide 48

Slide 48 text

Middleware in bundle Redux Toolkit's configureStore() automatically applies middleware to the new store - especially Redux Thunk; additionally, development and production have different default lists of applied middleware. Middleware can be customized by passing another field to configureStore() 's options - middleware - which can be: an array of middleware - in lieu of calling applyMiddleware() a higher-order function, taking the getDefaultMiddleware() function and returning, especially via the prepend() and concat() methods, an array of middleware instances - if you want to add middleware to the default list 48

Slide 49

Slide 49 text

Composable and efficient selectors Redux Toolkit exports createSelector() - actually provided by the reselect package. To create a selector, you can: just define it as a function (state) => T , with arbitrary T use createSelector() , which takes: an array of source selectors, defined in either way - ensuring composability a function whose arguments will be the actual return values of the sources and that must compute the selector's value This kind of compound selectors relies on memoization for efficiency: the value returned by a compound selector stays the same as long as so do the sources 49

Slide 50

Slide 50 text

Section 3.2 Simplified actions and reducers

Slide 51

Slide 51 text

Simplified action creators createAction(type: string, [prepare]) returns an action creator with: toString() : the action type - so, there is no more need for separate string constants match(action) : true if the action type matches - very useful in type guards Furthermore, this action creator returns a PayloadAction - an interface extending Action and having the additional payload field - standardizing action payloads all over Redux Toolkit 51

Slide 52

Slide 52 text

Generating a custom payload The prepare argument in createAction() is optional: if prepare is omitted, the action creator takes just one argument - defined by the TPayload type argument of createAction() if the type argument is omitted as well, it defaults to undefined - and the action creator will take no argument otherwise, prepare must be a function taking any argument list and returning an object of type {payload: TPayload} - where TPayload is arbitrary The actual creator will automatically add the type field 52

Slide 53

Slide 53 text

createAction() in action Please, refer to the GitHub project 53

Slide 54

Slide 54 text

Simplified and flexible reducers createReducer(initialState, (builder) => {}) simplifies the implementation of a reducer via a fluent builder in lieu of the traditional switch construct. A few aspects are definitely worth noting: the very action creators produced by createAction() should be the case argument of builder.addCase(case, (state[, action]) => state) : this ensures type checking for each function passed as the second argument Each subreducer can modify the state! Because createReducer() internally uses the immer library to actually manipulate a proxy wrapped around the state a default case can be specified via the builder, but it is not mandatory - the current state is returned by default 54

Slide 55

Slide 55 text

createReducer() in action Please, refer to the GitHub project 55

Slide 56

Slide 56 text

Section 3.3 Slices

Slide 57

Slide 57 text

Creating a slice createSlice() is the simplest and most elegant way to create both the actions and the reducer for a branch of the state - replacing also createAction() and createReducer() : const bearSlice = createSlice({ name: "bears", //The prefix for all the related action types initialState: [{name: "Yogi"}], reducers: { //This creates both the action creator and the related reducer case bearAdded(state: Bear[], action: PayloadAction) { //The action is optional state.push({name: action.payload}) //Fake mutability via Immer } bearsCleared(state: Bear[]) { state.length = 0 } } }) 57

Slide 58

Slide 58 text

Exporting from a slice Once you have created a slice within a module, you can export both the action creators and the overall reducer: export const { addBear, clearBears } = bearSlice.actions; export const bearReducer = bearSlice.reducer; The action creators can be imported and called as usual The reducer takes into account the cases declared within the slice and can in turn become a sub-reducer passed to a combining function such as combineReducers() 58

Slide 59

Slide 59 text

Decoupled action creator and reducer in a slice You can actually decouple the action creator and the reducer in a slice as well: type AddBearPayload = {name: string, token: number} (...) reducers: { addBear: { //This works just like prepare in createAction() prepare: (name: string) => { //As usual, you definitely mustn't specify the "type" field here return { payload: { name, token: DateTime.now() } } }, //This works just like a case in createReducer() //Static typing ensures that action is PayloadAction reducer: (state, action) => { state.push({name: action.name}) } } 59

Slide 60

Slide 60 text

Extra reducers In slices, each reducer acts like a switch case applied to its specific action type, because of the coupling with action creators provided by createSlice() . However, a slice might need to logically handle actions whose creators are not defined in the slice itself. The options passed to createSlice() can have another, optional field, extraReducers : its value must be a lambda taking just a builder which works like in createReducer() . Ultimately, both reducers and extraReducers are combined into the slice's reducer 60

Slide 61

Slide 61 text

Slices in action Please, refer to the GitHub project 61

Slide 62

Slide 62 text

Section 3.4 Redux Thunk via Toolkit

Slide 63

Slide 63 text

Is Redux synchronous? By design, Redux is synchronous: you cannot add asynchronous logic to reducers, as they must be pure functions you could add async logic to action creators, but that is not recommended both the state and the action received by the reducer must be serializable - so promises and functions cannot be dispatched However, Redux Thunk - that configureStore() applies by default - alters the dispatch process to support asynchronous operations by dispatching thunks 63

Slide 64

Slide 64 text

Thunks « A thunk is a function that wraps an expression to delay its evaluation, such as () => 42 » In the case of Redux Thunk, a thunk is a higher-order function receiving two arguments, actually referencing the store methods: dispatch() getState() You usually return thunks from thunk creators - functions whose arguments customize the thunk behavior 64

Slide 65

Slide 65 text

Dispatching thunks Dispatching a thunk runs its code, which can include: reading the current state of the store dispatching actions or even other thunks performing one or more await , if the thunk is an async function The body of the thunk is arbitrary - and it is executed as soon as it is dispatched 65

Slide 66

Slide 66 text

Typical thunk workflow Most often, async operations follow a dedicated pattern: 1. Dispatch an action notifying that the operation is pending: this usually alters the state so that the application's view displays some Waiting... component 2. Perform arbitrary operations, that might also involve await on promises 3. Finally, dispatch an action notifying: The success of the operations - with the related payload, which is usually the result of the computation process The failure of the operations - maybe with the related error In both cases, the UI is updated accordingly 66

Slide 67

Slide 67 text

Simplified thunk creation When TypeScript's static type checking is enforced, even the implementation of a basic pattern for asynchronous operations via Redux Thunk can become exponentially cumbersome. Consequently, Redux Toolkit exports the createAsyncThunk() function to define a thunk creator, which in turn creates a thunk that: executes an arbitrary block of code - the actual logic, encapsulated within a Promise keeps track of the async action lifecycle by dispatching the related actions - pending , fulfilled and rejected - all sharing the same prefix 67

Slide 68

Slide 68 text

Using createAsyncThunk() createAsyncThunk() , in a fairly basic form, takes the following arguments: the type prefix shared by the 3 workflow actions a factory function - whose optional arguments are: arg : the argument that could be passed to the thunk creator thunkAPI - object with essential APIs such as dispatch() and getState() The thunk creator returned by the function has 3 properties - pending , fulfilled and rejected - that reference the action creators, as returned by createAction() , of the lifecycle actions - so they can be used in reducer cases 68

Slide 69

Slide 69 text

Thunks in action Please, refer to the GitHub project 69

Slide 70

Slide 70 text

The return value of dispatch() The dispatch() method provided by the store dispatches actions to the middleware chain and ultimately to the reducer - but its return value has interesting properties: by default, dispatch() returns the dispatched action however, if middleware is applied to the store, dispatch() returns the value returned by the middleware in particular, when using Redux Thunk combined with createAsyncThunk() , dispatch() returns the Promise returned by the thunk - consequently, one can use await on its return value, within an async function 70

Slide 71

Slide 71 text

Part 4 React Redux

Slide 72

Slide 72 text

One-way data flow One-way data flow is a paradigm adopted by both React and Redux: in Redux, via the immutability of the current state, plus the dispatch() mechanism in React, information flows via properties from a component to its sub-components React's waterfall approach actually works - but it has a major drawback: if a node holds data that are required in a remote descendant, such data must be passed to all the intermediary node as well. This is an inelegant but fairly infrequent scenario, although there may be more compelling reasons to adopt Redux, so: « Don't use Redux until you have problems with vanilla React » 72

Slide 73

Slide 73 text

Introducing React Redux « React Redux is the official Redux UI binding library for React » The very essence of React Redux is: subscribing to the store created by Redux updating the UI: only when needed - that is, if the state actually changes only where needed - to minimize DOM modifications 73

Slide 74

Slide 74 text

Where should the state reside? When Redux is added to a React app, there are at least 3 possible locations where each piece of the app state can reside: global state - provided by the Redux store: it can be modified only by dispatching actions to the store data can be extracted from it via selectors As the name implies, it is best suited for app-wide global information component state - living within the single React component and provided, for example, by the useState() hook. Typically recommended for form controls context state - an intermediate state container provided by React 74

Slide 75

Slide 75 text

Components: connected or presentational? « When a React component interacts with the Redux store, it becomes a connected component » Not all components need to be connected: on the contrary, most React components should be presentational - that is, relying just on their properties and maybe their internal state. Consequently, in React Redux it is common to see connected components that fetch data from the Redux store and propagate such data to their sub-tree of presentational components 75

Slide 76

Slide 76 text

Plugging the Redux store into React React Redux is available via NPM as react-redux . After the installation, it takes just a few steps: 1. Create the store, as you would in vanilla Redux - for example, you can export either the store instance or a store creator from a dedicated module 2. In JSX, plug the Provider tag - usually as the parent of the application's root component tag passed to React's render() function - assigning the store instance to its store property: 76

Slide 77

Slide 77 text

Connecting components to the store To create a connected component: if the component is implemented as a class - which is less and less frequent, the connect() function is required - as well as the related boilerplate code in modern, functional React components, everything can be achieved via hooks: useSelector(selector) : returns the value it calculates from the current state within the store useDispatch() returns a reference to the store's dispatch() method 77

Slide 78

Slide 78 text

Performance optimizations Calling useSelector() many times with different selectors should be preferred over calling useSelector() once with an aggregating selector real performance gains can be achieved via createSelector() - which is provided by reselect as well as Redux Toolkit General advice also apply - for example, in reducers it is more efficient to return the very same state instance rather than a deep copy 78

Slide 79

Slide 79 text

Part 5 Conclusion

Slide 80

Slide 80 text

Global takeaways Do you really need Redux? Just like any other software component, you should adopt it in scenarios where the benefits outweigh the drawbacks Use createSlice() to create the action creators and the reducers for each slice of the domain: this will make you especially rely on PayloadAction Call configureStore() to create the store with default middleware and DevTools Invoke the store methods - especially dispatch() and getState() Rely on createAsyncThunk() for asynchronous operations Call useSelector() and useDispatch() in React Redux for elegant code 80

Slide 81

Slide 81 text

Redux projects - directory structure Redux is not opinionated in terms of directory layout, thus different choices are viable: group by feature: the recommended style, based on Domain-Driven Design. It seems wise to have a bears.ts module containing everything about bears: action type constants and action interfaces action creators (exported) reducer (exported) group by construct: à la Ruby on Rails - with one folder per construct ( actions/ , reducers/ , ...), each containing a module per feature ( bears.ts , rangers.ts , ...) 81

Slide 82

Slide 82 text

Further references Redux - Official website Redux Toolkit Redux Thunk Redux Saga - a generator-based approach to async operations React Redux Redux - Style guide Middleware evolution - step-by-step architectural explanation React Redux sample app Elm - «A delightful language for reliable webapps» 82

Slide 83

Slide 83 text

Thank you! ^__^