|
1 |
| -### Actions |
| 1 | +## Getting Started |
| 2 | + |
| 3 | +Don't be fooled by all the fancy talk about reducers, middleware, higher-order stores — Redux is incredibly simple. If you've ever built a Flux application, you will feel right at home. (If you're new to Flux, it's easy, too!) |
| 4 | + |
| 5 | +In this guide, we'll walk through the process of creating a simple Todo app. |
| 6 | + |
| 7 | +## Actions |
| 8 | + |
| 9 | +First, let's define some actions. |
| 10 | + |
| 11 | +**Actions** are payloads of information that send data from your application to your store. They are the *only* source of information for a store. You send them to the store using `store.dispatch()`. |
| 12 | + |
| 13 | +Here's an example action which represents adding a new todo item: |
2 | 14 |
|
3 | 15 | ```js
|
4 |
| -// Still using constants... |
5 |
| -import { INCREMENT_COUNTER, DECREMENT_COUNTER } from '../constants/ActionTypes'; |
| 16 | +{ |
| 17 | + type: ADD_TODO, |
| 18 | + payload: { |
| 19 | + text: 'Build my first Redux app' |
| 20 | + } |
| 21 | +} |
| 22 | +``` |
6 | 23 |
|
7 |
| -// But action creators are pure functions returning actions |
8 |
| -export function increment() { |
9 |
| - return { |
10 |
| - type: INCREMENT_COUNTER |
11 |
| - }; |
| 24 | +Actions are plain JavaScript objects. By convention, actions should a have a `type` field that indicates the type of action being performed. Types should typically be defined as constants and imported from another module. |
| 25 | + |
| 26 | +```js |
| 27 | +import { ADD_TODO, REMOVE_TODO } from '../actionTypes'; |
| 28 | +``` |
| 29 | + |
| 30 | +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. |
| 31 | + |
| 32 | +We need one more action type, for removing todos: |
| 33 | + |
| 34 | +```js |
| 35 | +{ |
| 36 | + type: REMOVE_TODO, |
| 37 | + payload: 123 |
12 | 38 | }
|
| 39 | +``` |
13 | 40 |
|
14 |
| -export function decrement() { |
15 |
| - return { |
16 |
| - type: DECREMENT_COUNTER |
| 41 | +`payload` in this case indicates the id of the todo we want to remove. |
| 42 | + |
| 43 | +## Action creators |
| 44 | + |
| 45 | +**Action creators** exactly that — functions that create actions. It's easy to conflate the terms "action" and "action creator," so do your best to use the proper term. |
| 46 | + |
| 47 | +In *other* Flux implementations, action creators often trigger a dispatch when invoked, like so: |
| 48 | + |
| 49 | +```js |
| 50 | +function addTodoWithDispatch(text) { |
| 51 | + const action = { |
| 52 | + type: ADD_TODO, |
| 53 | + payload: { |
| 54 | + text |
| 55 | + } |
17 | 56 | };
|
| 57 | + dispatch(action); |
18 | 58 | }
|
| 59 | +``` |
| 60 | + |
| 61 | +By contrast, in Redux action creators are **pure functions** with zero side-effects. They simply return an action: |
19 | 62 |
|
20 |
| -// Can also be async if you return a function |
21 |
| -export function incrementAsync() { |
22 |
| - return dispatch => { |
23 |
| - setTimeout(() => { |
24 |
| - // Yay! Can invoke sync or async actions with `dispatch` |
25 |
| - dispatch(increment()); |
26 |
| - }, 1000); |
| 63 | +```js |
| 64 | +function addTodo(text) { |
| 65 | + return { |
| 66 | + type: ADD_TODO, |
| 67 | + payload: { |
| 68 | + text |
| 69 | + } |
27 | 70 | };
|
28 | 71 | }
|
| 72 | +``` |
29 | 73 |
|
| 74 | +This makes them more portable and easier to test. To actually initiate a dispatch, pass the result to the `dispatch()` function: |
30 | 75 |
|
31 |
| -// Could also read state of a store in the callback form |
32 |
| -export function incrementIfOdd() { |
33 |
| - return (dispatch, getState) => { |
34 |
| - const { counter } = getState(); |
| 76 | +```js |
| 77 | +dispatch(addTodo(text)); |
| 78 | +dispatch(removeTodo(id)); |
| 79 | +``` |
35 | 80 |
|
36 |
| - if (counter % 2 === 0) { |
37 |
| - return; |
38 |
| - } |
| 81 | +Or create **bound action creator** that automatically dispatches: |
39 | 82 |
|
40 |
| - dispatch(increment()); |
41 |
| - }; |
42 |
| -} |
| 83 | +```js |
| 84 | +const boundAddTodo = text => dispatch(addTodo(text)); |
| 85 | +const boundRemoveTodo = id => dispatch(addTodo(id)); |
43 | 86 | ```
|
44 | 87 |
|
45 |
| -### Stores |
| 88 | +This is an example of a **higher-order function**. You'll notice this pattern a lot when working with Redux. Don't be scared, though — a higher-order function is just a function that returns another function. As you can see, ES6 arrow functions make working with higher-order functions a breeze. |
| 89 | + |
| 90 | +The `dispatch()` function can be accessed directly from the store as `store.dispatch()`, but more likely you'll access it using a helper like react-redux's `connect()`. |
| 91 | + |
| 92 | +## Reducers |
| 93 | + |
| 94 | +Now let's set up our store to respond to the action we defined above. |
| 95 | + |
| 96 | +Unlike other Flux implementations, Redux only has a single store, rather than a different store for each domain of data in your application. Instead of creating multiple stores that manually manage their own internal state, we create **reducers** that specify how to calculate state in response to new actions. |
| 97 | + |
| 98 | +A reducer looks like this: |
| 99 | + |
46 | 100 | ```js
|
47 |
| -// ... too, use constants |
48 |
| -import { INCREMENT_COUNTER, DECREMENT_COUNTER } from '../constants/ActionTypes'; |
| 101 | +(previousState, action) => newState |
| 102 | +``` |
| 103 | + |
| 104 | +It's the type of function you would pass to `Array.prototype.reduce(reducer, ?initialValue)`. |
49 | 105 |
|
50 |
| -// what's important is that Store is a pure function, |
51 |
| -// and you can write it anyhow you like. |
| 106 | +This may seem radical, but it turns out that this function signature is sufficient to express the entirety of the store model from traditional Flux — in a purely functional way. **This is a essence of Redux**. It's what enables all the cool features like hot reloading, time travel, and universal rendering. Aside from all that, though, it's simply a better model for expressing state changes. |
52 | 107 |
|
53 |
| -// the Store signature is (state, action) => state, |
54 |
| -// and the state shape is up to you: you can use primitives, |
55 |
| -// objects, arrays, or even ImmutableJS objects. |
| 108 | +[**See Dan's talk at React Europe for more on this topic**](https://www.youtube.com/watch?v=xsSnOQynTHs). |
| 109 | + |
| 110 | +Let's make a reducer for our Todo app: |
| 111 | + |
| 112 | +```js |
| 113 | +const initialState = { todos: [], idCounter: 0 }; |
56 | 114 |
|
57 |
| -export default function counter(state = 0, action) { |
58 |
| - // this function returns the new state when an action comes |
| 115 | +function todoReducer(state = initialState, action) { |
59 | 116 | switch (action.type) {
|
60 |
| - case INCREMENT_COUNTER: |
61 |
| - return state + 1; |
62 |
| - case DECREMENT_COUNTER: |
63 |
| - return state - 1; |
64 |
| - default: |
65 |
| - return state; |
| 117 | + case ADD_TODO: |
| 118 | + return { |
| 119 | + ...state, |
| 120 | + todos: [ |
| 121 | + ...state.todos, |
| 122 | + { text: action.payload, id: state.idCounter + 1 } |
| 123 | + ], |
| 124 | + idCounter: state.idCounter + 1 |
| 125 | + }; |
| 126 | + case REMOVE_TODO: |
| 127 | + return { |
| 128 | + ...state, |
| 129 | + todos: state.todos.filter(todo => todo.id !== action.payload) |
| 130 | + }; |
| 131 | + default: |
| 132 | + return state; |
66 | 133 | }
|
| 134 | +} |
| 135 | +``` |
| 136 | + |
| 137 | +Whoa, what's going on here? A few things to note: |
| 138 | + |
| 139 | +- `state` is the previous state of the store. Redux will dispatch a dummy action immediately upon creating your store, at which point `state` is undefined. From that point on, `state` is the previous value returned by the reducer. |
| 140 | +- We're using a default parameter to specify the initial state of the store. |
| 141 | +- We're using a switch statement to check the action type. |
| 142 | +- **We're not mutating the previous state** — we're returning a **new** state object based on the **previous** state object. |
| 143 | + |
| 144 | +That last point is especially important: never mutate the previous state object. Always return a new state. Remember, reducers are pure functions, and should not perform mutations or side-effects. Here we're using the ES7 spread operator to shallow copy old state values to a new object. You can use a library like Immutable.js for a nicer API and better performance, since it uses [persistent data structures](http://en.wikipedia.org/wiki/Persistent_data_structure). Here's how that same store would look using immutable values: |
| 145 | + |
| 146 | +```js |
| 147 | +const initialState = Immutable.Map({ todos: [], idCounter: 0 }); |
67 | 148 |
|
68 |
| - // BUT THAT'S A SWITCH STATEMENT! |
69 |
| - // Right. If you hate 'em, see the FAQ below. |
| 149 | +function todoReducer(state = initialState, action) { |
| 150 | + switch (action.type) { |
| 151 | + case ADD_TODO: |
| 152 | + return state |
| 153 | + .update('todos', |
| 154 | + todos => todos.push(Immutable.Map({ |
| 155 | + text: action.payload, |
| 156 | + id: state.get('idCounter') |
| 157 | + }) |
| 158 | + )) |
| 159 | + .set('idCounter', state.get('idCounter') + 1); |
| 160 | + case REMOVE_TODO: |
| 161 | + return state |
| 162 | + .update('todos', |
| 163 | + todos => todos.filter(todo => todo.id !== action.payload ) |
| 164 | + ); |
| 165 | + default: |
| 166 | + return state; |
| 167 | + } |
70 | 168 | }
|
71 | 169 | ```
|
| 170 | + |
| 171 | +If you're thinking "yuck, switch statements," remember that reducers are just functions — you can abstract away these details using helpers. Check out [redux-actions](https://github.com/acdlite/redux-actions) for an example of how to use higher-order functions to create reducers. |
| 172 | + |
| 173 | +## Creating a store |
| 174 | + |
| 175 | +Now we have some action creators and a reducer to handle them. The next step is to create our store. |
| 176 | + |
| 177 | +To create a store, pass a reducer to `createStore()`: |
| 178 | + |
| 179 | +```js |
| 180 | +import { createStore } from 'redux'; |
| 181 | +import todoReducer from '../reducers/todos'; |
| 182 | +const store = createStore(todoReducer); |
| 183 | +``` |
| 184 | + |
| 185 | +Usually you'll have multiple reducers for different domains of data in your app. You can use the `combineReducers()` helper to combine multiple reducers into one: |
| 186 | + |
| 187 | +```js |
| 188 | +import { createStore, combineReducers } from 'redux'; |
| 189 | +import * as reducers from '../reducers'; |
| 190 | +const reducer = combine(reducers); |
| 191 | +const store = createStore(reducer); |
| 192 | +``` |
| 193 | + |
| 194 | +For example, if the object passed to `combineReducers()` looks like this: |
| 195 | + |
| 196 | +```js |
| 197 | +const reducers = { |
| 198 | + todos: todoReducer, |
| 199 | + counter: counterReducer |
| 200 | +}; |
| 201 | +``` |
| 202 | + |
| 203 | +It will create a reducer which produces a state object like this: |
| 204 | + |
| 205 | +```js |
| 206 | +const state = { |
| 207 | + todos: todoState, |
| 208 | + counter: counterState |
| 209 | +}; |
| 210 | +``` |
| 211 | + |
| 212 | +## Middleware |
| 213 | + |
| 214 | +Middleware is an optional feature of Redux that enables you to customize how dispatches are handled. Think of middleware as a certain type of plugin or extension for Redux. |
| 215 | + |
| 216 | +A common use for middleware is to enable asynchronous dispatches. For example, a promise middleware adds support for dispatching promises: |
| 217 | + |
| 218 | +```js |
| 219 | +dispatch(Promise.resolve({ type: ADD_TODO, payload: { text: 'Use middleware!' } })); |
| 220 | +``` |
| 221 | +A promise middleware would detect that a promise was dispatched, intercept it, and instead dispatch with the resolved value at a future point in time. |
| 222 | + |
| 223 | +Middleware is very simple to create using function composition. We won't focus on how middleware works in this document but here's how you enable it when creating your store: |
| 224 | + |
| 225 | +```js |
| 226 | +import { createStore, applyMiddleware } from 'redux'; |
| 227 | +// where promise, thunk, and observable are examples of middleware |
| 228 | +const store = applyMiddleware(promise, thunk, observable)(createStore)(reducer); |
| 229 | +``` |
| 230 | + |
| 231 | +Yes, you read that correctly. [Read more about how middleware works here.](middleware.md) |
| 232 | + |
| 233 | +## Connecting to your views |
| 234 | + |
| 235 | +You'll rarely interact with the store object directly. Most often, you'll use some sort of binding to your preferred view library. |
| 236 | + |
| 237 | +Flux is most popular within the React community, but Redux works with any kind of view layer. The React bindings for Redux are available as react-redux — see that project for details on how to integrate with React. |
| 238 | + |
| 239 | +However, if you do find yourself needing to access the store directly, the API for doing so is very simple: |
| 240 | + |
| 241 | +- `store.dispatch()` dispatches an action. |
| 242 | +- `store.getState()` gets the current state. |
| 243 | +- `store.subscribe()` registers a listener which is called after every dispatch, and returns a function which you call to unsubscribe. |
| 244 | + |
| 245 | + |
| 246 | +## Go make something great |
| 247 | + |
| 248 | +That's it! As you can see, despite the powerful features that Redux enables, the core of Redux is really quite simple. |
| 249 | + |
| 250 | +Please let us know if you have suggestions for how this guide could be improved. |
0 commit comments