overhauling steps 4-6, part 1

This commit is contained in:
Ken
2019-03-02 20:57:25 -08:00
parent 8fc928ea8d
commit b91914e1d8
21 changed files with 296 additions and 221 deletions

View File

@@ -1,101 +1,112 @@
# Step 2.5 - Redux: Reducers (Demo)
# Step 2.5 - Redux: The Store (Demo)
[Lessons](../) | [Exercise](./exercise/) | [Demo](./demo/)
## Flux
In this step, we will look at solving the problems of complex application (as mentioned in Step 4) with a library called Redux.
Ideally React gives us a mental model of:
1. Introduction to Redux
2. Creating the Redux store
3. Writing reducers
4. Dispatching actions
```
f(data) => view
```
---
We can pass shared state data down to components using props and update it using callbacks, but as an application grows larger, passing around callbacks becomes unwieldy and the state data flow becomes hard to understand. To keep the application maintainable, we need a well-defined pattern for sharing and updating application state.
As a reminder, the problem that we want to address are:
Facebook invented the [Flux](https://facebook.github.io/flux/) architectural pattern to solve this shared state issue. [Redux](https://redux.js.org/) is an implementation of Flux.
1. Data needs to be passed down from component to component via props. Even when some components do not need to know about some data.
2. There is a lack of coordination of changes that can happen to the data
Redux is used inside many large, complex applications because of its clarity and predictability. It is easy to debug and easily extensible via its middleware architecture. In this lesson, we'll explore the heart of how Redux manages state.
Redux expects the data (store) to be a singleton state tree. It listens for messages to manipulate the state and passes updates down to views.
Redux is an implementation of the Flux architectural pattern:
![Flux Diagram](../assets/flux.png)
### View
A view is a React component that consumes the store as its data. There is a special way Redux maps data from the state tree into the different React components. The components will know to re-render when these bits of state are changed.
A view is a React component that consumes the store as its data.
### Action
[Actions](https://redux.js.org/basics/actions) are messages that represent some event, such as a user's action or a network request. With the aid of *reducers*, they affect the overall state.
[Actions](https://redux.js.org/basics/actions) are messages that represent some event, such as a user's action or a network request. With the aid of _reducers_, they affect the overall state.
### Store
The [store](https://redux.js.org/basics/store) contains a singleton state tree. The state tree is immutable and needs to be re-created at every action. This helps connected views easily to know when to update by allowing them to do a simple reference comparison rather than a deep comparison. You can think of each state as a snapshot of the app at that point in time.
The [store](https://redux.js.org/basics/store) consists of a **state tree**, a **dispatcher**, and **reducers**.
### Dispatcher
1. A state tree is a **singleton**, **serializable**, **immutable** json data. It is updated from one snapshot to another through `reducers`.
There is a single [dispatcher](https://redux.js.org/basics/data-flow), provided by the store. It simply informs the store of all the actions that need to be performed. Additional middleware (explained later) can be applied to the store, and the dispatcher's job is to dispatch the message through all the middleware layers.
2. A dispatcher accepts actions passing them to the reducers.
### Reducers
3. Reducers are functions that take in the current state tree and an action, producing the next snapshot of the state tree.
Redux uses [reducers](https://redux.js.org/basics/reducers) to manage state changes. This name comes from the "reducer" function passed to `Array.reduce()`.
A Redux reducer is a simple **pure function** (no side effects). Its only job is to transform the state from one immutable snapshot to another. It takes state + an action message as input, makes a modified copy of the state based on the action message type and payload, and returns the new state. (Reducers [should not modify](https://redux.js.org/introduction/three-principles#state-is-read-only) the previous state.)
**Mental Model**: Think of a reducer as part of the store. It should have no side effects and only define how data changes from one state to the next given action messages.
### Advanced: Middleware
From the [documentation site](https://redux.js.org/advanced/middleware):
> Redux middleware provides a third-party extension point between dispatching an action, and the moment it reaches the reducer.
We won't be covering middleware much in these lessons since it's a more advanced topic.
## Getting started with Redux
We begin the journey into Redux by looking at the store. The store consists of several parts:
1. **State/data** - We represent this both with an initial state and with a TypeScript interface.
2. **Reducers** - Responsible for reacting to action messages to change from one state to the next.
3. **Dispatcher** - There should be only one dispatcher, which is exported by the store. We'll look at this in Step 6!
### Create store
# Creating the Redux store
The [`createStore()`](https://redux.js.org/api/createstore) function is provided by Redux and can take in several arguments. The simplest form just takes in reducers.
```ts
const store = createStore(reducer);
const store = createStore(reducer, initialState);
```
`createStore()` can also take in an initial state. There are two common sources for the initial state:
`createStore()` creates a store with a reducer, and some initial state.
1. Load initial data from a server
2. Load data that is generated by a server-side rendering environment
# Writing Reducers
We will write our reducers with the help of some utilities from `redux-starter-kit`. Here is how we will write our reducers:
## 1. Organize reducers according to the keys of the state tree object:
```ts
const store = createStore(reducer, {
/* the initial state */
import { createReducer } from 'redux-starter-kit';
const todosReducer = createReducer({}, {
// first argument is the initial state
// second argument is an object where the keys corresponds to the "action.type"
addTodo: (state, action) => ...
});
const filterReducer = createReducer('all', {
setFilter: (state, action) => ...
});
const reducer = combineReducer({
todos: todosReducer,
filter: filterReducer
})
```
`createStore()` can take a third argument that injects middleware, but we won't use this until later.
## 2. Write the reducers with mutables.
### Reducers
Remember that the [reducers are **pure**](https://redux.js.org/introduction/three-principles#changes-are-made-with-pure-functions). Pure functions have no side effects. They always return the same output given the same input (idempotent). They are easily testable.
Reducers look at the action's message to decide what to do to the state. A convention established in the Flux community is that the action message (payload) should include a `type` key. Another convention is using switch statements against the `type` key to trigger further reducer functions.
`createReducer()` will automatically translate all the mutations to the state into immutable snapshots (!!!!!):
```ts
function reducer(state: Store['todos'], payload: any): Store['todos'] {
switch (payload.type) {
case 'addTodo':
return addTodo(state, payload.id, payload.label);
const todosReducer = createReducer(
{},
{
// first argument is the initial state
// second argument is an object where the keys corresponds to the "action.type"
addTodo: (state, action) => {
state[action.id] = { label: action.label, completed: false };
}
}
return state;
}
);
```
In the demo and exercises for this step, I separated the pure and reducer functions into different files to make it cleaner. The tests inside `pureFunctions.spec.ts` should describe the behavior of the individual functions. They are easy to follow and easy to write.
# Dispatching Actions
Dispatching action will pass the action and the current state to the _reducers_. The root _reducer_ will produce a new snapshot for the entire state tree. We can inspect the affected snapshot with the help of `getState()`.
```ts
const store = createStore(reducer, initialState);
store.dispatch({ type: 'addTodo', label: 'hello' });
store.dispatch({ type: 'addTodo', label: 'world' });
console.log(store.getState());
```
Creating these action messages by hand is tedious, so we use action creators to do that:
```ts
const actions = {
addTodo = (label: string) => ({ label, id: nextId(), completed: false })
};
store.dispatch(actions.addTodo('hello'));
```