From b27ed78be69d659acda940202af5fe76d4db7943 Mon Sep 17 00:00:00 2001 From: Corbin Crutchley Date: Tue, 23 Sep 2025 10:21:00 -0700 Subject: [PATCH] docs: add understanding sections --- docs/understanding/glossary.md | 139 +++++++ docs/understanding/middleware.md | 488 +++++++++++++++++++++++++ docs/understanding/motivation.md | 17 + docs/understanding/three-principles.md | 100 +++++ website/docusaurus.config.ts | 2 +- website/sidebars.ts | 13 +- 6 files changed, 757 insertions(+), 2 deletions(-) create mode 100644 docs/understanding/glossary.md create mode 100644 docs/understanding/middleware.md create mode 100644 docs/understanding/motivation.md create mode 100644 docs/understanding/three-principles.md diff --git a/docs/understanding/glossary.md b/docs/understanding/glossary.md new file mode 100644 index 00000000..72445e9c --- /dev/null +++ b/docs/understanding/glossary.md @@ -0,0 +1,139 @@ +--- +id: glossary +title: Glossary +--- + +# Glossary + +This is a glossary of the core terms in Redux, along with their type signatures. The types are documented using [Flow notation](https://flow.org/en/docs/types). + +## State + +```js +type State = any +``` + +_State_ (also called the _state tree_) is a broad term, but in the Redux API it usually refers to the single state value that is managed by the store and returned by [`getState()`](api/Store.md#getState). It represents the entire state of a Redux application, which is often a deeply nested object. + +By convention, the top-level state is an object or some other key-value collection like a Map, but technically it can be any type. Still, you should do your best to keep the state serializable. Don't put anything inside it that you can't easily turn into JSON. + +## Action + +```js +type Action = Object +``` + +An _action_ is a plain object that represents an intention to change the state. Actions are the only way to get data into the store. Any data, whether from UI events, network callbacks, or other sources such as WebSockets needs to eventually be dispatched as actions. + +Actions must have a `type` field that indicates the type of action being performed. Types can be defined as constants and imported from another module. It's better to use strings for `type` than [Symbols](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Symbol) because strings are serializable. + +Other than `type`, the structure of an action object is really up to you. If you're interested, check out [Flux Standard Action](https://github.com/acdlite/flux-standard-action) for recommendations on how actions should be constructed. + +See also [async action](#async-action) below. + +## Reducer + +```js +type Reducer = (state: S, action: A) => S +``` + +A _reducer_ is a function that accepts an accumulation and a value and returns a new accumulation. They are used to reduce a collection of values down to a single value. + +Reducers are not unique to Redux—they are a fundamental concept in functional programming. Even most non-functional languages, like JavaScript, have a built-in API for reducing. In JavaScript, it's [`Array.prototype.reduce()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/Reduce). + +In Redux, the accumulated value is the state object, and the values being accumulated are actions. Reducers calculate a new state given the previous state and an action. They must be _pure functions_—functions that return the exact same output for given inputs. They should also be free of side-effects. This is what enables exciting features like hot reloading and time travel. + +Reducers are the most important concept in Redux. + +_Do not put API calls into reducers._ + +## Dispatching Function + +```js +type BaseDispatch = (a: Action) => Action +type Dispatch = (a: Action | AsyncAction) => any +``` + +A _dispatching function_ (or simply _dispatch function_) is a function that accepts an action or an [async action](#async-action); it then may or may not dispatch one or more actions to the store. + +We must distinguish between dispatching functions in general and the base [`dispatch`](api/Store.md#dispatchaction) function provided by the store instance without any middleware. + +The base dispatch function _always_ synchronously sends an action to the store's reducer, along with the previous state returned by the store, to calculate a new state. It expects actions to be plain objects ready to be consumed by the reducer. + +[Middleware](#middleware) wraps the base dispatch function. It allows the dispatch function to handle [async actions](#async-action) in addition to actions. Middleware may transform, delay, ignore, or otherwise interpret actions or async actions before passing them to the next middleware. See below for more information. + +## Action Creator + +```js +type ActionCreator = (...args: P) => Action | AsyncAction +``` + +An _action creator_ is, quite simply, a function that creates an action. Do not confuse the two terms—again, an action is a payload of information, and an action creator is a factory that creates an action. + +Calling an action creator only produces an action, but does not dispatch it. You need to call the store's [`dispatch`](api/Store.md#dispatchaction) function to actually cause the mutation. Sometimes we say _bound action creators_ to mean functions that call an action creator and immediately dispatch its result to a specific store instance. + +If an action creator needs to read the current state, perform an API call, or cause a side effect, like a routing transition, it should return an [async action](#async-action) instead of an action. + +## Async Action + +```js +type AsyncAction = any +``` + +An _async action_ is a value that is sent to a dispatching function, but is not yet ready for consumption by the reducer. It will be transformed by [middleware](#middleware) into an action (or a series of actions) before being sent to the base [`dispatch()`](api/Store.md#dispatchaction) function. Async actions may have different types, depending on the middleware you use. They are often asynchronous primitives, like a Promise or a thunk, which are not passed to the reducer immediately, but trigger action dispatches once an operation has completed. + +## Middleware + +```js +type MiddlewareAPI = { dispatch: Dispatch, getState: () => State } +type Middleware = (api: MiddlewareAPI) => (next: Dispatch) => Dispatch +``` + +A middleware is a higher-order function that composes a [dispatch function](#dispatching-function) to return a new dispatch function. It often turns [async actions](#async-action) into actions. + +Middleware is composable using function composition. It is useful for logging actions, performing side effects like routing, or turning an asynchronous API call into a series of synchronous actions. + +See [`applyMiddleware(...middlewares)`](../../api/applyMiddleware.md) for a detailed look at middleware. + +## Store + +```js +type Store = { + dispatch: Dispatch + getState: () => State + subscribe: (listener: () => void) => () => void + replaceReducer: (reducer: Reducer) => void +} +``` + +A store is an object that holds the application's state tree. +There should only be a single store in a Redux app, as the composition happens on the reducer level. + +- [`dispatch(action)`](api/Store.md#dispatchaction) is the base dispatch function described above. +- [`getState()`](api/Store.md#getState) returns the current state of the store. +- [`subscribe(listener)`](api/Store.md#subscribelistener) registers a function to be called on state changes. +- [`replaceReducer(nextReducer)`](api/Store.md#replacereducernextreducer) can be used to implement hot reloading and code splitting. Most likely you won't use it. + +See the complete [store API reference](api/Store.md#dispatchaction) for more details. + +## Store creator + +```js +type StoreCreator = (reducer: Reducer, preloadedState: ?State) => Store +``` + +A store creator is a function that creates a Redux store. Like with dispatching function, we must distinguish the base store creator, [`createStore(reducer, preloadedState)`](api/createStore.md) exported from the Redux package, from store creators that are returned from the store enhancers. + +## Store enhancer + +```js +type StoreEnhancer = (next: StoreCreator) => StoreCreator +``` + +A store enhancer is a higher-order function that composes a store creator to return a new enhanced store creator. This is similar to middleware in that it allows you to alter the store interface in a composable way. + +Store enhancers are much the same concept as higher-order components in React, which are also occasionally called “component enhancers”. + +Because a store is not an instance, but rather a plain-object collection of functions, copies can be easily created and modified without mutating the original store. There is an example in [`compose`](api/compose.md) documentation demonstrating that. + +Most likely you'll never write a store enhancer, but you may use the one provided by the [developer tools](https://github.com/reduxjs/redux-devtools). It is what makes time travel possible without the app being aware it is happening. Amusingly, the [Redux middleware implementation](api/applyMiddleware.md) is itself a store enhancer. diff --git a/docs/understanding/middleware.md b/docs/understanding/middleware.md new file mode 100644 index 00000000..853f574a --- /dev/null +++ b/docs/understanding/middleware.md @@ -0,0 +1,488 @@ +--- +id: middleware +title: Middleware +description: 'History and Design > Middleware: How middleware enable adding additional capabilities to the Redux store' +--- + +# Middleware + +You've seen middleware in action in the ["Redux Fundamentals" tutorial](../../tutorials/fundamentals/part-4-store.md#middleware). If you've used server-side libraries like [Express](https://expressjs.com/) and [Koa](https://koajs.com/), you were also probably already familiar with the concept of _middleware_. In these frameworks, middleware is some code you can put between the framework receiving a request, and the framework generating a response. For example, Express or Koa middleware may add CORS headers, logging, compression, and more. The best feature of middleware is that it's composable in a chain. You can use multiple independent third-party middleware in a single project. + +Redux middleware solves different problems than Express or Koa middleware, but in a conceptually similar way. **It provides a third-party extension point between dispatching an action, and the moment it reaches the reducer.** People use Redux middleware for logging, crash reporting, talking to an asynchronous API, routing, and more. + +This article is divided into an in-depth intro to help you grok the concept, and [a few practical examples](#seven-examples) to show the power of middleware at the very end. You may find it helpful to switch back and forth between them, as you flip between feeling bored and inspired. + +## Understanding Middleware + +While middleware can be used for a variety of things, including asynchronous API calls, it's really important that you understand where it comes from. We'll guide you through the thought process leading to middleware, by using logging and crash reporting as examples. + +### Problem: Logging + +One of the benefits of Redux is that it makes state changes predictable and transparent. Every time an action is dispatched, the new state is computed and saved. The state cannot change by itself, it can only change as a consequence of a specific action. + +Wouldn't it be nice if we logged every action that happens in the app, together with the state computed after it? When something goes wrong, we can look back at our log, and figure out which action corrupted the state. + + + +How do we approach this with Redux? + +### Attempt #1: Logging Manually + +The most naïve solution is just to log the action and the next state yourself every time you call [`store.dispatch(action)`](../../api/Store.md#dispatchaction). It's not really a solution, but just a first step towards understanding the problem. + +> ##### Note +> +> If you're using [react-redux](https://github.com/reduxjs/react-redux) or similar bindings, you likely won't have direct access to the store instance in your components. For the next few paragraphs, just assume you pass the store down explicitly. + +Say, you call this when creating a todo: + +```js +store.dispatch(addTodo('Use Redux')) +``` + +To log the action and state, you can change it to something like this: + +```js +const action = addTodo('Use Redux') + +console.log('dispatching', action) +store.dispatch(action) +console.log('next state', store.getState()) +``` + +This produces the desired effect, but you wouldn't want to do it every time. + +### Attempt #2: Wrapping Dispatch + +You can extract logging into a function: + +```js +function dispatchAndLog(store, action) { + console.log('dispatching', action) + store.dispatch(action) + console.log('next state', store.getState()) +} +``` + +You can then use it everywhere instead of `store.dispatch()`: + +```js +dispatchAndLog(store, addTodo('Use Redux')) +``` + +We could end this here, but it's not very convenient to import a special function every time. + +### Attempt #3: Monkeypatching Dispatch + +What if we just replace the `dispatch` function on the store instance? The Redux store is a plain object with [a few methods](../../api/Store.md), and we're writing JavaScript, so we can just monkeypatch the `dispatch` implementation: + +```js +const next = store.dispatch +store.dispatch = function dispatchAndLog(action) { + console.log('dispatching', action) + let result = next(action) + console.log('next state', store.getState()) + return result +} +``` + +This is already closer to what we want! No matter where we dispatch an action, it is guaranteed to be logged. Monkeypatching never feels right, but we can live with this for now. + +### Problem: Crash Reporting + +What if we want to apply **more than one** such transformation to `dispatch`? + +A different useful transformation that comes to my mind is reporting JavaScript errors in production. The global `window.onerror` event is not reliable because it doesn't provide stack information in some older browsers, which is crucial to understand why an error is happening. + +Wouldn't it be useful if, any time an error is thrown as a result of dispatching an action, we would send it to a crash reporting service like [Sentry](https://getsentry.com/welcome/) with the stack trace, the action that caused the error, and the current state? This way it's much easier to reproduce the error in development. + +However, it is important that we keep logging and crash reporting separate. Ideally we want them to be different modules, potentially in different packages. Otherwise we can't have an ecosystem of such utilities. (Hint: we're slowly getting to what middleware is!) + +If logging and crash reporting are separate utilities, they might look like this: + +```js +function patchStoreToAddLogging(store) { + const next = store.dispatch + store.dispatch = function dispatchAndLog(action) { + console.log('dispatching', action) + let result = next(action) + console.log('next state', store.getState()) + return result + } +} + +function patchStoreToAddCrashReporting(store) { + const next = store.dispatch + store.dispatch = function dispatchAndReportErrors(action) { + try { + return next(action) + } catch (err) { + console.error('Caught an exception!', err) + Raven.captureException(err, { + extra: { + action, + state: store.getState() + } + }) + throw err + } + } +} +``` + +If these functions are published as separate modules, we can later use them to patch our store: + +```js +patchStoreToAddLogging(store) +patchStoreToAddCrashReporting(store) +``` + +Still, this isn't nice. + +### Attempt #4: Hiding Monkeypatching + +Monkeypatching is a hack. “Replace any method you like”, what kind of API is that? Let's figure out the essence of it instead. Previously, our functions replaced `store.dispatch`. What if they _returned_ the new `dispatch` function instead? + +```js +function logger(store) { + const next = store.dispatch + + // Previously: + // store.dispatch = function dispatchAndLog(action) { + + return function dispatchAndLog(action) { + console.log('dispatching', action) + let result = next(action) + console.log('next state', store.getState()) + return result + } +} +``` + +We could provide a helper inside Redux that would apply the actual monkeypatching as an implementation detail: + +```js +function applyMiddlewareByMonkeypatching(store, middlewares) { + middlewares = middlewares.slice() + middlewares.reverse() + + // Transform dispatch function with each middleware. + middlewares.forEach(middleware => (store.dispatch = middleware(store))) +} +``` + +We could use it to apply multiple middleware like this: + +```js +applyMiddlewareByMonkeypatching(store, [logger, crashReporter]) +``` + +However, it is still monkeypatching. +The fact that we hide it inside the library doesn't alter this fact. + +### Attempt #5: Removing Monkeypatching + +Why do we even overwrite `dispatch`? Of course, to be able to call it later, but there's also another reason: so that every middleware can access (and call) the previously wrapped `store.dispatch`: + +```js +function logger(store) { + // Must point to the function returned by the previous middleware: + const next = store.dispatch + + return function dispatchAndLog(action) { + console.log('dispatching', action) + let result = next(action) + console.log('next state', store.getState()) + return result + } +} +``` + +It is essential to chaining middleware! + +If `applyMiddlewareByMonkeypatching` doesn't assign `store.dispatch` immediately after processing the first middleware, `store.dispatch` will keep pointing to the original `dispatch` function. Then the second middleware will also be bound to the original `dispatch` function. + +But there's also a different way to enable chaining. The middleware could accept the `next()` dispatch function as a parameter instead of reading it from the `store` instance. + +```js +function logger(store) { + return function wrapDispatchToAddLogging(next) { + return function dispatchAndLog(action) { + console.log('dispatching', action) + let result = next(action) + console.log('next state', store.getState()) + return result + } + } +} +``` + +It's a [“we need to go deeper”](https://knowyourmeme.com/memes/we-need-to-go-deeper) kind of moment, so it might take a while for this to make sense. The function cascade feels intimidating. Arrow functions make this [currying](https://en.wikipedia.org/wiki/Currying) easier on eyes: + +```js +const logger = store => next => action => { + console.log('dispatching', action) + let result = next(action) + console.log('next state', store.getState()) + return result +} + +const crashReporter = store => next => action => { + try { + return next(action) + } catch (err) { + console.error('Caught an exception!', err) + Raven.captureException(err, { + extra: { + action, + state: store.getState() + } + }) + throw err + } +} +``` + +**This is exactly what Redux middleware looks like.** + +Now middleware takes the `next()` dispatch function, and returns a dispatch function, which in turn serves as `next()` to the middleware to the left, and so on. It's still useful to have access to some store methods like `getState()`, so `store` stays available as the top-level argument. + +### Attempt #6: Naïvely Applying the Middleware + +Instead of `applyMiddlewareByMonkeypatching()`, we could write `applyMiddleware()` that first obtains the final, fully wrapped `dispatch()` function, and returns a copy of the store using it: + +```js +// Warning: Naïve implementation! +// That's *not* Redux API. +function applyMiddleware(store, middlewares) { + middlewares = middlewares.slice() + middlewares.reverse() + let dispatch = store.dispatch + middlewares.forEach(middleware => (dispatch = middleware(store)(dispatch))) + return Object.assign({}, store, { dispatch }) +} +``` + +The implementation of [`applyMiddleware()`](../../api/applyMiddleware.md) that ships with Redux is similar, but **different in three important aspects**: + +- It only exposes a subset of the [store API](../../api/Store.md) to the middleware: [`dispatch(action)`](../../api/Store.md#dispatchaction) and [`getState()`](../../api/Store.md#getState). + +- It does a bit of trickery to make sure that if you call `store.dispatch(action)` from your middleware instead of `next(action)`, the action will actually travel the whole middleware chain again, including the current middleware. [This is useful for asynchronous middleware](../../tutorials/fundamentals/part-6-async-logic.md). There is one caveat when calling `dispatch` during setup, described below. + +- To ensure that you may only apply middleware once, it operates on `createStore()` rather than on `store` itself. Instead of `(store, middlewares) => store`, its signature is `(...middlewares) => (createStore) => createStore`. + +Because it is cumbersome to apply functions to `createStore()` before using it, `createStore()` accepts an optional last argument to specify such functions. + +#### Caveat: Dispatching During Setup + +While `applyMiddleware` executes and sets up your middleware, the `store.dispatch` function will point to the vanilla version provided by `createStore`. Dispatching would result in no other middleware being applied. If you are expecting an interaction with another middleware during setup, you will probably be disappointed. Because of this unexpected behavior, `applyMiddleware` will throw an error if you try to dispatch an action before the set up completes. Instead, you should either communicate directly with that other middleware via a common object (for an API-calling middleware, this may be your API client object) or waiting until after the middleware is constructed with a callback. + +### The Final Approach + +Given this middleware we just wrote: + +```js +const logger = store => next => action => { + console.log('dispatching', action) + let result = next(action) + console.log('next state', store.getState()) + return result +} + +const crashReporter = store => next => action => { + try { + return next(action) + } catch (err) { + console.error('Caught an exception!', err) + Raven.captureException(err, { + extra: { + action, + state: store.getState() + } + }) + throw err + } +} +``` + +Here's how to apply it to a Redux store: + +```js +import { createStore, combineReducers, applyMiddleware } from 'redux' + +const todoApp = combineReducers(reducers) +const store = createStore( + todoApp, + // applyMiddleware() tells createStore() how to handle middleware + applyMiddleware(logger, crashReporter) +) +``` + +That's it! Now any actions dispatched to the store instance will flow through `logger` and `crashReporter`: + +```js +// Will flow through both logger and crashReporter middleware! +store.dispatch(addTodo('Use Redux')) +``` + +## Seven Examples + +If your head boiled from reading the above section, imagine what it was like to write it. This section is meant to be a relaxation for you and me, and will help get your gears turning. + +Each function below is a valid Redux middleware. They are not equally useful, but at least they are equally fun. + +```js +/** + * Logs all actions and states after they are dispatched. + */ +const logger = store => next => action => { + console.group(action.type) + console.info('dispatching', action) + let result = next(action) + console.log('next state', store.getState()) + console.groupEnd() + return result +} + +/** + * Sends crash reports as state is updated and listeners are notified. + */ +const crashReporter = store => next => action => { + try { + return next(action) + } catch (err) { + console.error('Caught an exception!', err) + Raven.captureException(err, { + extra: { + action, + state: store.getState() + } + }) + throw err + } +} + +/** + * Schedules actions with { meta: { delay: N } } to be delayed by N milliseconds. + * Makes `dispatch` return a function to cancel the timeout in this case. + */ +const timeoutScheduler = store => next => action => { + if (!action.meta || !action.meta.delay) { + return next(action) + } + + const timeoutId = setTimeout(() => next(action), action.meta.delay) + + return function cancel() { + clearTimeout(timeoutId) + } +} + +/** + * Schedules actions with { meta: { raf: true } } to be dispatched inside a rAF loop + * frame. Makes `dispatch` return a function to remove the action from the queue in + * this case. + */ +const rafScheduler = store => next => { + const queuedActions = [] + let frame = null + + function loop() { + frame = null + try { + if (queuedActions.length) { + next(queuedActions.shift()) + } + } finally { + maybeRaf() + } + } + + function maybeRaf() { + if (queuedActions.length && !frame) { + frame = requestAnimationFrame(loop) + } + } + + return action => { + if (!action.meta || !action.meta.raf) { + return next(action) + } + + queuedActions.push(action) + maybeRaf() + + return function cancel() { + queuedActions = queuedActions.filter(a => a !== action) + } + } +} + +/** + * Lets you dispatch promises in addition to actions. + * If the promise is resolved, its result will be dispatched as an action. + * The promise is returned from `dispatch` so the caller may handle rejection. + */ +const vanillaPromise = store => next => action => { + if (typeof action.then !== 'function') { + return next(action) + } + + return Promise.resolve(action).then(store.dispatch) +} + +/** + * Lets you dispatch special actions with a { promise } field. + * + * This middleware will turn them into a single action at the beginning, + * and a single success (or failure) action when the `promise` resolves. + * + * For convenience, `dispatch` will return the promise so the caller can wait. + */ +const readyStatePromise = store => next => action => { + if (!action.promise) { + return next(action) + } + + function makeAction(ready, data) { + const newAction = Object.assign({}, action, { ready }, data) + delete newAction.promise + return newAction + } + + next(makeAction(false)) + return action.promise.then( + result => next(makeAction(true, { result })), + error => next(makeAction(true, { error })) + ) +} + +/** + * Lets you dispatch a function instead of an action. + * This function will receive `dispatch` and `getState` as arguments. + * + * Useful for early exits (conditions over `getState()`), as well + * as for async control flow (it can `dispatch()` something else). + * + * `dispatch` will return the return value of the dispatched function. + */ +const thunk = store => next => action => + typeof action === 'function' + ? action(store.dispatch, store.getState) + : next(action) + +// You can use all of them! (It doesn't mean you should.) +const todoApp = combineReducers(reducers) +const store = createStore( + todoApp, + applyMiddleware( + rafScheduler, + timeoutScheduler, + thunk, + vanillaPromise, + readyStatePromise, + logger, + crashReporter + ) +) +``` diff --git a/docs/understanding/motivation.md b/docs/understanding/motivation.md new file mode 100644 index 00000000..345c28e2 --- /dev/null +++ b/docs/understanding/motivation.md @@ -0,0 +1,17 @@ +--- +id: motivation +title: Motivation +description: 'Introduction > Motivation: What problems does Redux try to solve?' +--- + +# Motivation + +As the requirements for JavaScript single-page applications have become increasingly complicated, **our code must manage more state than ever before**. This state can include server responses and cached data, as well as locally created data that has not yet been persisted to the server. UI state is also increasing in complexity, as we need to manage active routes, selected tabs, spinners, pagination controls, and so on. + +Managing this ever-changing state is hard. If a model can update another model, then a view can update a model, which updates another model, and this, in turn, might cause another view to update. At some point, you no longer understand what happens in your app as you have **lost control over the when, why, and how of its state.** When a system is opaque and non-deterministic, it's hard to reproduce bugs or add new features. + +As if this weren't bad enough, consider the **new requirements becoming common in front-end product development**. As developers, we are expected to handle optimistic updates, server-side rendering, fetching data before performing route transitions, and so on. We find ourselves trying to manage a complexity that we have never had to deal with before, and we inevitably ask the question: [is it time to give up?](https://www.quirksmode.org/blog/archives/2015/07/stop_pushing_th.html) The answer is _no_. + +This complexity is difficult to handle as **we're mixing two concepts** that are very hard for the human mind to reason about: **mutation and asynchronicity.** I call them [Mentos and Coke](https://en.wikipedia.org/wiki/Diet_Coke_and_Mentos_eruption). Both can be great in separation, but together they create a mess. Libraries like [React](https://facebook.github.io/react) attempt to solve this problem in the view layer by removing both asynchrony and direct DOM manipulation. However, managing the state of your data is left up to you. This is where Redux enters. + +Following in the steps of [Flux](https://facebookarchive.github.io/flux), [CQRS](https://martinfowler.com/bliki/CQRS.html), and [Event Sourcing](https://martinfowler.com/eaaDev/EventSourcing.html), **Redux attempts to make state mutations predictable** by imposing certain restrictions on how and when updates can happen. These restrictions are reflected in the [three principles](ThreePrinciples.md) of Redux. diff --git a/docs/understanding/three-principles.md b/docs/understanding/three-principles.md new file mode 100644 index 00000000..f582fb7b --- /dev/null +++ b/docs/understanding/three-principles.md @@ -0,0 +1,100 @@ +--- +id: three-principles +title: Three Principles +description: 'Introduction > Three Principles: Three key principles for using Redux' +--- + +# Three Principles + +Redux can be described in three fundamental principles: + +### Single source of truth + +**The [global state](./Glossary.md#state) of your application is stored in an object tree within a single [store](./Glossary.md#store).** + +This makes it easy to create universal apps, as the state from your server can be serialized and hydrated into the client with no extra coding effort. A single state tree also makes it easier to debug or inspect an application; it also enables you to persist your app's state in development, for a faster development cycle. Some functionality which has been traditionally difficult to implement - Undo/Redo, for example - can suddenly become trivial to implement, if all of your state is stored in a single tree. + +```js +console.log(store.getState()) + +/* Prints +{ + visibilityFilter: 'SHOW_ALL', + todos: [ + { + text: 'Consider using Redux', + completed: true, + }, + { + text: 'Keep all state in a single tree', + completed: false + } + ] +} +*/ +``` + +### State is read-only + +**The only way to change the state is to emit an [action](./Glossary.md), an object describing what happened.** + +This ensures that neither the views nor the network callbacks will ever write directly to the state. Instead, they express an intent to transform the state. Because all changes are centralized and happen one by one in a strict order, there are no subtle race conditions to watch out for. As actions are just plain objects, they can be logged, serialized, stored, and later replayed for debugging or testing purposes. + +```js +store.dispatch({ + type: 'COMPLETE_TODO', + index: 1 +}) + +store.dispatch({ + type: 'SET_VISIBILITY_FILTER', + filter: 'SHOW_COMPLETED' +}) +``` + +### Changes are made with pure functions + +**To specify how the state tree is transformed by actions, you write pure [reducers](./Glossary.md#reducer).** + +Reducers are just pure functions that take the previous state and an action, and return the next state. Remember to return new state objects, instead of mutating the previous state. You can start with a single reducer, and as your app grows, split it off into smaller reducers that manage specific parts of the state tree. Because reducers are just functions, you can control the order in which they are called, pass additional data, or even make reusable reducers for common tasks such as pagination. + +```js +function visibilityFilter(state = 'SHOW_ALL', action) { + switch (action.type) { + case 'SET_VISIBILITY_FILTER': + return action.filter + default: + return state + } +} + +function todos(state = [], action) { + switch (action.type) { + case 'ADD_TODO': + return [ + ...state, + { + text: action.text, + completed: false + } + ] + case 'COMPLETE_TODO': + return state.map((todo, index) => { + if (index === action.index) { + return Object.assign({}, todo, { + completed: true + }) + } + return todo + }) + default: + return state + } +} + +import { combineReducers, createStore } from 'redux' +const reducer = combineReducers({ visibilityFilter, todos }) +const store = createStore(reducer) +``` + +That's it! Now you know what Redux is all about. diff --git a/website/docusaurus.config.ts b/website/docusaurus.config.ts index dbce6989..acf5e7ff 100644 --- a/website/docusaurus.config.ts +++ b/website/docusaurus.config.ts @@ -21,7 +21,7 @@ const config: Config = { routeBasePath: '/', editUrl: 'https://github.com/reduxjs/redux-toolkit/blob/master/docs/', include: [ - '{introduction,reference,guides,faq,ecosystem,migrations}/**/*.{md,mdx}', + '{introduction,reference,guides,faq,ecosystem,migrations,understanding}/**/*.{md,mdx}', ], // no other way to exclude node_modules remarkPlugins: [ [ diff --git a/website/sidebars.ts b/website/sidebars.ts index 8450c70f..51e94bfc 100644 --- a/website/sidebars.ts +++ b/website/sidebars.ts @@ -16,7 +16,7 @@ const sidebars: SidebarsConfig = { { type: 'category', label: 'Guides', - collapsed: false, + collapsed: true, items: [ 'guides/deriving-data-selectors', 'guides/typescript', @@ -25,6 +25,17 @@ const sidebars: SidebarsConfig = { 'guides/troubleshooting', ], }, + { + type: 'category', + label: 'Understanding Redux', + collapsed: true, + items: [ + 'understanding/motivation', + 'understanding/three-principles', + 'understanding/glossary', + 'understanding/middleware', + ], + }, { type: 'category', label: 'Migrations', -- 2.51.2