diff --git a/docs/advanced/AsyncActions.md b/docs/advanced/AsyncActions.md index 9ef10f3246..77da01f504 100644 --- a/docs/advanced/AsyncActions.md +++ b/docs/advanced/AsyncActions.md @@ -215,8 +215,10 @@ Before going into the details of dispatching actions together with network reque ```js import { combineReducers } from 'redux' import { - SELECT_SUBREDDIT, INVALIDATE_SUBREDDIT, - REQUEST_POSTS, RECEIVE_POSTS + SELECT_SUBREDDIT, + INVALIDATE_SUBREDDIT, + REQUEST_POSTS, + RECEIVE_POSTS } from '../actions' function selectedSubreddit(state = 'reactjs', action) { @@ -228,11 +230,14 @@ function selectedSubreddit(state = 'reactjs', action) { } } -function posts(state = { - isFetching: false, - didInvalidate: false, - items: [] -}, action) { +function posts( + state = { + isFetching: false, + didInvalidate: false, + items: [] + }, + action +) { switch (action.type) { case INVALIDATE_SUBREDDIT: return Object.assign({}, state, { @@ -332,13 +337,11 @@ function receivePosts(subreddit, json) { // store.dispatch(fetchPosts('reactjs')) export function fetchPosts(subreddit) { - // Thunk middleware knows how to handle functions. // It passes the dispatch method as an argument to the function, // thus making it able to dispatch actions itself. - return function (dispatch) { - + return function(dispatch) { // First dispatch: the app state is updated to inform // that the API call is starting. @@ -353,7 +356,6 @@ export function fetchPosts(subreddit) { return fetch(`https://www.reddit.com/r/${subreddit}.json`) .then( response => response.json(), - // Do not use catch, because that will also catch // any errors in the dispatch and resulting render, // causing an loop of 'Unexpected batch number' errors. @@ -361,7 +363,6 @@ export function fetchPosts(subreddit) { error => console.log('An error occured.', error) ) .then(json => - // We can dispatch many times! // Here, we update the app state with the results of the API call. @@ -411,9 +412,7 @@ const store = createStore( ) store.dispatch(selectSubreddit('reactjs')) -store.dispatch(fetchPosts('reactjs')).then(() => - console.log(store.getState()) -) +store.dispatch(fetchPosts('reactjs')).then(() => console.log(store.getState())) ``` The nice thing about thunks is that they can dispatch results of each other: @@ -462,7 +461,6 @@ function shouldFetchPosts(state, subreddit) { } export function fetchPostsIfNeeded(subreddit) { - // Note that the function also receives getState() // which lets you choose what to dispatch next. @@ -486,9 +484,9 @@ This lets us write more sophisticated async control flow gradually, while the co #### `index.js` ```js -store.dispatch(fetchPostsIfNeeded('reactjs')).then(() => - console.log(store.getState()) -) +store + .dispatch(fetchPostsIfNeeded('reactjs')) + .then(() => console.log(store.getState())) ``` >##### Note about Server Rendering diff --git a/docs/advanced/ExampleRedditAPI.md b/docs/advanced/ExampleRedditAPI.md index 30054b325a..1ff0d1e43e 100644 --- a/docs/advanced/ExampleRedditAPI.md +++ b/docs/advanced/ExampleRedditAPI.md @@ -13,10 +13,7 @@ import React from 'react' import { render } from 'react-dom' import Root from './containers/Root' -render( - , - document.getElementById('root') -) +render(, document.getElementById('root')) ``` ## Action Creators and Constants @@ -97,24 +94,29 @@ export function fetchPostsIfNeeded(subreddit) { ```js import { combineReducers } from 'redux' import { - SELECT_SUBREDDIT, INVALIDATE_SUBREDDIT, - REQUEST_POSTS, RECEIVE_POSTS + SELECT_SUBREDDIT, + INVALIDATE_SUBREDDIT, + REQUEST_POSTS, + RECEIVE_POSTS } from './actions' function selectedSubreddit(state = 'reactjs', action) { switch (action.type) { - case SELECT_SUBREDDIT: - return action.subreddit - default: - return state + case SELECT_SUBREDDIT: + return action.subreddit + default: + return state } } -function posts(state = { - isFetching: false, - didInvalidate: false, - items: [] -}, action) { +function posts( + state = { + isFetching: false, + didInvalidate: false, + items: [] + }, + action +) { switch (action.type) { case INVALIDATE_SUBREDDIT: return Object.assign({}, state, { @@ -137,7 +139,7 @@ function posts(state = { } } -function postsBySubreddit(state = { }, action) { +function postsBySubreddit(state = {}, action) { switch (action.type) { case INVALIDATE_SUBREDDIT: case RECEIVE_POSTS: @@ -174,10 +176,7 @@ export default function configureStore(preloadedState) { return createStore( rootReducer, preloadedState, - applyMiddleware( - thunkMiddleware, - loggerMiddleware - ) + applyMiddleware(thunkMiddleware, loggerMiddleware) ) } ``` @@ -211,7 +210,11 @@ export default class Root extends Component { import React, { Component } from 'react' import PropTypes from 'prop-types' import { connect } from 'react-redux' -import { selectSubreddit, fetchPostsIfNeeded, invalidateSubreddit } from '../actions' +import { + selectSubreddit, + fetchPostsIfNeeded, + invalidateSubreddit +} from '../actions' import Picker from '../components/Picker' import Posts from '../components/Posts' @@ -251,34 +254,28 @@ class AsyncApp extends Component { const { selectedSubreddit, posts, isFetching, lastUpdated } = this.props return (
- +

{lastUpdated && Last updated at {new Date(lastUpdated).toLocaleTimeString()}. {' '} - - } + } {!isFetching && - + Refresh - - } + }

- {isFetching && posts.length === 0 && -

Loading...

- } - {!isFetching && posts.length === 0 && -

Empty.

- } + {isFetching && posts.length === 0 &&

Loading...

} + {!isFetching && posts.length === 0 &&

Empty.

} {posts.length > 0 &&
-
- } +
} ) } @@ -294,11 +291,9 @@ AsyncApp.propTypes = { function mapStateToProps(state) { const { selectedSubreddit, postsBySubreddit } = state - const { - isFetching, - lastUpdated, - items: posts - } = postsBySubreddit[selectedSubreddit] || { + const { isFetching, lastUpdated, items: posts } = postsBySubreddit[ + selectedSubreddit + ] || { isFetching: true, items: [] } @@ -329,13 +324,12 @@ export default class Picker extends Component { return (

{value}

- onChange(e.target.value)} value={value}> + {options.map(option => ( ) - } + + ))}
) @@ -343,9 +337,7 @@ export default class Picker extends Component { } Picker.propTypes = { - options: PropTypes.arrayOf( - PropTypes.string.isRequired - ).isRequired, + options: PropTypes.arrayOf(PropTypes.string.isRequired).isRequired, value: PropTypes.string.isRequired, onChange: PropTypes.func.isRequired } @@ -361,9 +353,7 @@ export default class Posts extends Component { render() { return ( ) } diff --git a/docs/advanced/Middleware.md b/docs/advanced/Middleware.md index fe51ff6493..2d7611224f 100644 --- a/docs/advanced/Middleware.md +++ b/docs/advanced/Middleware.md @@ -161,16 +161,14 @@ function applyMiddlewareByMonkeypatching(store, middlewares) { middlewares.reverse() // Transform dispatch function with each middleware. - middlewares.forEach(middleware => - store.dispatch = middleware(store) - ) + middlewares.forEach(middleware => (store.dispatch = middleware(store))) } ``` We could use it to apply multiple middleware like this: ```js -applyMiddlewareByMonkeypatching(store, [ logger, crashReporter ]) +applyMiddlewareByMonkeypatching(store, [logger, crashReporter]) ``` However, it is still monkeypatching. @@ -250,16 +248,11 @@ Instead of `applyMiddlewareByMonkeypatching()`, we could write `applyMiddleware( ```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) - ) - + middlewares.forEach(middleware => (dispatch = middleware(store)(dispatch))) return Object.assign({}, store, { dispatch }) } ``` @@ -368,10 +361,7 @@ const timeoutScheduler = store => next => action => { return next(action) } - let timeoutId = setTimeout( - () => next(action), - action.meta.delay - ) + let timeoutId = setTimeout(() => next(action), action.meta.delay) return function cancel() { clearTimeout(timeoutId) @@ -467,10 +457,9 @@ const readyStatePromise = store => next => action => { * `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) - + (typeof action === 'function' + ? action(store.dispatch, store.getState) + : next(action)) // You can use all of them! (It doesn't mean you should.) let todoApp = combineReducers(reducers) diff --git a/docs/advanced/UsageWithReactRouter.md b/docs/advanced/UsageWithReactRouter.md index 692d2681c4..9923b82264 100644 --- a/docs/advanced/UsageWithReactRouter.md +++ b/docs/advanced/UsageWithReactRouter.md @@ -17,19 +17,19 @@ Before integrating React Router, we need to configure our development server. In ### Configuring Express If you are serving your `index.html` from Express: -``` js - app.get('/*', (req,res) => { - res.sendFile(path.join(__dirname, 'index.html')) - }) +```js +app.get('/*', (req, res) => { + res.sendFile(path.join(__dirname, 'index.html')) +}) ``` ### Configuring WebpackDevServer If you are serving your `index.html` from WebpackDevServer: You can add to your webpack.config.dev.js: ```js - devServer: { - historyApiFallback: true, - } +devServer: { + historyApiFallback: true +} ``` ## Connecting React Router with Redux App @@ -39,7 +39,7 @@ Along this chapter, we will be using the [Todos](https://github.com/reactjs/redu First we will need to import `` and `` from React Router. Here's how to do it: ```js -import { Router, Route } from 'react-router'; +import { Router, Route } from 'react-router' ``` In a React app, usually you would wrap `` in `` so that when the URL changes, `` will match a branch of its routes, and render their configured components. `` is used to declaratively map routes to your application's component hierarchy. You would declare in `path` the path used in the URL and in `component` the single component to be rendered when the route matches the URL. @@ -48,8 +48,8 @@ In a React app, usually you would wrap `` in `` so that when const Root = () => ( - -); + +) ``` However, in our Redux App we will still need ``. `` is the higher-order component provided by React Redux that lets you bind Redux to React (see [Usage with React](../basics/UsageWithReact.md)). @@ -57,7 +57,7 @@ However, in our Redux App we will still need ``. `` is t We will then import the `` from React Redux: ```js -import { Provider } from 'react-redux'; +import { Provider } from 'react-redux' ``` We will wrap `` in `` so that route handlers can get [access to the `store`](http://redux.js.org/docs/basics/UsageWithReact.html#passing-the-store). @@ -69,7 +69,7 @@ const Root = ({ store }) => ( -); +) ``` Now the `` component will be rendered if the URL matches '/'. Additionally, we will add the optional `(:filter)` parameter to `/`, because we will need it further on when we try to read the parameter `(:filter)` from the URL. @@ -81,26 +81,26 @@ Now the `` component will be rendered if the URL matches '/'. Additionall You will probably want to remove the hash from the URL (e.g: `http://localhost:3000/#/?_k=4sbb0i`). For doing this, you will need to also import `browserHistory` from React Router: ```js -import { Router, Route, browserHistory } from 'react-router'; +import { Router, Route, browserHistory } from 'react-router' ``` and pass it to the `` in order to remove the hash from the URL: ```js - - - + + + ``` Unless you are targeting old browsers like IE9, you can always use `browserHistory`. #### `components/Root.js` -``` js -import React from 'react'; -import PropTypes from 'prop-types'; -import { Provider } from 'react-redux'; -import { Router, Route, browserHistory } from 'react-router'; -import App from './App'; +```js +import React from 'react' +import PropTypes from 'prop-types' +import { Provider } from 'react-redux' +import { Router, Route, browserHistory } from 'react-router' +import App from './App' const Root = ({ store }) => ( @@ -108,20 +108,20 @@ const Root = ({ store }) => ( -); +) Root.propTypes = { - store: PropTypes.object.isRequired, -}; + store: PropTypes.object.isRequired +} -export default Root; +export default Root ``` We will also need to refactor `index.js` to render the `` component to the DOM. #### `index.js` ```js -import React from 'react'; +import React from 'react' import { render } from 'react-dom' import { createStore } from 'redux' import todoApp from './reducers' @@ -129,10 +129,7 @@ import Root from './components/Root' let store = createStore(todoApp) -render( - , - document.getElementById('root') -) +render(, document.getElementById('root')) ``` ## Navigating with React Router @@ -142,8 +139,8 @@ React Router comes with a [``](https://github.com/ReactTraining/react-ro #### `containers/FilterLink.js` ```js -import React from 'react'; -import { Link } from 'react-router'; +import React from 'react' +import { Link } from 'react-router' const FilterLink = ({ filter, children }) => ( ( > {children} -); +) -export default FilterLink; +export default FilterLink ``` #### `components/Footer.js` @@ -166,22 +163,22 @@ import React from 'react' import FilterLink from '../containers/FilterLink' const Footer = () => ( -

- Show: - {" "} - - All - - {", "} - - Active - - {", "} - - Completed - -

-); +

+ Show: + {' '} + + All + + {', '} + + Active + + {', '} + + Completed + +

+) export default Footer ``` @@ -196,8 +193,8 @@ Currently, the todo list is not filtered even after the URL changed. This is bec const mapStateToProps = (state, ownProps) => { return { todos: getVisibleTodos(state.todos, ownProps.filter) // previously was getVisibleTodos(state.todos, state.visibilityFilter) - }; -}; + } +} ``` Right now we are not passing anything to `` so `ownProps` is an empty object. To filter our todos according to the URL, we want to pass the URL params to ``. @@ -214,13 +211,11 @@ const App = ({ params }) => { return (
- +
- ); -}; + ) +} ``` ## Next Steps diff --git a/docs/api/Store.md b/docs/api/Store.md index 661996d290..90edd69691 100644 --- a/docs/api/Store.md +++ b/docs/api/Store.md @@ -63,7 +63,7 @@ To learn how to describe asynchronous API calls, read the current state inside a ```js import { createStore } from 'redux' -let store = createStore(todos, [ 'Use Redux' ]) +let store = createStore(todos, ['Use Redux']) function addTodo(text) { return { @@ -113,9 +113,14 @@ let currentValue function handleChange() { let previousValue = currentValue currentValue = select(store.getState()) - + if (previousValue !== currentValue) { - console.log('Some deep nested property changed from', previousValue, 'to', currentValue) + console.log( + 'Some deep nested property changed from', + previousValue, + 'to', + currentValue + ) } } diff --git a/docs/api/applyMiddleware.md b/docs/api/applyMiddleware.md index 8e969bcbdd..34e9ac8afe 100644 --- a/docs/api/applyMiddleware.md +++ b/docs/api/applyMiddleware.md @@ -23,7 +23,7 @@ import { createStore, applyMiddleware } from 'redux' import todos from './reducers' function logger({ getState }) { - return (next) => (action) => { + return next => action => { console.log('will dispatch', action) // Call the next dispatch method in the middleware chain. @@ -37,11 +37,7 @@ function logger({ getState }) { } } -let store = createStore( - todos, - [ 'Use Redux' ], - applyMiddleware(logger) -) +let store = createStore(todos, ['Use Redux'], applyMiddleware(logger)) store.dispatch({ type: 'ADD_TODO', @@ -70,7 +66,6 @@ function fetchSecretSauce() { // These are the normal action creators you have seen so far. // The actions they return can be dispatched without any middleware. // However, they only express “facts” and not the “async flow”. - function makeASandwich(forPerson, secretSauce) { return { type: 'MAKE_SANDWICH', @@ -78,7 +73,6 @@ function makeASandwich(forPerson, secretSauce) { secretSauce } } - function apologize(fromPerson, toPerson, error) { return { type: 'APOLOGIZE', @@ -87,131 +81,74 @@ function apologize(fromPerson, toPerson, error) { error } } - function withdrawMoney(amount) { return { type: 'WITHDRAW', amount } -} - -// Even without middleware, you can dispatch an action: -store.dispatch(withdrawMoney(100)) - -// But what do you do when you need to start an asynchronous action, -// such as an API call, or a router transition? - -// Meet thunks. -// A thunk is a function that returns a function. -// This is a thunk. - +} // Even without middleware, you can dispatch an action: +store.dispatch(withdrawMoney(100)) // But what do you do when you need to start an asynchronous action, // such as an API call, or a router transition? // Meet thunks. // A thunk is a function that returns a function. // This is a thunk. function makeASandwichWithSecretSauce(forPerson) { - // Invert control! // Return a function that accepts `dispatch` so we can dispatch later. // Thunk middleware knows how to turn thunk async actions into actions. - - return function (dispatch) { + return function(dispatch) { return fetchSecretSauce().then( sauce => dispatch(makeASandwich(forPerson, sauce)), error => dispatch(apologize('The Sandwich Shop', forPerson, error)) ) } -} - -// Thunk middleware lets me dispatch thunk async actions -// as if they were actions! - -store.dispatch( - makeASandwichWithSecretSauce('Me') -) - -// It even takes care to return the thunk's return value -// from the dispatch, so I can chain Promises as long as I return them. - -store.dispatch( - makeASandwichWithSecretSauce('My wife') -).then(() => { +} // Thunk middleware lets me dispatch thunk async actions // as if they were actions! +store.dispatch(makeASandwichWithSecretSauce('Me')) // It even takes care to return the thunk's return value // from the dispatch, so I can chain Promises as long as I return them. +store.dispatch(makeASandwichWithSecretSauce('My wife')).then(() => { console.log('Done!') -}) - -// In fact I can write action creators that dispatch -// actions and async actions from other action creators, -// and I can build my control flow with Promises. - +}) // In fact I can write action creators that dispatch // actions and async actions from other action creators, // and I can build my control flow with Promises. function makeSandwichesForEverybody() { - return function (dispatch, getState) { + return function(dispatch, getState) { if (!getState().sandwiches.isShopOpen) { - // You don't have to return Promises, but it's a handy convention // so the caller can always call .then() on async dispatch result. - return Promise.resolve() - } - - // We can dispatch both plain object actions and other thunks, - // which lets us compose the asynchronous actions in a single flow. - - return dispatch( - makeASandwichWithSecretSauce('My Grandma') - ).then(() => - Promise.all([ - dispatch(makeASandwichWithSecretSauce('Me')), - dispatch(makeASandwichWithSecretSauce('My wife')) - ]) - ).then(() => - dispatch(makeASandwichWithSecretSauce('Our kids')) - ).then(() => - dispatch(getState().myMoney > 42 ? - withdrawMoney(42) : - apologize('Me', 'The Sandwich Shop') + } // We can dispatch both plain object actions and other thunks, // which lets us compose the asynchronous actions in a single flow. + return dispatch(makeASandwichWithSecretSauce('My Grandma')) + .then(() => + Promise.all([ + dispatch(makeASandwichWithSecretSauce('Me')), + dispatch(makeASandwichWithSecretSauce('My wife')) + ]) + ) + .then(() => dispatch(makeASandwichWithSecretSauce('Our kids'))) + .then(() => + dispatch( + getState().myMoney > 42 + ? withdrawMoney(42) + : apologize('Me', 'The Sandwich Shop') + ) ) - ) } -} - -// This is very useful for server side rendering, because I can wait -// until data is available, then synchronously render the app. - +} // This is very useful for server side rendering, because I can wait // until data is available, then synchronously render the app. import { renderToString } from 'react-dom/server' - -store.dispatch( - makeSandwichesForEverybody() -).then(() => - response.send(renderToString()) -) - -// I can also dispatch a thunk async action from a component -// any time its props change to load the missing data. - +store + .dispatch(makeSandwichesForEverybody()) + .then(() => response.send(renderToString())) // I can also dispatch a thunk async action from a component // any time its props change to load the missing data. import { connect } from 'react-redux' import { Component } from 'react' - class SandwichShop extends Component { componentDidMount() { - this.props.dispatch( - makeASandwichWithSecretSauce(this.props.forPerson) - ) + this.props.dispatch(makeASandwichWithSecretSauce(this.props.forPerson)) } - componentWillReceiveProps(nextProps) { if (nextProps.forPerson !== this.props.forPerson) { - this.props.dispatch( - makeASandwichWithSecretSauce(nextProps.forPerson) - ) + this.props.dispatch(makeASandwichWithSecretSauce(nextProps.forPerson)) } } - render() { return

{this.props.sandwiches.join('mustard')}

} } - -export default connect( - state => ({ - sandwiches: state.sandwiches - }) -)(SandwichShop) +export default connect(state => ({ + sandwiches: state.sandwiches +}))(SandwichShop) ``` #### Tips @@ -223,19 +160,19 @@ export default connect( * If you want to conditionally apply a middleware, make sure to only import it when it's needed: ```js - let middleware = [ a, b ] - if (process.env.NODE_ENV !== 'production') { - let c = require('some-debug-middleware') - let d = require('another-debug-middleware') - middleware = [ ...middleware, c, d ] - } +let middleware = [a, b] +if (process.env.NODE_ENV !== 'production') { + let c = require('some-debug-middleware') + let d = require('another-debug-middleware') + middleware = [...middleware, c, d] +} - const store = createStore( - reducer, - preloadedState, - applyMiddleware(...middleware) - ) - ``` +const store = createStore( + reducer, + preloadedState, + applyMiddleware(...middleware) +) +``` This makes it easier for bundling tools to cut out unneeded modules and reduces the size of your builds. diff --git a/docs/api/bindActionCreators.md b/docs/api/bindActionCreators.md index 68d7bf74ed..3c1bcc69c9 100644 --- a/docs/api/bindActionCreators.md +++ b/docs/api/bindActionCreators.md @@ -82,10 +82,7 @@ class TodoListContainer extends Component { // removeTodo: Function // } - return ( - - ) + return // An alternative to bindActionCreators is to pass // just the dispatch function down, but then your child component @@ -95,9 +92,7 @@ class TodoListContainer extends Component { } } -export default connect( - state => ({ todos: state.todos }) -)(TodoListContainer) +export default connect(state => ({ todos: state.todos }))(TodoListContainer) ``` #### Tips diff --git a/docs/api/combineReducers.md b/docs/api/combineReducers.md index a126c43c67..ba1d8b938f 100644 --- a/docs/api/combineReducers.md +++ b/docs/api/combineReducers.md @@ -55,7 +55,7 @@ While `combineReducers` attempts to check that your reducers conform to some of export default function todos(state = [], action) { switch (action.type) { case 'ADD_TODO': - return state.concat([ action.text ]) + return state.concat([action.text]) default: return state } diff --git a/docs/api/compose.md b/docs/api/compose.md index 887de902d0..4adbc23ffb 100644 --- a/docs/api/compose.md +++ b/docs/api/compose.md @@ -25,10 +25,7 @@ import reducer from '../reducers/index' const store = createStore( reducer, - compose( - applyMiddleware(thunk), - DevTools.instrument() - ) + compose(applyMiddleware(thunk), DevTools.instrument()) ) ``` diff --git a/docs/api/createStore.md b/docs/api/createStore.md index 8ccdec7d13..f97a0a1266 100644 --- a/docs/api/createStore.md +++ b/docs/api/createStore.md @@ -23,13 +23,13 @@ import { createStore } from 'redux' function todos(state = [], action) { switch (action.type) { case 'ADD_TODO': - return state.concat([ action.text ]) + return state.concat([action.text]) default: return state } } -let store = createStore(todos, [ 'Use Redux' ]) +let store = createStore(todos, ['Use Redux']) store.dispatch({ type: 'ADD_TODO', diff --git a/docs/basics/Actions.md b/docs/basics/Actions.md index 0446a36bcf..13b7040965 100644 --- a/docs/basics/Actions.md +++ b/docs/basics/Actions.md @@ -89,8 +89,8 @@ dispatch(completeTodo(index)) Alternatively, you can create a **bound action creator** that automatically dispatches: ```js -const boundAddTodo = (text) => dispatch(addTodo(text)) -const boundCompleteTodo = (index) => dispatch(completeTodo(index)) +const boundAddTodo = text => dispatch(addTodo(text)) +const boundCompleteTodo = index => dispatch(completeTodo(index)) ``` Now you'll be able to call them directly: diff --git a/docs/basics/DataFlow.md b/docs/basics/DataFlow.md index 00d07f205f..e5b1362318 100644 --- a/docs/basics/DataFlow.md +++ b/docs/basics/DataFlow.md @@ -27,26 +27,26 @@ The data lifecycle in any Redux app follows these 4 steps: The [store](Store.md) will pass two arguments to the [reducer](Reducers.md): the current state tree and the action. For example, in the todo app, the root reducer might receive something like this: ```js - // The current application state (list of todos and chosen filter) - let previousState = { - visibleTodoFilter: 'SHOW_ALL', - todos: [ - { - text: 'Read the docs.', - complete: false - } - ] +// The current application state (list of todos and chosen filter) +let previousState = { + visibleTodoFilter: 'SHOW_ALL', + todos: [ + { + text: 'Read the docs.', + complete: false } + ] +} - // The action being performed (adding a todo) - let action = { - type: 'ADD_TODO', - text: 'Understand the flow.' - } +// The action being performed (adding a todo) +let action = { + type: 'ADD_TODO', + text: 'Understand the flow.' +} - // Your reducer returns the next application state - let nextState = todoApp(previousState, action) - ``` +// Your reducer returns the next application state +let nextState = todoApp(previousState, action) +``` Note that a reducer is a pure function. It only *computes* the next state. It should be completely predictable: calling it with the same inputs many times should produce the same outputs. It shouldn't perform any side effects like API calls or router transitions. These should happen before an action is dispatched. @@ -57,28 +57,28 @@ The data lifecycle in any Redux app follows these 4 steps: Here's how [`combineReducers()`](../api/combineReducers.md) works. Let's say you have two reducers, one for a list of todos, and another for the currently selected filter setting: ```js - function todos(state = [], action) { - // Somehow calculate it... - return nextState - } - - function visibleTodoFilter(state = 'SHOW_ALL', action) { - // Somehow calculate it... - return nextState - } - - let todoApp = combineReducers({ - todos, - visibleTodoFilter - }) - ``` +function todos(state = [], action) { + // Somehow calculate it... + return nextState +} + +function visibleTodoFilter(state = 'SHOW_ALL', action) { + // Somehow calculate it... + return nextState +} + +let todoApp = combineReducers({ + todos, + visibleTodoFilter +}) +``` When you emit an action, `todoApp` returned by `combineReducers` will call both reducers: ```js - let nextTodos = todos(state.todos, action) - let nextVisibleTodoFilter = visibleTodoFilter(state.visibleTodoFilter, action) - ``` +let nextTodos = todos(state.todos, action) +let nextVisibleTodoFilter = visibleTodoFilter(state.visibleTodoFilter, action) +``` It will then combine both sets of results into a single state tree: diff --git a/docs/basics/ExampleTodoList.md b/docs/basics/ExampleTodoList.md index ea81a095c5..ad3430589e 100644 --- a/docs/basics/ExampleTodoList.md +++ b/docs/basics/ExampleTodoList.md @@ -30,7 +30,7 @@ render( ```js let nextTodoId = 0 -export const addTodo = (text) => { +export const addTodo = text => { return { type: 'ADD_TODO', id: nextTodoId++, @@ -38,14 +38,14 @@ export const addTodo = (text) => { } } -export const setVisibilityFilter = (filter) => { +export const setVisibilityFilter = filter => { return { type: 'SET_VISIBILITY_FILTER', filter } } -export const toggleTodo = (id) => { +export const toggleTodo = id => { return { type: 'TOGGLE_TODO', id @@ -74,7 +74,7 @@ const todo = (state = {}, action) => { return Object.assign({}, state, { completed: !state.completed }) - + default: return state } @@ -83,14 +83,9 @@ const todo = (state = {}, action) => { const todos = (state = [], action) => { switch (action.type) { case 'ADD_TODO': - return [ - ...state, - todo(undefined, action) - ] + return [...state, todo(undefined, action)] case 'TOGGLE_TODO': - return state.map(t => - todo(t, action) - ) + return state.map(t => todo(t, action)) default: return state } @@ -134,7 +129,7 @@ export default todoApp #### `components/Todo.js` ```js -import React, from 'react' +import React from 'react' import PropTypes from 'prop-types' const Todo = ({ onClick, completed, text }) => ( @@ -166,22 +161,20 @@ import Todo from './Todo' const TodoList = ({ todos, onTodoClick }) => (
    - {todos.map(todo => - onTodoClick(todo.id)} - /> - )} + {todos.map(todo => ( + onTodoClick(todo.id)} /> + ))}
) TodoList.propTypes = { - todos: PropTypes.arrayOf(PropTypes.shape({ - id: PropTypes.number.isRequired, - completed: PropTypes.bool.isRequired, - text: PropTypes.string.isRequired - }).isRequired).isRequired, + todos: PropTypes.arrayOf( + PropTypes.shape({ + id: PropTypes.number.isRequired, + completed: PropTypes.bool.isRequired, + text: PropTypes.string.isRequired + }).isRequired + ).isRequired, onTodoClick: PropTypes.func.isRequired } @@ -199,11 +192,12 @@ const Link = ({ active, children, onClick }) => { } return ( - { - e.preventDefault() - onClick() - }} + { + e.preventDefault() + onClick() + }} > {children} @@ -228,15 +222,15 @@ import FilterLink from '../containers/FilterLink' const Footer = () => (

Show: - {" "} + {' '} All - {", "} + {', '} Active - {", "} + {', '} Completed @@ -285,24 +279,21 @@ const getVisibleTodos = (todos, filter) => { } } -const mapStateToProps = (state) => { +const mapStateToProps = state => { return { todos: getVisibleTodos(state.todos, state.visibilityFilter) } } -const mapDispatchToProps = (dispatch) => { +const mapDispatchToProps = dispatch => { return { - onTodoClick: (id) => { + onTodoClick: id => { dispatch(toggleTodo(id)) } } } -const VisibleTodoList = connect( - mapStateToProps, - mapDispatchToProps -)(TodoList) +const VisibleTodoList = connect(mapStateToProps, mapDispatchToProps)(TodoList) export default VisibleTodoList ``` @@ -328,10 +319,7 @@ const mapDispatchToProps = (dispatch, ownProps) => { } } -const FilterLink = connect( - mapStateToProps, - mapDispatchToProps -)(Link) +const FilterLink = connect(mapStateToProps, mapDispatchToProps)(Link) export default FilterLink ``` @@ -350,17 +338,21 @@ let AddTodo = ({ dispatch }) => { return (

-
{ - e.preventDefault() - if (!input.value.trim()) { - return - } - dispatch(addTodo(input.value)) - input.value = '' - }}> - { - input = node - }} /> + { + e.preventDefault() + if (!input.value.trim()) { + return + } + dispatch(addTodo(input.value)) + input.value = '' + }} + > + { + input = node + }} + /> diff --git a/docs/basics/Reducers.md b/docs/basics/Reducers.md index 2ae31501ea..cff1965169 100644 --- a/docs/basics/Reducers.md +++ b/docs/basics/Reducers.md @@ -38,7 +38,7 @@ You'll often find that you need to store some data, as well as some UI state, in Now that we've decided what our state object looks like, we're ready to write a reducer for it. The reducer is a pure function that takes the previous state and an action, and returns the next state. ```js -(previousState, action) => newState +;(previousState, action) => newState ``` It's called a reducer because it's the type of function you would pass to [`Array.prototype.reduce(reducer, ?initialValue)`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/Reduce). It's very important that the reducer stays pure. Things you should **never** do inside a reducer: @@ -137,7 +137,7 @@ function todoApp(state = initialState, action) { completed: false } ] - }) + }) default: return state } @@ -188,7 +188,7 @@ function todoApp(state = initialState, action) { case TOGGLE_TODO: return Object.assign({}, state, { todos: state.todos.map((todo, index) => { - if(index === action.index) { + if (index === action.index) { return Object.assign({}, todo, { completed: !todo.completed }) @@ -252,7 +252,7 @@ Let's explore reducer composition more. Can we also extract a reducer managing j Below our imports, let's use [ES6 Object Destructuring](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment) to declare `SHOW_ALL`: ```js -const { SHOW_ALL } = VisibilityFilters; +const { SHOW_ALL } = VisibilityFilters ``` Then: @@ -380,7 +380,12 @@ All [`combineReducers()`](../api/combineReducers.md) does is generate a function ```js import { combineReducers } from 'redux' -import { ADD_TODO, TOGGLE_TODO, SET_VISIBILITY_FILTER, VisibilityFilters } from './actions' +import { + ADD_TODO, + TOGGLE_TODO, + SET_VISIBILITY_FILTER, + VisibilityFilters +} from './actions' const { SHOW_ALL } = VisibilityFilters function visibilityFilter(state = SHOW_ALL, action) { diff --git a/docs/basics/Store.md b/docs/basics/Store.md index 4bdc250e55..c0cddd7c92 100644 --- a/docs/basics/Store.md +++ b/docs/basics/Store.md @@ -31,16 +31,19 @@ let store = createStore(todoApp, window.STATE_FROM_SERVER) Now that we have created a store, let's verify our program works! Even without any UI, we can already test the update logic. ```js -import { addTodo, toggleTodo, setVisibilityFilter, VisibilityFilters } from './actions' +import { + addTodo, + toggleTodo, + setVisibilityFilter, + VisibilityFilters +} from './actions' // Log the initial state console.log(store.getState()) // Every time the state changes, log it // Note that subscribe() returns a function for unregistering the listener -let unsubscribe = store.subscribe(() => - console.log(store.getState()) -) +let unsubscribe = store.subscribe(() => console.log(store.getState())) // Dispatch some actions store.dispatch(addTodo('Learn about actions')) diff --git a/docs/basics/UsageWithReact.md b/docs/basics/UsageWithReact.md index 198f4b2a75..f47b02b805 100644 --- a/docs/basics/UsageWithReact.md +++ b/docs/basics/UsageWithReact.md @@ -146,22 +146,20 @@ import Todo from './Todo' const TodoList = ({ todos, onTodoClick }) => (
    - {todos.map(todo => - onTodoClick(todo.id)} - /> - )} + {todos.map(todo => ( + onTodoClick(todo.id)} /> + ))}
) TodoList.propTypes = { - todos: PropTypes.arrayOf(PropTypes.shape({ - id: PropTypes.number.isRequired, - completed: PropTypes.bool.isRequired, - text: PropTypes.string.isRequired - }).isRequired).isRequired, + todos: PropTypes.arrayOf( + PropTypes.shape({ + id: PropTypes.number.isRequired, + completed: PropTypes.bool.isRequired, + text: PropTypes.string.isRequired + }).isRequired + ).isRequired, onTodoClick: PropTypes.func.isRequired } @@ -180,11 +178,12 @@ const Link = ({ active, children, onClick }) => { } return ( - { - e.preventDefault() - onClick() - }} + { + e.preventDefault() + onClick() + }} > {children} @@ -209,15 +208,15 @@ import FilterLink from '../containers/FilterLink' const Footer = () => (

Show: - {" "} + {' '} All - {", "} + {', '} Active - {", "} + {', '} Completed @@ -264,7 +263,7 @@ const getVisibleTodos = (todos, filter) => { } } -const mapStateToProps = (state) => { +const mapStateToProps = state => { return { todos: getVisibleTodos(state.todos, state.visibilityFilter) } @@ -274,9 +273,9 @@ const mapStateToProps = (state) => { In addition to reading the state, container components can dispatch actions. In a similar fashion, you can define a function called `mapDispatchToProps()` that receives the [`dispatch()`](../api/Store.md#dispatch) method and returns callback props that you want to inject into the presentational component. For example, we want the `VisibleTodoList` to inject a prop called `onTodoClick` into the `TodoList` component, and we want `onTodoClick` to dispatch a `TOGGLE_TODO` action: ```js -const mapDispatchToProps = (dispatch) => { +const mapDispatchToProps = dispatch => { return { - onTodoClick: (id) => { + onTodoClick: id => { dispatch(toggleTodo(id)) } } @@ -288,10 +287,7 @@ Finally, we create the `VisibleTodoList` by calling `connect()` and passing thes ```js import { connect } from 'react-redux' -const VisibleTodoList = connect( - mapStateToProps, - mapDispatchToProps -)(TodoList) +const VisibleTodoList = connect(mapStateToProps, mapDispatchToProps)(TodoList) export default VisibleTodoList ``` @@ -321,10 +317,7 @@ const mapDispatchToProps = (dispatch, ownProps) => { } } -const FilterLink = connect( - mapStateToProps, - mapDispatchToProps -)(Link) +const FilterLink = connect(mapStateToProps, mapDispatchToProps)(Link) export default FilterLink ``` @@ -347,24 +340,21 @@ const getVisibleTodos = (todos, filter) => { } } -const mapStateToProps = (state) => { +const mapStateToProps = state => { return { todos: getVisibleTodos(state.todos, state.visibilityFilter) } } -const mapDispatchToProps = (dispatch) => { +const mapDispatchToProps = dispatch => { return { - onTodoClick: (id) => { + onTodoClick: id => { dispatch(toggleTodo(id)) } } } -const VisibleTodoList = connect( - mapStateToProps, - mapDispatchToProps -)(TodoList) +const VisibleTodoList = connect(mapStateToProps, mapDispatchToProps)(TodoList) export default VisibleTodoList ``` @@ -383,17 +373,21 @@ let AddTodo = ({ dispatch }) => { return (

- { - e.preventDefault() - if (!input.value.trim()) { - return - } - dispatch(addTodo(input.value)) - input.value = '' - }}> - { - input = node - }} /> + { + e.preventDefault() + if (!input.value.trim()) { + return + } + dispatch(addTodo(input.value)) + input.value = '' + }} + > + { + input = node + }} + /> diff --git a/docs/faq/ImmutableData.md b/docs/faq/ImmutableData.md index 0922130a87..ade942a24f 100644 --- a/docs/faq/ImmutableData.md +++ b/docs/faq/ImmutableData.md @@ -63,7 +63,7 @@ Redux's use of shallow equality checking requires immutability if any connected ### How do shallow and deep equality checking differ? Shallow equality checking (or _reference equality_) simply checks that two different _variables_ reference the same object; in contrast, deep equality checking (or _value equality_) must check every _value_ of two objects' properties. -A shallow equality check is therefore as simple (and as fast) as `a === b`, whereas a deep equality check involves a recursive traversal through the properties of two objects, comparing the value of each property at each step. +A shallow equality check is therefore as simple (and as fast) as `a === b`, whereas a deep equality check involves a recursive traversal through the properties of two objects, comparing the value of each property at each step. It's for this improvement in performance that Redux uses shallow equality checking. @@ -90,8 +90,8 @@ The [suggested structure](http://redux.js.org/docs/faq/Reducers.html#reducers-sh `combineReducers` makes working with this style of structure easier by taking a `reducers` argument that’s defined as a hash table comprising a set of key/value pairs, where each key is the name of a state slice, and the corresponding value is the reducer function that will act on it. So, for example, if your state shape is `{ todos, counter }`, the call to `combineReducers` would be: -``` - combineReducers({ todos: myTodosReducer, counter: myCounterReducer }) +```js +combineReducers({ todos: myTodosReducer, counter: myCounterReducer }) ``` where: @@ -105,7 +105,7 @@ where: As it continues through the iterations, `combineReducers` will construct a new state object with the state slices returned from each reducer. This new state object may or may not be different from the current state object. It is here that `combineReducers` uses shallow equality checking to determine whether the state has changed. -Specifically, at each stage of the iteration, `combineReducers` performs a shallow equality check on the current state slice and the state slice returned from the reducer. If the reducer returns a new object, the shallow equality check will fail, and `combineReducers` will set a `hasChanged` flag to true. +Specifically, at each stage of the iteration, `combineReducers` performs a shallow equality check on the current state slice and the state slice returned from the reducer. If the reducer returns a new object, the shallow equality check will fail, and `combineReducers` will set a `hasChanged` flag to true. After the iterations have completed, `combineReducers` will check the state of the `hasChanged` flag. If it’s true, the newly-constructed state object will be returned. If it’s false, the _current_ state object is returned. @@ -148,12 +148,12 @@ React-Redux performs a shallow equality check on on each _value_ within the prop It does so because the props object is actually a hash of prop names and their values (or selector functions that are used to retrieve or generate the values), such as in this example: -``` +```js function mapStateToProps(state) { - return { - todos: state.todos, // prop value - visibleTodos: getVisibleTodos(state) // selector - } + return { + todos: state.todos, // prop value + visibleTodos: getVisibleTodos(state) // selector + } } export default connect(mapStateToProps)(TodoApp) @@ -161,7 +161,7 @@ export default connect(mapStateToProps)(TodoApp) As such, a shallow equality check of the props object returned from repeated calls to `mapStateToProps` would always fail, as a new object would be returned each time. -React-Redux therefore maintains separate references to each _value_ in the returned props object. +React-Redux therefore maintains separate references to each _value_ in the returned props object. #### Further Information @@ -173,18 +173,18 @@ React-Redux therefore maintains separate references to each _value_ in the retur ### How does React-Redux use shallow equality checking to determine whether a component needs re-rendering? Each time React-Redux’s `connect` function is called, it will perform a shallow equality check on its stored reference to the root state object, and the current root state object passed to it from the store. If the check passes, the root state object has not been updated, and so there is no need to re-render the component, or even call `mapStateToProps`. -If the check fails, however, the root state object _has_ been updated, and so `connect` will call `mapStateToProps`to see if the props for the wrapped component have been updated. +If the check fails, however, the root state object _has_ been updated, and so `connect` will call `mapStateToProps`to see if the props for the wrapped component have been updated. It does this by performing a shallow equality check on each value within the object individually, and will only trigger a re-render if one of those checks fails. In the example below, if `state.todos` and the value returned from `getVisibleTodos()` do not change on successive calls to `connect`, then the component will not re-render . -``` +```js function mapStateToProps(state) { - return { - todos: state.todos, // prop value - visibleTodos: getVisibleTodos(state) // selector - } + return { + todos: state.todos, // prop value + visibleTodos: getVisibleTodos(state) // selector + } } export default connect(mapStateToProps)(TodoApp) @@ -192,16 +192,16 @@ export default connect(mapStateToProps)(TodoApp) Conversely, in this next example (below), the component will _always_ re-render, as the value of `todos` is always a new object, regardless of whether or not its values change: -``` +```js // AVOID - will always cause a re-render function mapStateToProps(state) { - return { - // todos always references a newly-created object - todos: { - all: state.todos, - visibleTodos: getVisibleTodos(state) - } - } + return { + // todos always references a newly-created object + todos: { + all: state.todos, + visibleTodos: getVisibleTodos(state) + } + } } export default connect(mapStateToProps)(TodoApp) @@ -228,16 +228,16 @@ Shallow equality checking cannot be used to detect if a function mutates an obje This is because two variables that reference the same object will _always_ be equal, regardless of whether the object’s values changes or not, as they're both referencing the same object. Thus, the following will always return true: -``` +```js function mutateObj(obj) { - obj.key = 'newValue'; - return obj; + obj.key = 'newValue' + return obj } -const param = { key: 'originalValue' }; -const returnVal = mutateObj(param); +const param = { key: 'originalValue' } +const returnVal = mutateObj(param) -param === returnVal; +param === returnVal //> true ``` @@ -254,7 +254,7 @@ The shallow check of `param` and `returnValue` simply checks whether both variab ### Does shallow equality checking with a mutable object cause problems with Redux? Shallow equality checking with a mutable object will not cause problems with Redux, but [it will cause problems with libraries that depend on the store, such as React-Redux](#shallow-checking-problems-with-react-redux). -Specifically, if the state slice passed to a reducer by `combineReducers` is a mutable object, the reducer can modify it directly and return it. +Specifically, if the state slice passed to a reducer by `combineReducers` is a mutable object, the reducer can modify it directly and return it. If it does, the shallow equality check that `combineReducers` performs will always pass, as the values of the state slice returned by the reducer may have been mutated, but the object itself has not - it’s still the same object that was passed to the reducer. @@ -289,37 +289,35 @@ As we’ve seen, the values in the mutable object returned by the selector funct For example, the following `mapStateToProps` function will never trigger a re-render: -``` +```js // State object held in the Redux store const state = { - user: { - accessCount: 0, - name: 'keith' - } -}; + user: { + accessCount: 0, + name: 'keith' + } +} // Selector function -const getUser = (state) => { - ++state.user.accessCount; // mutate the state object - return state; +const getUser = state => { + ++state.user.accessCount // mutate the state object + return state } // mapStateToProps -const mapStateToProps = (state) => ({ - // The object returned from getUser() is always - // the same object, so this wrapped - // component will never re-render, even though it's been - // mutated - userRecord: getUser(state) -}); - +const mapStateToProps = state => ({ + // The object returned from getUser() is always + // the same object, so this wrapped + // component will never re-render, even though it's been + // mutated + userRecord: getUser(state) +}) -const a = mapStateToProps(state); -const b = mapStateToProps(state); +const a = mapStateToProps(state) +const b = mapStateToProps(state) -a.userRecord === b.userRecord; +a.userRecord === b.userRecord //> true - ``` Note that, conversely, if an _immutable_ object is used, the [component may re-render when it should not](#immutability-issues-with-react-redux). @@ -335,7 +333,7 @@ Note that, conversely, if an _immutable_ object is used, the [component may re-r ### How does immutability enable a shallow check to detect object mutations? -If an object is immutable, any changes that need to be made to it within a function must be made to a _copy_ of the object. +If an object is immutable, any changes that need to be made to it within a function must be made to a _copy_ of the object. This mutated copy is a _separate_ object from that passed into the function, and so when it is returned, a shallow check will identify it as being a different object from that passed in, and so will fail. @@ -366,17 +364,17 @@ To prevent this from happening, you must *always return the state slice object t ### How can immutability in `mapStateToProps` cause components to render unnecessarily? Certain immutable operations, such as an Array filter, will always return a new object, even if the values themselves have not changed. -If such an operation is used as a selector function in `mapStateToProps`, the shallow equality check that React-Redux performs on each value +If such an operation is used as a selector function in `mapStateToProps`, the shallow equality check that React-Redux performs on each value in the props object that’s returned will always fail, as the selector is returning a new object each time. -As such, even though the values of that new object have not changed, the wrapped component will always be re-rendered, +As such, even though the values of that new object have not changed, the wrapped component will always be re-rendered, For example, the following will always trigger a re-render: -``` +```js // A JavaScript array's 'filter' method treats the array as immutable, // and returns a filtered copy of the array. -const getVisibleTodos = (todos) => todos.filter(t => !t.completed); +const getVisibleTodos = todos => todos.filter(t => !t.completed) const state = { todos: [ @@ -385,31 +383,31 @@ const state = { completed: false }, { - text: 'do todo 2', + text: 'do todo 2', completed: true - }] + } + ] } - - -const mapStateToProps = (state) => ({ - // getVisibleTodos() always returns a new array, and so the - // 'visibleToDos' prop will always reference a different array, - // causing the wrapped component to re-render, even if the array's - // values haven't changed - visibleToDos: getVisibleTodos(state.todos) + +const mapStateToProps = state => ({ + // getVisibleTodos() always returns a new array, and so the + // 'visibleToDos' prop will always reference a different array, + // causing the wrapped component to re-render, even if the array's + // values haven't changed + visibleToDos: getVisibleTodos(state.todos) }) -const a = mapStateToProps(state); +const a = mapStateToProps(state) // Call mapStateToProps(state) again with exactly the same arguments -const b = mapStateToProps(state); +const b = mapStateToProps(state) -a.visibleToDos; +a.visibleToDos //> { "completed": false, "text": "do todo 1" } -b.visibleToDos; +b.visibleToDos //> { "completed": false, "text": "do todo 1" } -a.visibleToDos === b.visibleToDos; +a.visibleToDos === b.visibleToDos //> false ``` @@ -425,8 +423,8 @@ Note that, conversely, if the values in your props object refer to mutable objec -## What approaches are there for handling data immutably? Do I have to use Immutable.JS? -You do not need to use Immutable.JS with Redux. Plain JavaScript, if written correctly, is perfectly capable of providing immutability without having to use an immutable-focused library. +## What approaches are there for handling data immutably? Do I have to use Immutable.JS? +You do not need to use Immutable.JS with Redux. Plain JavaScript, if written correctly, is perfectly capable of providing immutability without having to use an immutable-focused library. However, guaranteeing immutability with JavaScript is difficult, and it can be easy to mutate an object accidentally, causing bugs in your app that are extremely difficult to locate. For this reason, using an immutable update utility library such as Immutable.JS can significantly improve the reliability of your app, and make your app’s development much easier. @@ -442,7 +440,7 @@ However, guaranteeing immutability with JavaScript is difficult, and it can be e JavaScript was never designed to provide guaranteed immutable operations. Accordingly, there are several issues you need to be aware of if you choose to use it for your immutable operations in your Redux app. ### Accidental Object Mutation -With JavaScript, you can accidentally mutate an object (such as the Redux state tree) quite easily without realising it. For example, updating deeply nested properties, creating a new *reference* to an object instead of a new object, or performing a shallow copy rather than a deep copy, can all lead to inadvertent object mutations, and can trip up even the most experienced JavaScript coder. +With JavaScript, you can accidentally mutate an object (such as the Redux state tree) quite easily without realising it. For example, updating deeply nested properties, creating a new *reference* to an object instead of a new object, or performing a shallow copy rather than a deep copy, can all lead to inadvertent object mutations, and can trip up even the most experienced JavaScript coder. To avoid these issues, ensure you follow the recommended [immutable update patterns for ES6](http://redux.js.org/docs/recipes/reducers/ImmutableUpdatePatterns.html). diff --git a/docs/faq/ReactRedux.md b/docs/faq/ReactRedux.md index 3689ffb14d..4e95e8c00e 100644 --- a/docs/faq/ReactRedux.md +++ b/docs/faq/ReactRedux.md @@ -51,7 +51,7 @@ Note that “updating data immutably” does *not* mean that you must use [Immut React Redux implements several optimizations to ensure your actual component only re-renders when actually necessary. One of those is a shallow equality check on the combined props object generated by the `mapStateToProps` and `mapDispatchToProps` arguments passed to `connect`. Unfortunately, shallow equality does not help in cases where new array or object instances are created each time `mapStateToProps` is called. A typical example might be mapping over an array of IDs and returning the matching object references, such as: ```js -const mapStateToProps = (state) => { +const mapStateToProps = state => { return { objects: state.objectIds.map(id => state.objects[id]) } diff --git a/docs/introduction/CoreConcepts.md b/docs/introduction/CoreConcepts.md index 0398cf69c9..ab36749cb8 100644 --- a/docs/introduction/CoreConcepts.md +++ b/docs/introduction/CoreConcepts.md @@ -34,24 +34,25 @@ It would be hard to write such a function for a big app, so we write smaller fun ```js function visibilityFilter(state = 'SHOW_ALL', action) { if (action.type === 'SET_VISIBILITY_FILTER') { - return action.filter; + return action.filter } else { - return state; + return state } } function todos(state = [], action) { switch (action.type) { - case 'ADD_TODO': - return state.concat([{ text: action.text, completed: false }]); - case 'TOGGLE_TODO': - return state.map((todo, index) => - action.index === index ? - { text: todo.text, completed: !todo.completed } : - todo - ) - default: - return state; + case 'ADD_TODO': + return state.concat([{ text: action.text, completed: false }]) + case 'TOGGLE_TODO': + return state.map( + (todo, index) => + (action.index === index + ? { text: todo.text, completed: !todo.completed } + : todo) + ) + default: + return state } } ``` @@ -63,7 +64,7 @@ function todoApp(state = {}, action) { return { todos: todos(state.todos, action), visibilityFilter: visibilityFilter(state.visibilityFilter, action) - }; + } } ``` diff --git a/docs/introduction/ThreePrinciples.md b/docs/introduction/ThreePrinciples.md index 70aca9cf29..3eef79c003 100644 --- a/docs/introduction/ThreePrinciples.md +++ b/docs/introduction/ThreePrinciples.md @@ -53,7 +53,6 @@ store.dispatch({ 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': diff --git a/docs/recipes/ComputingDerivedData.md b/docs/recipes/ComputingDerivedData.md index e525b43ed7..95e47a27b8 100644 --- a/docs/recipes/ComputingDerivedData.md +++ b/docs/recipes/ComputingDerivedData.md @@ -24,24 +24,21 @@ const getVisibleTodos = (todos, filter) => { } } -const mapStateToProps = (state) => { +const mapStateToProps = state => { return { todos: getVisibleTodos(state.todos, state.visibilityFilter) } } -const mapDispatchToProps = (dispatch) => { +const mapDispatchToProps = dispatch => { return { - onTodoClick: (id) => { + onTodoClick: id => { dispatch(toggleTodo(id)) } } } -const VisibleTodoList = connect( - mapStateToProps, - mapDispatchToProps -)(TodoList) +const VisibleTodoList = connect(mapStateToProps, mapDispatchToProps)(TodoList) export default VisibleTodoList ``` @@ -61,11 +58,11 @@ Let's define a memoized selector named `getVisibleTodos` to replace the non-memo ```js import { createSelector } from 'reselect' -const getVisibilityFilter = (state) => state.visibilityFilter -const getTodos = (state) => state.todos +const getVisibilityFilter = state => state.visibilityFilter +const getTodos = state => state.todos export const getVisibleTodos = createSelector( - [ getVisibilityFilter, getTodos ], + [getVisibilityFilter, getTodos], (visibilityFilter, todos) => { switch (visibilityFilter) { case 'SHOW_ALL': @@ -86,13 +83,12 @@ In the example above, `getVisibilityFilter` and `getTodos` are input-selectors. A memoized selector can itself be an input-selector to another memoized selector. Here is `getVisibleTodos` being used as an input-selector to a selector that further filters the todos by keyword: ```js -const getKeyword = (state) => state.keyword +const getKeyword = state => state.keyword const getVisibleTodosFilteredByKeyword = createSelector( - [ getVisibleTodos, getKeyword ], - (visibleTodos, keyword) => visibleTodos.filter( - todo => todo.text.indexOf(keyword) > -1 - ) + [getVisibleTodos, getKeyword], + (visibleTodos, keyword) => + visibleTodos.filter(todo => todo.text.indexOf(keyword) > -1) ) ``` @@ -108,24 +104,21 @@ import { toggleTodo } from '../actions' import TodoList from '../components/TodoList' import { getVisibleTodos } from '../selectors' -const mapStateToProps = (state) => { +const mapStateToProps = state => { return { todos: getVisibleTodos(state) } } -const mapDispatchToProps = (dispatch) => { +const mapDispatchToProps = dispatch => { return { - onTodoClick: (id) => { + onTodoClick: id => { dispatch(toggleTodo(id)) } } } -const VisibleTodoList = connect( - mapStateToProps, - mapDispatchToProps -)(TodoList) +const VisibleTodoList = connect(mapStateToProps, mapDispatchToProps)(TodoList) export default VisibleTodoList ``` @@ -165,11 +158,10 @@ import { createSelector } from 'reselect' const getVisibilityFilter = (state, props) => state.todoLists[props.listId].visibilityFilter -const getTodos = (state, props) => - state.todoLists[props.listId].todos +const getTodos = (state, props) => state.todoLists[props.listId].todos const getVisibleTodos = createSelector( - [ getVisibilityFilter, getTodos ], + [getVisibilityFilter, getTodos], (visibilityFilter, todos) => { switch (visibilityFilter) { case 'SHOW_COMPLETED': @@ -216,18 +208,15 @@ const mapStateToProps = (state, props) => { } } -const mapDispatchToProps = (dispatch) => { +const mapDispatchToProps = dispatch => { return { - onTodoClick: (id) => { + onTodoClick: id => { dispatch(toggleTodo(id)) } } } -const VisibleTodoList = connect( - mapStateToProps, - mapDispatchToProps -)(TodoList) +const VisibleTodoList = connect(mapStateToProps, mapDispatchToProps)(TodoList) export default VisibleTodoList ``` @@ -250,12 +239,11 @@ import { createSelector } from 'reselect' const getVisibilityFilter = (state, props) => state.todoLists[props.listId].visibilityFilter -const getTodos = (state, props) => - state.todoLists[props.listId].todos +const getTodos = (state, props) => state.todoLists[props.listId].todos const makeGetVisibleTodos = () => { return createSelector( - [ getVisibilityFilter, getTodos ], + [getVisibilityFilter, getTodos], (visibilityFilter, todos) => { switch (visibilityFilter) { case 'SHOW_COMPLETED': @@ -310,18 +298,17 @@ const makeMapStateToProps = () => { return mapStateToProps } -const mapDispatchToProps = (dispatch) => { +const mapDispatchToProps = dispatch => { return { - onTodoClick: (id) => { + onTodoClick: id => { dispatch(toggleTodo(id)) } } } -const VisibleTodoList = connect( - makeMapStateToProps, - mapDispatchToProps -)(TodoList) +const VisibleTodoList = connect(makeMapStateToProps, mapDispatchToProps)( + TodoList +) export default VisibleTodoList ``` diff --git a/docs/recipes/ImplementingUndoHistory.md b/docs/recipes/ImplementingUndoHistory.md index 6556f29d07..17f0ae689e 100644 --- a/docs/recipes/ImplementingUndoHistory.md +++ b/docs/recipes/ImplementingUndoHistory.md @@ -44,7 +44,7 @@ It is reasonable to suggest that our state shape should change to answer these q ```js { counter: { - past: [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 ], + past: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], present: 10, future: [] } @@ -56,9 +56,9 @@ Now, if user presses “Undo”, we want it to change to move into the past: ```js { counter: { - past: [ 0, 1, 2, 3, 4, 5, 6, 7, 8 ], + past: [0, 1, 2, 3, 4, 5, 6, 7, 8], present: 9, - future: [ 10 ] + future: [10] } } ``` @@ -68,9 +68,9 @@ And further yet: ```js { counter: { - past: [ 0, 1, 2, 3, 4, 5, 6, 7 ], + past: [0, 1, 2, 3, 4, 5, 6, 7], present: 8, - future: [ 9, 10 ] + future: [9, 10] } } ``` @@ -80,9 +80,9 @@ When the user presses “Redo”, we want to move one step back into the future: ```js { counter: { - past: [ 0, 1, 2, 3, 4, 5, 6, 7, 8 ], + past: [0, 1, 2, 3, 4, 5, 6, 7, 8], present: 9, - future: [ 10 ] + future: [10] } } ``` @@ -92,7 +92,7 @@ Finally, if the user performs an action (e.g. decrement the counter) while we're ```js { counter: { - past: [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 ], + past: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], present: 8, future: [] } @@ -104,9 +104,9 @@ The interesting part here is that it does not matter whether we want to keep an ```js { counter: { - past: [ 0, 1, 2 ], + past: [0, 1, 2], present: 3, - future: [ 4 ] + future: [4] } } ``` @@ -116,12 +116,18 @@ The interesting part here is that it does not matter whether we want to keep an todos: { past: [ [], - [ { text: 'Use Redux' } ], - [ { text: 'Use Redux', complete: true } ] + [{ text: 'Use Redux' }], + [{ text: 'Use Redux', complete: true }] + ], + present: [ + { text: 'Use Redux', complete: true }, + { text: 'Implement Undo' } ], - present: [ { text: 'Use Redux', complete: true }, { text: 'Implement Undo' } ], future: [ - [ { text: 'Use Redux', complete: true }, { text: 'Implement Undo', complete: true } ] + [ + { text: 'Use Redux', complete: true }, + { text: 'Implement Undo', complete: true } + ] ] } } @@ -156,12 +162,12 @@ Or many granular histories so user can undo and redo actions in them independent ```js { counterA: { - past: [ 1, 0 ], + past: [1, 0], present: 2, future: [] }, counterB: { - past: [ 0 ], + past: [0], present: 1, future: [] } @@ -221,13 +227,13 @@ function undoable(state = initialState, action) { return { past: newPast, present: previous, - future: [ present, ...future ] + future: [present, ...future] } case 'REDO': const next = future[0] const newFuture = future.slice(1) return { - past: [ ...past, present ], + past: [...past, present], present: next, future: newFuture } @@ -256,7 +262,7 @@ A reducer enhancer that doesn't do anything looks like this: ```js function doNothingWith(reducer) { - return function (state, action) { + return function(state, action) { // Just call the passed reducer return reducer(state, action) } @@ -267,7 +273,7 @@ A reducer enhancer that combines other reducers might look like this: ```js function combineReducers(reducers) { - return function (state = {}, action) { + return function(state = {}, action) { return Object.keys(reducers).reduce((nextState, key) => { // Call every reducer with the part of the state it manages nextState[key] = reducers[key](state[key], action) @@ -291,7 +297,7 @@ function undoable(reducer) { } // Return a reducer that handles undo and redo - return function (state = initialState, action) { + return function(state = initialState, action) { const { past, present, future } = state switch (action.type) { @@ -301,13 +307,13 @@ function undoable(reducer) { return { past: newPast, present: previous, - future: [ present, ...future ] + future: [present, ...future] } case 'REDO': const next = future[0] const newFuture = future.slice(1) return { - past: [ ...past, present ], + past: [...past, present], present: next, future: newFuture } @@ -318,7 +324,7 @@ function undoable(reducer) { return state } return { - past: [ ...past, present ], + past: [...past, present], present: newPresent, future: [] } @@ -429,12 +435,18 @@ Now the `todos` part of the state looks like this: todos: { past: [ [], - [ { text: 'Use Redux' } ], - [ { text: 'Use Redux', complete: true } ] + [{ text: 'Use Redux' }], + [{ text: 'Use Redux', complete: true }] + ], + present: [ + { text: 'Use Redux', complete: true }, + { text: 'Implement Undo' } ], - present: [ { text: 'Use Redux', complete: true }, { text: 'Implement Undo' } ], future: [ - [ { text: 'Use Redux', complete: true }, { text: 'Implement Undo', complete: true } ] + [ + { text: 'Use Redux', complete: true }, + { text: 'Implement Undo', complete: true } + ] ] } } @@ -446,7 +458,7 @@ just `state.todos`: #### `containers/VisibleTodoList.js` ```js -const mapStateToProps = (state) => { +const mapStateToProps = state => { return { todos: getVisibleTodos(state.todos.present, state.visibilityFilter) } @@ -490,24 +502,21 @@ import { connect } from 'react-redux' /* ... */ -const mapStateToProps = (state) => { +const mapStateToProps = state => { return { canUndo: state.todos.past.length > 0, canRedo: state.todos.future.length > 0 } } -const mapDispatchToProps = (dispatch) => { +const mapDispatchToProps = dispatch => { return { onUndo: () => dispatch(UndoActionCreators.undo()), onRedo: () => dispatch(UndoActionCreators.redo()) } } -UndoRedo = connect( - mapStateToProps, - mapDispatchToProps -)(UndoRedo) +UndoRedo = connect(mapStateToProps, mapDispatchToProps)(UndoRedo) export default UndoRedo ``` diff --git a/docs/recipes/ReducingBoilerplate.md b/docs/recipes/ReducingBoilerplate.md index 4d2335d7a0..c7c647c4a4 100644 --- a/docs/recipes/ReducingBoilerplate.md +++ b/docs/recipes/ReducingBoilerplate.md @@ -86,12 +86,11 @@ function addTodoWithoutCheck(text) { export function addTodo(text) { // This form is allowed by Redux Thunk middleware // described below in “Async Action Creators” section. - return function (dispatch, getState) { + return function(dispatch, getState) { if (getState().todos.length === 3) { // Exit early return } - dispatch(addTodoWithoutCheck(text)) } } @@ -190,7 +189,11 @@ export function loadPostsRequest(userId) { ```js import { Component } from 'react' import { connect } from 'react-redux' -import { loadPostsRequest, loadPostsSuccess, loadPostsFailure } from './actionCreators' +import { + loadPostsRequest, + loadPostsSuccess, + loadPostsFailure +} from './actionCreators' class Posts extends Component { loadData(userId) { @@ -228,9 +231,7 @@ class Posts extends Component { return

Loading...

} - let posts = this.props.posts.map(post => - - ) + let posts = this.props.posts.map(post => ) return
{posts}
} @@ -258,7 +259,7 @@ Consider the code above rewritten with [redux-thunk](https://github.com/gaearon/ ```js export function loadPosts(userId) { // Interpreted by the thunk middleware: - return function (dispatch, getState) { + return function(dispatch, getState) { let { posts } = getState() if (posts[userId]) { // There is cached data! Don't do anything. @@ -272,16 +273,18 @@ export function loadPosts(userId) { // Dispatch vanilla actions asynchronously fetch(`http://myapi.com/users/${userId}/posts`).then( - response => dispatch({ - type: 'LOAD_POSTS_SUCCESS', - userId, - response - }), - error => dispatch({ - type: 'LOAD_POSTS_FAILURE', - userId, - error - }) + response => + dispatch({ + type: 'LOAD_POSTS_SUCCESS', + userId, + response + }), + error => + dispatch({ + type: 'LOAD_POSTS_FAILURE', + userId, + error + }) ) } } @@ -310,9 +313,7 @@ class Posts extends Component { return

Loading...

} - let posts = this.props.posts.map(post => - - ) + let posts = this.props.posts.map(post => ) return
{posts}
} @@ -333,7 +334,7 @@ export function loadPosts(userId) { // Types of actions to emit before and after types: ['LOAD_POSTS_REQUEST', 'LOAD_POSTS_SUCCESS', 'LOAD_POSTS_FAILURE'], // Check the cache (optional): - shouldCallAPI: (state) => !state.posts[userId], + shouldCallAPI: state => !state.posts[userId], // Perform the fetching: callAPI: () => fetch(`http://myapi.com/users/${userId}/posts`), // Arguments to inject in begin/end actions @@ -347,12 +348,7 @@ The middleware that interprets such actions could look like this: ```js function callAPIMiddleware({ dispatch, getState }) { return next => action => { - const { - types, - callAPI, - shouldCallAPI = () => true, - payload = {} - } = action + const { types, callAPI, shouldCallAPI = () => true, payload = {} } = action if (!types) { // Normal action: pass it on @@ -375,21 +371,29 @@ function callAPIMiddleware({ dispatch, getState }) { return } - const [ requestType, successType, failureType ] = types + const [requestType, successType, failureType] = types - dispatch(Object.assign({}, payload, { - type: requestType - })) + dispatch( + Object.assign({}, payload, { + type: requestType + }) + ) return callAPI().then( - response => dispatch(Object.assign({}, payload, { - response, - type: successType - })), - error => dispatch(Object.assign({}, payload, { - error, - type: failureType - })) + response => + dispatch( + Object.assign({}, payload, { + response, + type: successType + }) + ), + error => + dispatch( + Object.assign({}, payload, { + error, + type: failureType + }) + ) ) } } @@ -401,7 +405,7 @@ After passing it once to [`applyMiddleware(...middlewares)`](../api/applyMiddlew export function loadPosts(userId) { return { types: ['LOAD_POSTS_REQUEST', 'LOAD_POSTS_SUCCESS', 'LOAD_POSTS_FAILURE'], - shouldCallAPI: (state) => !state.posts[userId], + shouldCallAPI: state => !state.posts[userId], callAPI: () => fetch(`http://myapi.com/users/${userId}/posts`), payload: { userId } } @@ -409,8 +413,12 @@ export function loadPosts(userId) { export function loadComments(postId) { return { - types: ['LOAD_COMMENTS_REQUEST', 'LOAD_COMMENTS_SUCCESS', 'LOAD_COMMENTS_FAILURE'], - shouldCallAPI: (state) => !state.comments[postId], + types: [ + 'LOAD_COMMENTS_REQUEST', + 'LOAD_COMMENTS_SUCCESS', + 'LOAD_COMMENTS_FAILURE' + ], + shouldCallAPI: state => !state.comments[postId], callAPI: () => fetch(`http://myapi.com/posts/${postId}/comments`), payload: { postId } } @@ -418,15 +426,20 @@ export function loadComments(postId) { export function addComment(postId, message) { return { - types: ['ADD_COMMENT_REQUEST', 'ADD_COMMENT_SUCCESS', 'ADD_COMMENT_FAILURE'], - callAPI: () => fetch(`http://myapi.com/posts/${postId}/comments`, { - method: 'post', - headers: { - 'Accept': 'application/json', - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ message }) - }), + types: [ + 'ADD_COMMENT_REQUEST', + 'ADD_COMMENT_SUCCESS', + 'ADD_COMMENT_FAILURE' + ], + callAPI: () => + fetch(`http://myapi.com/posts/${postId}/comments`, { + method: 'post', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ message }) + }), payload: { postId, message } } } @@ -447,7 +460,7 @@ const TodoStore = Object.assign({}, EventEmitter.prototype, { } }) -AppDispatcher.register(function (action) { +AppDispatcher.register(function(action) { switch (action.type) { case ActionTypes.ADD_TODO: let text = action.text.trim() @@ -464,11 +477,11 @@ With Redux, the same update logic can be described as a reducing function: ```js export function todos(state = [], action) { switch (action.type) { - case ActionTypes.ADD_TODO: - let text = action.text.trim() - return [ ...state, text ] - default: - return state + case ActionTypes.ADD_TODO: + let text = action.text.trim() + return [...state, text] + default: + return state } } ``` @@ -485,7 +498,7 @@ Let's write a function that lets us express reducers as an object mapping from a export const todos = createReducer([], { [ActionTypes.ADD_TODO](state, action) { let text = action.text.trim() - return [ ...state, text ] + return [...state, text] } }) ``` diff --git a/docs/recipes/ServerRendering.md b/docs/recipes/ServerRendering.md index 507899f5d4..83f516f0e1 100644 --- a/docs/recipes/ServerRendering.md +++ b/docs/recipes/ServerRendering.md @@ -51,14 +51,18 @@ const app = Express() const port = 3000 //Serve static files -app.use('/static', Express.static('static')); +app.use('/static', Express.static('static')) // This is fired every time the server side receives a request app.use(handleRender) // We are going to fill these out in the sections to follow -function handleRender(req, res) { /* ... */ } -function renderFullPage(html, preloadedState) { /* ... */ } +function handleRender(req, res) { + /* ... */ +} +function renderFullPage(html, preloadedState) { + /* ... */ +} app.listen(port) ``` @@ -146,7 +150,7 @@ const preloadedState = window.__PRELOADED_STATE__ // Allow the passed state to be garbage-collected delete window.__PRELOADED_STATE__ - + // Create Redux store with initial state const store = createStore(counterApp, preloadedState) diff --git a/docs/recipes/UsingImmutableJS.md b/docs/recipes/UsingImmutableJS.md index dffb1f21c0..c4c1bd59e0 100644 --- a/docs/recipes/UsingImmutableJS.md +++ b/docs/recipes/UsingImmutableJS.md @@ -9,7 +9,7 @@ ## Why should I use an immutable-focused library such as Immutable.JS? -Immutable-focused libraries such as Immutable.JS have been designed to overcome the issues with immutability inherent within JavaScript, providing all the benefits of immutability with the performance your app requires. +Immutable-focused libraries such as Immutable.JS have been designed to overcome the issues with immutability inherent within JavaScript, providing all the benefits of immutability with the performance your app requires. Whether you choose to use such a library, or stick with plain JavaScript, depends on how comfortable you are with adding another dependency to your app, or how sure you are that you can avoid the pitfalls inherent within JavaScript’s approach to immutability. @@ -42,7 +42,7 @@ Immutable.JS provides a rich set of immutable objects to encapsulate your data ( Immutable.JS does a lot work behind the scenes to optimize performance. This is the key to its power, as using immutable data structures can involve a lot of expensive copying. In particular, immutably manipulating large, complex data sets, such as a nested Redux state tree, can generate many intermediate copies of objects, which consume memory and slow down performance as the browser’s garbage collector fights to clean things up. -Immutable.JS avoids this by [cleverly sharing data structures](https://medium.com/@dtinth/immutable-js-persistent-data-structures-and-structural-sharing-6d163fbd73d2#.z1g1ofrsi) under the surface, minimizing the need to copy data. It also enables complex chains of operations to be carried out without creating unnecessary (and costly) cloned intermediate data that will quickly be thrown away. +Immutable.JS avoids this by [cleverly sharing data structures](https://medium.com/@dtinth/immutable-js-persistent-data-structures-and-structural-sharing-6d163fbd73d2#.z1g1ofrsi) under the surface, minimizing the need to copy data. It also enables complex chains of operations to be carried out without creating unnecessary (and costly) cloned intermediate data that will quickly be thrown away. You never see this, of course - the data you give to an Immutable.JS object is never mutated. Rather, it’s the *intermediate* data generated within Immutable.JS from a chained sequence of method calls that is free to be mutated. You therefore get all the benefits of immutable data structures with none (or very little) of the potential performance hits. @@ -67,7 +67,7 @@ JavaScript does not provide immutable data structures. As such, for Immutable.JS For example, you will no longer be able to reference an object’s properties through standard JavaScript dot or bracket notation. Instead, you must reference them via Immutable.JS’s `get()` or `getIn()` methods, which use an awkward syntax that accesses properties via an array of strings, each of which represents a property key. -For example, instead of `myObj.prop1.prop2.prop3`, you would use `myImmutableMap.getIn([‘prop1’, ‘prop2’, ‘prop3’])`. +For example, instead of `myObj.prop1.prop2.prop3`, you would use `myImmutableMap.getIn([‘prop1’, ‘prop2’, ‘prop3’])`. This makes it awkward to interoperate not just with your own code, but also with other libraries, such as lodash or ramda, that expect plain JavaScript objects. @@ -75,7 +75,7 @@ Note that Immutable.JS objects do have a `toJS()` method, which returns the data ### Once used, Immutable.JS will spread throughout your codebase -Once you encapsulate your data with Immutable.JS, you have to use Immutable.JS’s `get()` or `getIn()` property accessors to access it. +Once you encapsulate your data with Immutable.JS, you have to use Immutable.JS’s `get()` or `getIn()` property accessors to access it. This has the effect of spreading Immutable.JS across your entire codebase, including potentially your components, where you may prefer not to have such external dependencies. Your entire codebase must know what is, and what is not, an Immutable.JS object. It also makes removing Immutable.JS from your app difficult in the future, should you ever need to. @@ -87,19 +87,19 @@ Because you must access your data via Immutable.JS’s own `get()` and `getIn()` ### Not suitable for small values that change often -Immutable.JS works best for collections of data, and the larger the better. It can be slow when your data comprises lots of small, simple JavaScript objects, with each comprising a few keys of primitive values. +Immutable.JS works best for collections of data, and the larger the better. It can be slow when your data comprises lots of small, simple JavaScript objects, with each comprising a few keys of primitive values. Note, however, that this does not apply to the Redux state tree, which is (usually) represented as a large collection of data. ### Difficult to Debug -Immutable.JS objects, such as `Map`, `List`, etc., can be difficult to debug, as inspecting such an object will reveal an entire nested hierarchy of Immutable.JS-specific properties that you don’t care about, while your actual data that you do care about is encapsulated several layers deep. +Immutable.JS objects, such as `Map`, `List`, etc., can be difficult to debug, as inspecting such an object will reveal an entire nested hierarchy of Immutable.JS-specific properties that you don’t care about, while your actual data that you do care about is encapsulated several layers deep. To resolve this issue, use a browser extension such as the [Immutable.js Object Formatter](https://chrome.google.com/webstore/detail/immutablejs-object-format/hgldghadipiblonfkkicmgcbbijnpeog), which surfaces your data in Chrome Dev Tools, and hides Immutable.JS’s properties when inspecting your data. ### Breaks object references, causing poor performance -One of the key advantages of immutability is that it enables shallow equality checking, which dramatically improves performance. +One of the key advantages of immutability is that it enables shallow equality checking, which dramatically improves performance. If two different variables reference the same immutable object, then a simple equality check of the two variables is enough to determine that they are equal, and that the object they both reference is unchanged. The equality check never has to check the values of any of the object’s properties, as it is, of course, immutable. @@ -109,12 +109,12 @@ Accordingly, calling `toJS()` twice, for example, and assigning the result to tw This is a particular issue if you use `toJS()` in a wrapped component’s `mapStateToProps` function, as React-Redux shallowly compares each value in the returned props object. For example, the value referenced by the `todos` prop returned from `mapStateToProps` below will always be a different object, and so will fail a shallow equality check. -``` +```js // AVOID .toJS() in mapStateToProps function mapStateToProps(state) { - return { - todos: state.get('todos').toJS() // Always a new object - } + return { + todos: state.get('todos').toJS() // Always a new object + } } ``` @@ -141,7 +141,7 @@ Frequently, yes. There are various tradeoffs and opinions to consider, but there Components will both re-render when they shouldn’t, and refuse to render when they should, and tracking down the bug causing the rendering issue is hard, as the component rendering incorrectly is not necessarily the one whose properties are being accidentally mutated. -This problem is caused predominantly by returning a mutated state object from a Redux reducer. With Immutable.JS, this problem simply does not exist, thereby removing a whole class of bugs from your app. +This problem is caused predominantly by returning a mutated state object from a Redux reducer. With Immutable.JS, this problem simply does not exist, thereby removing a whole class of bugs from your app. This, together with its performance and rich API for data manipulation, is why Immutable.JS is worth the effort. @@ -168,9 +168,9 @@ Never let a plain JavaScript object contain Immutable.JS properties. Equally, ne ### Make your entire Redux state tree an Immutable.JS object -For a Redux app, your entire state tree should be an Immutable.JS object, with no plain JavaScript objects used at all. +For a Redux app, your entire state tree should be an Immutable.JS object, with no plain JavaScript objects used at all. -* Create the tree using Immutable.JS’s `fromJS()` function. +* Create the tree using Immutable.JS’s `fromJS()` function. * Use an Immutable.JS-aware version of the `combineReducers` function, such as the one in [redux-immutable](https://www.npmjs.com/package/redux-immutable), as Redux itself expects the state tree to be a plain JavaScript object. @@ -178,16 +178,16 @@ For a Redux app, your entire state tree should be an Immutable.JS object, with n **Example** -``` +```js // avoid -const newObj = { key: value}; -const newState = state.setIn(['prop1’], newObj); // <-- newObj has been added as a plain - // JavaScript object - NOT as an Immutable.JS Map +const newObj = { key: value } +const newState = state.setIn(['prop1'], newObj) // <-- newObj has been added as a plain +// JavaScript object - NOT as an Immutable.JS Map // recommend -const newObj = { key: value}; -const newState = state.setIn(['prop1’], fromJS(newObj)); // <-- newObj is now an - // Immutable.JS Map +const newObj = { key: value } +const newState = state.setIn(['prop1'], fromJS(newObj)) // <-- newObj is now an +// Immutable.JS Map ``` #### Further Information @@ -201,7 +201,7 @@ const newState = state.setIn(['prop1’], fromJS(newObj)); // <-- newObj is now ### Use Immutable.JS everywhere except your dumb components -Using Immutable.JS everywhere keeps your code performant. Use it in your smart components, your selectors, your sagas or thunks, action creators, and especially your reducers. +Using Immutable.JS everywhere keeps your code performant. Use it in your smart components, your selectors, your sagas or thunks, action creators, and especially your reducers. Do not, however, use Immutable.JS in your dumb components. @@ -269,51 +269,50 @@ Something needs to map the Immutable.JS props in your Smart Component to the pur Here is an example of such a HOC: -``` -import React from 'react'; -import { Iterable } from 'immutable'; - -export const toJS = (WrappedComponent) => - (wrappedComponentProps) => { - const KEY = 0; - const VALUE = 1; - - const propsJS = Object.entries(wrappedComponentProps) - .reduce((newProps, wrappedComponentProp) => { - newProps[wrappedComponentProp[KEY]] = - Iterable.isIterable(wrappedComponentProp[VALUE]) - ? wrappedComponentProp[VALUE].toJS() - : wrappedComponentProp[VALUE]; - return newProps; - }, {}); - - return - }; - +```js +import React from 'react' +import { Iterable } from 'immutable' + +export const toJS = WrappedComponent => wrappedComponentProps => { + const KEY = 0 + const VALUE = 1 + + const propsJS = Object.entries( + wrappedComponentProps + ).reduce((newProps, wrappedComponentProp) => { + newProps[wrappedComponentProp[KEY]] = Iterable.isIterable( + wrappedComponentProp[VALUE] + ) + ? wrappedComponentProp[VALUE].toJS() + : wrappedComponentProp[VALUE] + return newProps + }, {}) + + return +} ``` And this is how you would use it in your Smart Component: -``` -import { connect } from 'react-redux'; - -import { toJS } from `./to-js'; +```js +import { connect } from 'react-redux' -import DumbComponent from './dumb.component'; +import { toJS } from './to-js' +import DumbComponent from './dumb.component' -const mapStateToProps = (state) => { - return { - /** - obj is an Immutable object in Smart Component, but it’s converted to a plain - JavaScript object by toJS, and so passed to DumbComponent as a pure JavaScript - object. Because it’s still an Immutable.JS object here in mapStateToProps, though, +const mapStateToProps = state => { + return { + /** + obj is an Immutable object in Smart Component, but it’s converted to a plain + JavaScript object by toJS, and so passed to DumbComponent as a pure JavaScript + object. Because it’s still an Immutable.JS object here in mapStateToProps, though, there is no issue with errant re-renderings. - */ - obj: getImmutableObjectFromStateTree(state) - } -}; - -export default connect(mapStateToProps)(toJS(DumbComponent)); + */ obj: getImmutableObjectFromStateTree( + state + ) + } +} +export default connect(mapStateToProps)(toJS(DumbComponent)) ``` By converting Immutable.JS objects to plain JavaScript values within a HOC, we achieve Dumb Component portability, but without the performance hits of using `toJS()` in the Smart Component. diff --git a/docs/recipes/UsingObjectSpreadOperator.md b/docs/recipes/UsingObjectSpreadOperator.md index 7addad5fd8..e705070d92 100644 --- a/docs/recipes/UsingObjectSpreadOperator.md +++ b/docs/recipes/UsingObjectSpreadOperator.md @@ -36,13 +36,11 @@ function todoApp(state = initialState, action) { The advantage of using the object spread syntax becomes more apparent when you're composing complex objects. Below `getAddedIds` maps an array of `id` values to an array of objects with values returned from `getProduct` and `getQuantity`. ```js -return getAddedIds(state.cart).map(id => Object.assign( - {}, - getProduct(state.products, id), - { +return getAddedIds(state.cart).map(id => + Object.assign({}, getProduct(state.products, id), { quantity: getQuantity(state.cart, id) - } -)) + }) +) ``` Object spread lets us simplify the above `map` call to: diff --git a/docs/recipes/WritingTests.md b/docs/recipes/WritingTests.md index 7d84dcb552..8dcc925fe6 100644 --- a/docs/recipes/WritingTests.md +++ b/docs/recipes/WritingTests.md @@ -13,7 +13,7 @@ npm install --save-dev jest To use it together with [Babel](http://babeljs.io), you will need to install `babel-jest`: -```js +``` npm install --save-dev babel-jest ``` @@ -80,7 +80,7 @@ For async action creators using [Redux Thunk](https://github.com/gaearon/redux-t #### Example ```js -import fetch from 'isomorphic-fetch'; +import fetch from 'isomorphic-fetch' function fetchTodosRequest() { return { @@ -123,7 +123,7 @@ import * as types from '../../constants/ActionTypes' import nock from 'nock' import expect from 'expect' // You can use any testing library -const middlewares = [ thunk ] +const middlewares = [thunk] const mockStore = configureMockStore(middlewares) describe('async actions', () => { @@ -134,18 +134,18 @@ describe('async actions', () => { it('creates FETCH_TODOS_SUCCESS when fetching todos has been done', () => { nock('http://example.com/') .get('/todos') - .reply(200, { body: { todos: ['do something'] }}) + .reply(200, { body: { todos: ['do something'] } }) const expectedActions = [ { type: types.FETCH_TODOS_REQUEST }, - { type: types.FETCH_TODOS_SUCCESS, body: { todos: ['do something'] } } + { type: types.FETCH_TODOS_SUCCESS, body: { todos: ['do something'] } } ] const store = mockStore({ todos: [] }) - return store.dispatch(actions.fetchTodos()) - .then(() => { // return of async actions - expect(store.getActions()).toEqual(expectedActions) - }) + return store.dispatch(actions.fetchTodos()).then(() => { + // return of async actions + expect(store.getActions()).toEqual(expectedActions) + }) }) }) ``` @@ -192,9 +192,7 @@ import * as types from '../../constants/ActionTypes' describe('todos reducer', () => { it('should return the initial state', () => { - expect( - reducer(undefined, {}) - ).toEqual([ + expect(reducer(undefined, {})).toEqual([ { text: 'Use Redux', completed: false, @@ -209,15 +207,13 @@ describe('todos reducer', () => { type: types.ADD_TODO, text: 'Run the tests' }) - ).toEqual( - [ - { - text: 'Run the tests', - completed: false, - id: 0 - } - ] - ) + ).toEqual([ + { + text: 'Run the tests', + completed: false, + id: 0 + } + ]) expect( reducer( @@ -233,20 +229,18 @@ describe('todos reducer', () => { text: 'Run the tests' } ) - ).toEqual( - [ - { - text: 'Run the tests', - completed: false, - id: 1 - }, - { - text: 'Use Redux', - completed: false, - id: 0 - } - ] - ) + ).toEqual([ + { + text: 'Run the tests', + completed: false, + id: 1 + }, + { + text: 'Use Redux', + completed: false, + id: 0 + } + ]) }) }) ``` @@ -279,11 +273,13 @@ class Header extends Component { render() { return ( -
-

todos

- +
+

todos

+
) } @@ -340,7 +336,6 @@ describe('components', () => { }) }) }) - ``` ### Connected Components @@ -352,7 +347,9 @@ Consider the following `App` component: ```js import { connect } from 'react-redux' -class App extends Component { /* ... */ } +class App extends Component { + /* ... */ +} export default connect(mapStateToProps)(App) ``` @@ -371,7 +368,9 @@ In order to be able to test the App component itself without having to deal with import { connect } from 'react-redux' // Use named export for unconnected component (for tests) -export class App extends Component { /* ... */ } +export class App extends Component { + /* ... */ +} // Use default export for the connected component (for app) export default connect(mapStateToProps)(App) @@ -420,7 +419,9 @@ const createFakeStore = fakeData => ({ const dispatchWithStoreOf = (storeData, action) => { let dispatched = null - const dispatch = singleDispatch(createFakeStore(storeData))(actionAttempt => dispatched = actionAttempt) + const dispatch = singleDispatch(createFakeStore(storeData))( + actionAttempt => (dispatched = actionAttempt) + ) dispatch(action) return dispatched } @@ -431,9 +432,7 @@ describe('middleware', () => { type: types.ADD_TODO } - expect( - dispatchWithStoreOf({}, action) - ).toEqual(action) + expect(dispatchWithStoreOf({}, action)).toEqual(action) }) it('should not dispatch if store already has type', () => { @@ -442,9 +441,12 @@ describe('middleware', () => { } expect( - dispatchWithStoreOf({ - [types.ADD_TODO]: 'dispatched' - }, action) + dispatchWithStoreOf( + { + [types.ADD_TODO]: 'dispatched' + }, + action + ) ).toNotExist() }) }) diff --git a/docs/recipes/reducers/BasicReducerStructure.md b/docs/recipes/reducers/BasicReducerStructure.md index bcc38d1c49..02c0a40390 100644 --- a/docs/recipes/reducers/BasicReducerStructure.md +++ b/docs/recipes/reducers/BasicReducerStructure.md @@ -6,24 +6,22 @@ First and foremost, it's important to understand that your entire application re - The first time the reducer is called, the `state` value will be `undefined`. The reducer needs to handle this case by supplying a default state value before handling the incoming action. - It needs to look at the previous state and the dispatched action, and determine what kind of work needs to be done - Assuming actual changes need to occur, it needs to create new objects and arrays with the updated data and return those -- If no changes are needed, it should return the existing state as-is. +- If no changes are needed, it should return the existing state as-is. The simplest possible approach to writing reducer logic is to put everything into a single function declaration, like this: ```js function counter(state, action) { if (typeof state === 'undefined') { - state = 0; // If state is undefined, initialize it with a default value + state = 0 // If state is undefined, initialize it with a default value } if (action.type === 'INCREMENT') { - return state + 1; - } - else if (action.type === 'DECREMENT') { - return state - 1; - } - else { - return state; // In case an action is passed in we don't understand + return state + 1 + } else if (action.type === 'DECREMENT') { + return state - 1 + } else { + return state // In case an action is passed in we don't understand } } ``` @@ -36,11 +34,11 @@ There are some simple tweaks that can be made to this reducer. First, repeated function counter(state = 0, action) { switch (action.type) { case 'INCREMENT': - return state + 1; + return state + 1 case 'DECREMENT': - return state - 1; + return state - 1 default: - return state; + return state } } ``` @@ -51,7 +49,7 @@ This is the basic structure that a typical Redux reducer function uses. Redux encourages you to think about your application in terms of the data you need to manage. The data at any given point in time is the "*state*" of your application, and the structure and organization of that state is typically referred to as its "*shape*". The shape of your state plays a major role in how you structure your reducer logic. -A Redux state usually has a plain Javascript object as the top of the state tree. (It is certainly possible to have another type of data instead, such as a single number, an array, or a specialized data structure, but most libraries assume that the top-level value is a plain object.) The most common way to organize data within that top-level object is to further divide data into sub-trees, where each top-level key represents some "domain" or "slice" of related data. For example, a basic Todo app's state might look like: +A Redux state usually has a plain Javascript object as the top of the state tree. (It is certainly possible to have another type of data instead, such as a single number, an array, or a specialized data structure, but most libraries assume that the top-level value is a plain object.) The most common way to organize data within that top-level object is to further divide data into sub-trees, where each top-level key represents some "domain" or "slice" of related data. For example, a basic Todo app's state might look like: ```js { @@ -86,13 +84,13 @@ A typical app's state shape might look roughly like: ```js { - domainData1 : {}, - domainData2 : {}, - appState1 : {}, - appState2 : {}, - ui : { - uiState1 : {}, - uiState2 : {}, - } + domainData1: {}, + domainData2: {}, + appState1: {}, + appState2: {}, + ui: { + uiState1: {}, + uiState2: {} + } } -``` \ No newline at end of file +``` diff --git a/docs/recipes/reducers/BeyondCombineReducers.md b/docs/recipes/reducers/BeyondCombineReducers.md index df2b47fd25..633dcd5ff6 100644 --- a/docs/recipes/reducers/BeyondCombineReducers.md +++ b/docs/recipes/reducers/BeyondCombineReducers.md @@ -16,29 +16,30 @@ Similarly, if `sliceReducerA` happens to need some data from `sliceReducerB`'s s ```js function combinedReducer(state, action) { - switch(action.type) { - case "A_TYPICAL_ACTION" : { - return { - a : sliceReducerA(state.a, action), - b : sliceReducerB(state.b, action) - }; - } - case "SOME_SPECIAL_ACTION" : { - return { - // specifically pass state.b as an additional argument - a : sliceReducerA(state.a, action, state.b), - b : sliceReducerB(state.b, action) - } - } - case "ANOTHER_SPECIAL_ACTION" : { - return { - a : sliceReducerA(state.a, action), - // specifically pass the entire state as an additional argument - b : sliceReducerB(state.b, action, state) - } - } - default: return state; + switch (action.type) { + case 'A_TYPICAL_ACTION': { + return { + a: sliceReducerA(state.a, action), + b: sliceReducerB(state.b, action) + } } + case 'SOME_SPECIAL_ACTION': { + return { + // specifically pass state.b as an additional argument + a: sliceReducerA(state.a, action, state.b), + b: sliceReducerB(state.b, action) + } + } + case 'ANOTHER_SPECIAL_ACTION': { + return { + a: sliceReducerA(state.a, action), + // specifically pass the entire state as an additional argument + b: sliceReducerB(state.b, action, state) + } + } + default: + return state + } } ``` @@ -46,17 +47,17 @@ Another alternative to the "shared-slice updates" issue would be to simply put m ```js function someSpecialActionCreator() { - return (dispatch, getState) => { - const state = getState(); - const dataFromB = selectImportantDataFromB(state); - - dispatch({ - type : "SOME_SPECIAL_ACTION", - payload : { - dataFromB - } - }); - } + return (dispatch, getState) => { + const state = getState() + const dataFromB = selectImportantDataFromB(state) + + dispatch({ + type: 'SOME_SPECIAL_ACTION', + payload: { + dataFromB + } + }) + } } ``` @@ -67,27 +68,28 @@ A third approach would be to use the reducer generated by `combineReducers` to h ```js const combinedReducer = combineReducers({ - a : sliceReducerA, - b : sliceReducerB -}); + a: sliceReducerA, + b: sliceReducerB +}) function crossSliceReducer(state, action) { - switch(action.type) { - case "SOME_SPECIAL_ACTION" : { - return { - // specifically pass state.b as an additional argument - a : handleSpecialCaseForA(state.a, action, state.b), - b : sliceReducerB(state.b, action) - } - } - default : return state; + switch (action.type) { + case 'SOME_SPECIAL_ACTION': { + return { + // specifically pass state.b as an additional argument + a: handleSpecialCaseForA(state.a, action, state.b), + b: sliceReducerB(state.b, action) + } } + default: + return state + } } function rootReducer(state, action) { - const intermediateState = combinedReducer(state, action); - const finalState = crossSliceReducer(intermediateState, action); - return finalState; + const intermediateState = combinedReducer(state, action) + const finalState = crossSliceReducer(intermediateState, action) + return finalState } ``` @@ -95,7 +97,7 @@ As it turns out, there's a useful utility called [reduce-reducers](https://githu ```js // Same as the "manual" rootReducer above -const rootReducer = reduceReducers(combinedReducers, crossSliceReducer); +const rootReducer = reduceReducers(combinedReducers, crossSliceReducer) ``` Note that if you use `reduceReducers`, you should make sure that the first reducer in the list is able to define the initial state, since the later reducers will generally assume that the entire state already exists and not try to provide defaults. @@ -106,11 +108,15 @@ Note that if you use `reduceReducers`, you should make sure that the first reduc Again, it's important to understand that Redux reducers are _just_ functions. While `combineReducers` is useful, it's just one tool in the toolbox. Functions can contain conditional logic other than switch statements, functions can be composed to wrap each other, and functions can call other functions. Maybe you need one of your slice reducers to be able to reset its state, and to only respond to specific actions overall. You could do: ```js -const undoableFilteredSliceA = compose(undoReducer, filterReducer("ACTION_1", "ACTION_2"), sliceReducerA); +const undoableFilteredSliceA = compose( + undoReducer, + filterReducer('ACTION_1', 'ACTION_2'), + sliceReducerA +) const rootReducer = combineReducers({ - a : undoableFilteredSliceA, - b : normalSliceReducerB -}); + a: undoableFilteredSliceA, + b: normalSliceReducerB +}) ``` Note that `combineReducers` doesn't know or care that there's anything special about the reducer function that's responsible for managing `a`. We didn't need to modify `combineReducers` to specifically know how to undo things - we just built up the pieces we needed into a new composed function. diff --git a/docs/recipes/reducers/ImmutableUpdatePatterns.md b/docs/recipes/reducers/ImmutableUpdatePatterns.md index 4cd77c3ec3..8449ddf14e 100644 --- a/docs/recipes/reducers/ImmutableUpdatePatterns.md +++ b/docs/recipes/reducers/ImmutableUpdatePatterns.md @@ -13,14 +13,14 @@ Defining a new variable does _not_ create a new actual object - it only creates ```js function updateNestedState(state, action) { - let nestedState = state.nestedState; - // ERROR: this directly modifies the existing object reference - don't do this! - nestedState.nestedField = action.data; - - return { - ...state, - nestedState - }; + let nestedState = state.nestedState + // ERROR: this directly modifies the existing object reference - don't do this! + nestedState.nestedField = action.data + + return { + ...state, + nestedState + } } ``` @@ -33,13 +33,13 @@ Another common version of this error looks like this: ```js function updateNestedState(state, action) { - // Problem: this only does a shallow copy! - let newState = {...state}; - - // ERROR: nestedState is still the same object! - newState.nestedState.nestedField = action.data; - - return newState; + // Problem: this only does a shallow copy! + let newState = { ...state } + + // ERROR: nestedState is still the same object! + newState.nestedState.nestedField = action.data + + return newState } ``` @@ -77,18 +77,15 @@ Normally, a Javascript array's contents are modified using mutative functions li ```js function insertItem(array, action) { - return [ - ...array.slice(0, action.index), - action.item, - ...array.slice(action.index) - ] + return [ + ...array.slice(0, action.index), + action.item, + ...array.slice(action.index) + ] } function removeItem(array, action) { - return [ - ...array.slice(0, action.index), - ...array.slice(action.index + 1) - ]; + return [...array.slice(0, action.index), ...array.slice(action.index + 1)] } ``` @@ -98,15 +95,15 @@ This means that we could also write the insert and remove functions like this: ```js function insertItem(array, action) { - let newArray = array.slice(); - newArray.splice(action.index, 0, action.item); - return newArray; + let newArray = array.slice() + newArray.splice(action.index, 0, action.item) + return newArray } function removeItem(array, action) { - let newArray = array.slice(); - newArray.splice(action.index, 1); - return newArray; + let newArray = array.slice() + newArray.splice(action.index, 1) + return newArray } ``` @@ -114,7 +111,7 @@ The remove function could also be implemented as: ```js function removeItem(array, action) { - return array.filter( (item, index) => index !== action.index); + return array.filter((item, index) => index !== action.index) } ``` @@ -124,18 +121,18 @@ Updating one item in an array can be accomplished by using `Array.map`, returnin ```js function updateObjectInArray(array, action) { - return array.map( (item, index) => { - if(index !== action.index) { - // This isn't the item we care about - keep it as-is - return item; - } - - // Otherwise, this is the one we want - return an updated value - return { - ...item, - ...action.item - }; - }); + return array.map((item, index) => { + if (index !== action.index) { + // This isn't the item we care about - keep it as-is + return item + } + + // Otherwise, this is the one we want - return an updated value + return { + ...item, + ...action.item + } + }) } ``` @@ -150,8 +147,10 @@ state = dotProp.set(state, `todos.${index}.complete`, true) Others, like [immutability-helper](https://github.com/kolodny/immutability-helper) (a fork of the now-deprecated React Immutability Helpers addon), use nested values and helper functions: ```js -var collection = [1, 2, {a: [12, 17, 15]}]; -var newCollection = update(collection, {2: {a: {$splice: [[1, 1, 13, 14]]}}}); +var collection = [1, 2, { a: [12, 17, 15] }] +var newCollection = update(collection, { + 2: { a: { $splice: [[1, 1, 13, 14]] } } +}) ``` They can provide a useful alternative to writing manual immutable update logic. diff --git a/docs/recipes/reducers/InitializingState.md b/docs/recipes/reducers/InitializingState.md index 0a2ff068e5..4ccf6e1668 100644 --- a/docs/recipes/reducers/InitializingState.md +++ b/docs/recipes/reducers/InitializingState.md @@ -27,19 +27,22 @@ Then your reducer might look like this: ```js function counter(state = 0, action) { switch (action.type) { - case 'INCREMENT': return state + 1; - case 'DECREMENT': return state - 1; - default: return state; + case 'INCREMENT': + return state + 1 + case 'DECREMENT': + return state - 1 + default: + return state } } ``` Now let's say you create a store with it. -``` -import { createStore } from 'redux'; -let store = createStore(counter); -console.log(store.getState()); // 0 +```js +import { createStore } from 'redux' +let store = createStore(counter) +console.log(store.getState()) // 0 ``` The initial state is zero. Why? Because the second argument to `createStore` was `undefined`. This is the `state` passed to your reducer the first time. When Redux initializes it dispatches a "dummy" action to fill the state. So your `counter` reducer was called with `state` equal to `undefined`. **This is exactly the case that "activates" the default argument.** Therefore, `state` is now `0` as per the default `state` value (`state = 0`). This state (`0`) will be returned. @@ -47,9 +50,9 @@ The initial state is zero. Why? Because the second argument to `createStore` w Let's consider a different scenario: ```js -import { createStore } from 'redux'; -let store = createStore(counter, 42); -console.log(store.getState()); // 42 +import { createStore } from 'redux' +let store = createStore(counter, 42) +console.log(store.getState()) // 42 ``` Why is it `42`, and not `0`, this time? Because `createStore` was called with `42` as the second argument. This argument becomes the `state` passed to your reducer along with the dummy action. **This time, `state` is not undefined (it's `42`!), so ES6 default argument syntax has no effect.** The `state` is `42`, and `42` is returned from the reducer. @@ -59,14 +62,14 @@ Why is it `42`, and not `0`, this time? Because `createStore` was called with `4 Now let's consider a case where you use `combineReducers()`. You have two reducers: - + ```js function a(state = 'lol', action) { - return state; + return state } function b(state = 'wat', action) { - return state; + return state } ``` @@ -76,26 +79,26 @@ The reducer generated by `combineReducers({ a, b })` looks like this: // const combined = combineReducers({ a, b }) function combined(state = {}, action) { return { - a: a(state.a, action), - b: b(state.b, action) - }; + a: a(state.a, action), + b: b(state.b, action) + } } ``` If we call `createStore` without the `preloadedState`, it's going to initialize the `state` to `{}`. Therefore, `state.a` and `state.b` will be `undefined` by the time it calls `a` and `b` reducers. **Both `a` and `b` reducers will receive `undefined` as *their* `state` arguments, and if they specify default `state` values, those will be returned.** This is how the combined reducer returns a `{ a: 'lol', b: 'wat' }` state object on the first invocation. ```js -import { createStore } from 'redux'; -let store = createStore(combined); -console.log(store.getState()); // { a: 'lol', b: 'wat' } +import { createStore } from 'redux' +let store = createStore(combined) +console.log(store.getState()) // { a: 'lol', b: 'wat' } ``` Let's consider a different scenario: ```js -import { createStore } from 'redux'; -let store = createStore(combined, { a: 'horse' }); -console.log(store.getState()); // { a: 'horse', b: 'wat' } +import { createStore } from 'redux' +let store = createStore(combined, { a: 'horse' }) +console.log(store.getState()) // { a: 'horse', b: 'wat' } ``` Now I specified the `preloadedState` as the argument to `createStore()`. The state returned from the combined reducer *combines* the initial state I specified for the `a` reducer with the `'wat'` default argument specified that `b` reducer chose itself. @@ -106,9 +109,9 @@ Let's recall what the combined reducer does: // const combined = combineReducers({ a, b }) function combined(state = {}, action) { return { - a: a(state.a, action), - b: b(state.b, action) - }; + a: a(state.a, action), + b: b(state.b, action) + } } ``` diff --git a/docs/recipes/reducers/NormalizingStateShape.md b/docs/recipes/reducers/NormalizingStateShape.md index 5b6279ea62..1d6ae4802b 100644 --- a/docs/recipes/reducers/NormalizingStateShape.md +++ b/docs/recipes/reducers/NormalizingStateShape.md @@ -4,46 +4,46 @@ Many applications deal with data that is nested or relational in nature. For ex ```js const blogPosts = [ - { - id : "post1", - author : {username : "user1", name : "User 1"}, - body : "......", - comments : [ - { - id : "comment1", - author : {username : "user2", name : "User 2"}, - comment : ".....", - }, - { - id : "comment2", - author : {username : "user3", name : "User 3"}, - comment : ".....", - } - ] - }, - { - id : "post2", - author : {username : "user2", name : "User 2"}, - body : "......", - comments : [ - { - id : "comment3", - author : {username : "user3", name : "User 3"}, - comment : ".....", - }, - { - id : "comment4", - author : {username : "user1", name : "User 1"}, - comment : ".....", - }, - { - id : "comment5", - author : {username : "user3", name : "User 3"}, - comment : ".....", - } - ] - } - // and repeat many times + { + id: 'post1', + author: { username: 'user1', name: 'User 1' }, + body: '......', + comments: [ + { + id: 'comment1', + author: { username: 'user2', name: 'User 2' }, + comment: '.....' + }, + { + id: 'comment2', + author: { username: 'user3', name: 'User 3' }, + comment: '.....' + } + ] + }, + { + id: 'post2', + author: { username: 'user2', name: 'User 2' }, + body: '......', + comments: [ + { + id: 'comment3', + author: { username: 'user3', name: 'User 3' }, + comment: '.....' + }, + { + id: 'comment4', + author: { username: 'user1', name: 'User 1' }, + comment: '.....' + }, + { + id: 'comment5', + author: { username: 'user3', name: 'User 3' }, + comment: '.....' + } + ] + } + // and repeat many times ] ``` @@ -68,70 +68,70 @@ An example of a normalized state structure for the blog example above might look ```js { - posts : { - byId : { - "post1" : { - id : "post1", - author : "user1", - body : "......", - comments : ["comment1", "comment2"] - }, - "post2" : { - id : "post2", - author : "user2", - body : "......", - comments : ["comment3", "comment4", "comment5"] - } - } - allIds : ["post1", "post2"] + posts: { + byId: { + post1: { + id: 'post1', + author: 'user1', + body: '......', + comments: ['comment1', 'comment2'] + }, + post2: { + id: 'post2', + author: 'user2', + body: '......', + comments: ['comment3', 'comment4', 'comment5'] + } }, - comments : { - byId : { - "comment1" : { - id : "comment1", - author : "user2", - comment : ".....", - }, - "comment2" : { - id : "comment2", - author : "user3", - comment : ".....", - }, - "comment3" : { - id : "comment3", - author : "user3", - comment : ".....", - }, - "comment4" : { - id : "comment4", - author : "user1", - comment : ".....", - }, - "comment5" : { - id : "comment5", - author : "user3", - comment : ".....", - }, - }, - allIds : ["comment1", "comment2", "comment3", "commment4", "comment5"] + allIds: ['post1', 'post2'] + }, + comments: { + byId: { + comment1: { + id: 'comment1', + author: 'user2', + comment: '.....' + }, + comment2: { + id: 'comment2', + author: 'user3', + comment: '.....' + }, + comment3: { + id: 'comment3', + author: 'user3', + comment: '.....' + }, + comment4: { + id: 'comment4', + author: 'user1', + comment: '.....' + }, + comment5: { + id: 'comment5', + author: 'user3', + comment: '.....' + } }, - users : { - byId : { - "user1" : { - username : "user1", - name : "User 1", - } - "user2" : { - username : "user2", - name : "User 2", - } - "user3" : { - username : "user3", - name : "User 3", - } - }, - allIds : ["user1", "user2", "user3"] - } + allIds: ['comment1', 'comment2', 'comment3', 'commment4', 'comment5'] + }, + users: { + byId: { + user1: { + username: 'user1', + name: 'User 1' + }, + user2: { + username: 'user2', + name: 'User 2' + }, + user3: { + username: 'user3', + name: 'User 3' + } + }, + allIds: ['user1', 'user2', 'user3'] + } } ``` @@ -151,16 +151,16 @@ A typical application will likely have a mixture of relational data and non-rela ```js { - simpleDomainData1: {....}, - simpleDomainData2: {....} - entities : { - entityType1 : {....}, - entityType2 : {....} - } - ui : { - uiSection1 : {....}, - uiSection2 : {....} - } + simpleDomainData1: { ... }, + simpleDomainData2: { ... }, + entities: { + entityType1: { ... }, + entityType2: { ... } + }, + ui: { + uiSection1: { ... }, + uiSection2: { ... } + } } ``` @@ -173,31 +173,30 @@ Because we're treating a portion of our Redux store as a "database", many of the ```js { - entities: { - authors : { byId : {}, allIds : [] }, - books : { byId : {}, allIds : [] }, - authorBook : { - byId : { - 1 : { - id : 1, - authorId : 5, - bookId : 22 - }, - 2 : { - id : 2, - authorId : 5, - bookId : 15, - } - 3 : { - id : 3, - authorId : 42, - bookId : 12 - } - }, - allIds : [1, 2, 3] - + entities: { + authors: { byId: {}, allIds: [] }, + books: { byId: {}, allIds: [] }, + authorBook: { + byId: { + 1: { + id: 1, + authorId: 5, + bookId: 22 + }, + 2: { + id: 2, + authorId: 5, + bookId: 15 + }, + 3: { + id: 3, + authorId: 42, + bookId: 12 } + }, + allIds: [1, 2, 3] } + } } ``` diff --git a/docs/recipes/reducers/RefactoringReducersExample.md b/docs/recipes/reducers/RefactoringReducersExample.md index e38b37ac35..72ce79bce9 100644 --- a/docs/recipes/reducers/RefactoringReducersExample.md +++ b/docs/recipes/reducers/RefactoringReducersExample.md @@ -10,57 +10,56 @@ Let's say that our initial reducer looks like this: ```js const initialState = { - visibilityFilter : 'SHOW_ALL', - todos : [] -}; - + visibilityFilter: 'SHOW_ALL', + todos: [] +} function appReducer(state = initialState, action) { - switch(action.type) { - case 'SET_VISIBILITY_FILTER' : { - return Object.assign({}, state, { - visibilityFilter : action.filter - }); - } - case 'ADD_TODO' : { - return Object.assign({}, state, { - todos : state.todos.concat({ - id: action.id, - text: action.text, - completed: false - }) - }); - } - case 'TOGGLE_TODO' : { - return Object.assign({}, state, { - todos : state.todos.map(todo => { - if (todo.id !== action.id) { - return todo; - } - - return Object.assign({}, todo, { - completed : !todo.completed - }) - }) - }); - } - case 'EDIT_TODO' : { - return Object.assign({}, state, { - todos : state.todos.map(todo => { - if (todo.id !== action.id) { - return todo; - } - - return Object.assign({}, todo, { - text : action.text - }) - }) - }); - } - default : return state; + switch (action.type) { + case 'SET_VISIBILITY_FILTER': { + return Object.assign({}, state, { + visibilityFilter: action.filter + }) + } + case 'ADD_TODO': { + return Object.assign({}, state, { + todos: state.todos.concat({ + id: action.id, + text: action.text, + completed: false + }) + }) + } + case 'TOGGLE_TODO': { + return Object.assign({}, state, { + todos: state.todos.map(todo => { + if (todo.id !== action.id) { + return todo + } + + return Object.assign({}, todo, { + completed: !todo.completed + }) + }) + }) } + case 'EDIT_TODO': { + return Object.assign({}, state, { + todos: state.todos.map(todo => { + if (todo.id !== action.id) { + return todo + } + + return Object.assign({}, todo, { + text: action.text + }) + }) + }) + } + default: + return state + } } - ``` That function is fairly short, but already becoming overly complex. We're dealing with two different areas of concern (filtering vs managing our list of todos), the nesting is making the update logic harder to read, and it's not exactly clear what's going on everywhere. @@ -72,56 +71,57 @@ A good first step might be to break out a utility function to return a new objec ```js function updateObject(oldObject, newValues) { - // Encapsulate the idea of passing a new object as the first parameter - // to Object.assign to ensure we correctly copy data instead of mutating - return Object.assign({}, oldObject, newValues); + // Encapsulate the idea of passing a new object as the first parameter + // to Object.assign to ensure we correctly copy data instead of mutating + return Object.assign({}, oldObject, newValues) } function updateItemInArray(array, itemId, updateItemCallback) { - const updatedItems = array.map(item => { - if(item.id !== itemId) { - // Since we only want to update one item, preserve all others as they are now - return item; - } - - // Use the provided callback to create an updated item - const updatedItem = updateItemCallback(item); - return updatedItem; - }); - - return updatedItems; + const updatedItems = array.map(item => { + if (item.id !== itemId) { + // Since we only want to update one item, preserve all others as they are now + return item + } + + // Use the provided callback to create an updated item + const updatedItem = updateItemCallback(item) + return updatedItem + }) + + return updatedItems } function appReducer(state = initialState, action) { - switch(action.type) { - case 'SET_VISIBILITY_FILTER' : { - return updateObject(state, {visibilityFilter : action.filter}); - } - case 'ADD_TODO' : { - const newTodos = state.todos.concat({ - id: action.id, - text: action.text, - completed: false - }); - - return updateObject(state, {todos : newTodos}); - } - case 'TOGGLE_TODO' : { - const newTodos = updateItemInArray(state.todos, action.id, todo => { - return updateObject(todo, {completed : !todo.completed}); - }); - - return updateObject(state, {todos : newTodos}); - } - case 'EDIT_TODO' : { - const newTodos = updateItemInArray(state.todos, action.id, todo => { - return updateObject(todo, {text : action.text}); - }); - - return updateObject(state, {todos : newTodos}); - } - default : return state; + switch (action.type) { + case 'SET_VISIBILITY_FILTER': { + return updateObject(state, { visibilityFilter: action.filter }) + } + case 'ADD_TODO': { + const newTodos = state.todos.concat({ + id: action.id, + text: action.text, + completed: false + }) + + return updateObject(state, { todos: newTodos }) + } + case 'TOGGLE_TODO': { + const newTodos = updateItemInArray(state.todos, action.id, todo => { + return updateObject(todo, { completed: !todo.completed }) + }) + + return updateObject(state, { todos: newTodos }) + } + case 'EDIT_TODO': { + const newTodos = updateItemInArray(state.todos, action.id, todo => { + return updateObject(todo, { text: action.text }) + }) + + return updateObject(state, { todos: newTodos }) } + default: + return state + } } ``` @@ -136,45 +136,49 @@ Next, we can split each specific case into its own function: function updateObject(oldObject, newValues) {} function updateItemInArray(array, itemId, updateItemCallback) {} - function setVisibilityFilter(state, action) { - return updateObject(state, {visibilityFilter : action.filter }); + return updateObject(state, { visibilityFilter: action.filter }) } function addTodo(state, action) { - const newTodos = state.todos.concat({ - id: action.id, - text: action.text, - completed: false - }); - - return updateObject(state, {todos : newTodos}); + const newTodos = state.todos.concat({ + id: action.id, + text: action.text, + completed: false + }) + + return updateObject(state, { todos: newTodos }) } function toggleTodo(state, action) { - const newTodos = updateItemInArray(state.todos, action.id, todo => { - return updateObject(todo, {completed : !todo.completed}); - }); - - return updateObject(state, {todos : newTodos}); + const newTodos = updateItemInArray(state.todos, action.id, todo => { + return updateObject(todo, { completed: !todo.completed }) + }) + + return updateObject(state, { todos: newTodos }) } function editTodo(state, action) { - const newTodos = updateItemInArray(state.todos, action.id, todo => { - return updateObject(todo, {text : action.text}); - }); - - return updateObject(state, {todos : newTodos}); + const newTodos = updateItemInArray(state.todos, action.id, todo => { + return updateObject(todo, { text: action.text }) + }) + + return updateObject(state, { todos: newTodos }) } function appReducer(state = initialState, action) { - switch(action.type) { - case 'SET_VISIBILITY_FILTER' : return setVisibilityFilter(state, action); - case 'ADD_TODO' : return addTodo(state, action); - case 'TOGGLE_TODO' : return toggleTodo(state, action); - case 'EDIT_TODO' : return editTodo(state, action); - default : return state; - } + switch (action.type) { + case 'SET_VISIBILITY_FILTER': + return setVisibilityFilter(state, action) + case 'ADD_TODO': + return addTodo(state, action) + case 'TOGGLE_TODO': + return toggleTodo(state, action) + case 'EDIT_TODO': + return editTodo(state, action) + default: + return state + } } ``` @@ -190,61 +194,64 @@ Our app reducer is still aware of all the different cases for our application. function updateObject(oldObject, newValues) {} function updateItemInArray(array, itemId, updateItemCallback) {} - - function setVisibilityFilter(visibilityState, action) { - // Technically, we don't even care about the previous state - return action.filter; + // Technically, we don't even care about the previous state + return action.filter } function visibilityReducer(visibilityState = 'SHOW_ALL', action) { - switch(action.type) { - case 'SET_VISIBILITY_FILTER' : return setVisibilityFilter(visibilityState, action); - default : return visibilityState; - } -}; - + switch (action.type) { + case 'SET_VISIBILITY_FILTER': + return setVisibilityFilter(visibilityState, action) + default: + return visibilityState + } +} function addTodo(todosState, action) { - const newTodos = todosState.concat({ - id: action.id, - text: action.text, - completed: false - }); - - return newTodos; + const newTodos = todosState.concat({ + id: action.id, + text: action.text, + completed: false + }) + + return newTodos } function toggleTodo(todosState, action) { - const newTodos = updateItemInArray(todosState, action.id, todo => { - return updateObject(todo, {completed : !todo.completed}); - }); - - return newTodos; + const newTodos = updateItemInArray(todosState, action.id, todo => { + return updateObject(todo, { completed: !todo.completed }) + }) + + return newTodos } function editTodo(todosState, action) { - const newTodos = updateItemInArray(todosState, action.id, todo => { - return updateObject(todo, {text : action.text}); - }); - - return newTodos; + const newTodos = updateItemInArray(todosState, action.id, todo => { + return updateObject(todo, { text: action.text }) + }) + + return newTodos } function todosReducer(todosState = [], action) { - switch(action.type) { - case 'ADD_TODO' : return addTodo(todosState, action); - case 'TOGGLE_TODO' : return toggleTodo(todosState, action); - case 'EDIT_TODO' : return editTodo(todosState, action); - default : return todosState; - } + switch (action.type) { + case 'ADD_TODO': + return addTodo(todosState, action) + case 'TOGGLE_TODO': + return toggleTodo(todosState, action) + case 'EDIT_TODO': + return editTodo(todosState, action) + default: + return todosState + } } function appReducer(state = initialState, action) { - return { - todos : todosReducer(state.todos, action), - visibilityFilter : visibilityReducer(state.visibilityFilter, action) - }; + return { + todos: todosReducer(state.todos, action), + visibilityFilter: visibilityReducer(state.visibilityFilter, action) + } } ``` @@ -269,13 +276,12 @@ function createReducer(initialState, handlers) { } } - // Omitted function setVisibilityFilter(visibilityState, action) {} const visibilityReducer = createReducer('SHOW_ALL', { - 'SET_VISIBILITY_FILTER' : setVisibilityFilter -}); + SET_VISIBILITY_FILTER: setVisibilityFilter +}) // Omitted function addTodo(todosState, action) {} @@ -283,16 +289,16 @@ function toggleTodo(todosState, action) {} function editTodo(todosState, action) {} const todosReducer = createReducer([], { - 'ADD_TODO' : addTodo, - 'TOGGLE_TODO' : toggleTodo, - 'EDIT_TODO' : editTodo -}); + ADD_TODO: addTodo, + TOGGLE_TODO: toggleTodo, + EDIT_TODO: editTodo +}) function appReducer(state = initialState, action) { - return { - todos : todosReducer(state.todos, action), - visibilityFilter : visibilityReducer(state.visibilityFilter, action) - }; + return { + todos: todosReducer(state.todos, action), + visibilityFilter: visibilityReducer(state.visibilityFilter, action) + } } ``` @@ -304,24 +310,24 @@ As our last step, we can now use Redux's built-in `combineReducers` utility to h // Reusable utility functions function updateObject(oldObject, newValues) { - // Encapsulate the idea of passing a new object as the first parameter - // to Object.assign to ensure we correctly copy data instead of mutating - return Object.assign({}, oldObject, newValues); + // Encapsulate the idea of passing a new object as the first parameter + // to Object.assign to ensure we correctly copy data instead of mutating + return Object.assign({}, oldObject, newValues) } function updateItemInArray(array, itemId, updateItemCallback) { - const updatedItems = array.map(item => { - if(item.id !== itemId) { - // Since we only want to update one item, preserve all others as they are now - return item; - } - - // Use the provided callback to create an updated item - const updatedItem = updateItemCallback(item); - return updatedItem; - }); - - return updatedItems; + const updatedItems = array.map(item => { + if (item.id !== itemId) { + // Since we only want to update one item, preserve all others as they are now + return item + } + + // Use the provided callback to create an updated item + const updatedItem = updateItemCallback(item) + return updatedItem + }) + + return updatedItems } function createReducer(initialState, handlers) { @@ -334,59 +340,58 @@ function createReducer(initialState, handlers) { } } - // Handler for a specific case ("case reducer") function setVisibilityFilter(visibilityState, action) { - // Technically, we don't even care about the previous state - return action.filter; + // Technically, we don't even care about the previous state + return action.filter } // Handler for an entire slice of state ("slice reducer") const visibilityReducer = createReducer('SHOW_ALL', { - 'SET_VISIBILITY_FILTER' : setVisibilityFilter -}); + SET_VISIBILITY_FILTER: setVisibilityFilter +}) // Case reducer function addTodo(todosState, action) { - const newTodos = todosState.concat({ - id: action.id, - text: action.text, - completed: false - }); - - return newTodos; + const newTodos = todosState.concat({ + id: action.id, + text: action.text, + completed: false + }) + + return newTodos } // Case reducer function toggleTodo(todosState, action) { - const newTodos = updateItemInArray(todosState, action.id, todo => { - return updateObject(todo, {completed : !todo.completed}); - }); - - return newTodos; + const newTodos = updateItemInArray(todosState, action.id, todo => { + return updateObject(todo, { completed: !todo.completed }) + }) + + return newTodos } // Case reducer function editTodo(todosState, action) { - const newTodos = updateItemInArray(todosState, action.id, todo => { - return updateObject(todo, {text : action.text}); - }); - - return newTodos; + const newTodos = updateItemInArray(todosState, action.id, todo => { + return updateObject(todo, { text: action.text }) + }) + + return newTodos } // Slice reducer const todosReducer = createReducer([], { - 'ADD_TODO' : addTodo, - 'TOGGLE_TODO' : toggleTodo, - 'EDIT_TODO' : editTodo -}); + ADD_TODO: addTodo, + TOGGLE_TODO: toggleTodo, + EDIT_TODO: editTodo +}) // "Root reducer" const appReducer = combineReducers({ - visibilityFilter : visibilityReducer, - todos : todosReducer -}); + visibilityFilter: visibilityReducer, + todos: todosReducer +}) ``` We now have examples of several kinds of split-up reducer functions: helper utilities like `updateObject` and `createReducer`, handlers for specific cases like `setVisibilityFilter` and `addTodo`, and slice-of-state handlers like `visibilityReducer` and `todosReducer`. We also can see that `appReducer` is an example of a "root reducer". diff --git a/docs/recipes/reducers/ReusingReducerLogic.md b/docs/recipes/reducers/ReusingReducerLogic.md index f992c864c5..7d67665df0 100644 --- a/docs/recipes/reducers/ReusingReducerLogic.md +++ b/docs/recipes/reducers/ReusingReducerLogic.md @@ -6,21 +6,21 @@ As an example, let's say that we want to track multiple counters in our applicat ```js function counter(state = 0, action) { - switch (action.type) { - case 'INCREMENT': - return state + 1; - case 'DECREMENT': - return state - 1; - default: - return state; - } + switch (action.type) { + case 'INCREMENT': + return state + 1 + case 'DECREMENT': + return state - 1 + default: + return state + } } const rootReducer = combineReducers({ - counterA : counter, - counterB : counter, - counterC : counter -}); + counterA: counter, + counterB: counter, + counterC: counter +}) ``` Unfortunately, this setup has a problem. Because `combineReducers` will call each slice reducer with the same action, dispatching `{type : 'INCREMENT'}` will actually cause _all three_ counter values to be incremented, not just one of them. We need some way to wrap the `counter` logic so that we can ensure that only the counter we care about is updated. @@ -34,32 +34,32 @@ The two most common ways to specialize a reducer are to generate new action cons ```js function createCounterWithNamedType(counterName = '') { - return function counter(state = 0, action) { - switch (action.type) { - case `INCREMENT_${counterName}`: - return state + 1; - case `DECREMENT_${counterName}`: - return state - 1; - default: - return state; - } + return function counter(state = 0, action) { + switch (action.type) { + case `INCREMENT_${counterName}`: + return state + 1 + case `DECREMENT_${counterName}`: + return state - 1 + default: + return state } + } } function createCounterWithNameData(counterName = '') { - return function counter(state = 0, action) { - const {name} = action; - if(name !== counterName) return state; - - switch (action.type) { - case `INCREMENT`: - return state + 1; - case `DECREMENT`: - return state - 1; - default: - return state; - } + return function counter(state = 0, action) { + const { name } = action + if (name !== counterName) return state + + switch (action.type) { + case `INCREMENT`: + return state + 1 + case `DECREMENT`: + return state - 1 + default: + return state } + } } ``` @@ -67,13 +67,13 @@ We should now be able to use either of these to generate our specialized counter ```js const rootReducer = combineReducers({ - counterA : createCounterWithNamedType('A'), - counterB : createCounterWithNamedType('B'), - counterC : createCounterWithNamedType('C'), -}); + counterA: createCounterWithNamedType('A'), + counterB: createCounterWithNamedType('B'), + counterC: createCounterWithNamedType('C') +}) -store.dispatch({type : 'INCREMENT_B'}); -console.log(store.getState()); +store.dispatch({ type: 'INCREMENT_B' }) +console.log(store.getState()) // {counterA : 0, counterB : 1, counterC : 0} ``` @@ -82,52 +82,58 @@ We could also vary the approach somewhat, and create a more generic higher-order ```js function counter(state = 0, action) { - switch (action.type) { - case 'INCREMENT': - return state + 1; - case 'DECREMENT': - return state - 1; - default: - return state; - } + switch (action.type) { + case 'INCREMENT': + return state + 1 + case 'DECREMENT': + return state - 1 + default: + return state + } } function createNamedWrapperReducer(reducerFunction, reducerName) { - return (state, action) => { - const {name} = action; - const isInitializationCall = state === undefined; - if(name !== reducerName && !isInitializationCall) return state; - - return reducerFunction(state, action); - } + return (state, action) => { + const { name } = action + const isInitializationCall = state === undefined + if (name !== reducerName && !isInitializationCall) return state + + return reducerFunction(state, action) + } } const rootReducer = combineReducers({ - counterA : createNamedWrapperReducer(counter, 'A'), - counterB : createNamedWrapperReducer(counter, 'B'), - counterC : createNamedWrapperReducer(counter, 'C'), -}); + counterA: createNamedWrapperReducer(counter, 'A'), + counterB: createNamedWrapperReducer(counter, 'B'), + counterC: createNamedWrapperReducer(counter, 'C') +}) ``` You could even go as far as to make a generic filtering higher-order reducer: ```js function createFilteredReducer(reducerFunction, reducerPredicate) { - return (state, action) => { - const isInitializationCall = state === undefined; - const shouldRunWrappedReducer = reducerPredicate(action) || isInitializationCall; - return shouldRunWrappedReducer ? reducerFunction(state, action) : state; - } + return (state, action) => { + const isInitializationCall = state === undefined + const shouldRunWrappedReducer = + reducerPredicate(action) || isInitializationCall + return shouldRunWrappedReducer ? reducerFunction(state, action) : state + } } const rootReducer = combineReducers({ - // check for suffixed strings - counterA : createFilteredReducer(counter, action => action.type.endsWith('_A')), - // check for extra data in the action - counterB : createFilteredReducer(counter, action => action.name === 'B'), - // respond to all 'INCREMENT' actions, but never 'DECREMENT' - counterC : createFilteredReducer(counter, action => action.type === 'INCREMENT') -}; + // check for suffixed strings + counterA: createFilteredReducer(counter, action => + action.type.endsWith('_A') + ), + // check for extra data in the action + counterB: createFilteredReducer(counter, action => action.name === 'B'), + // respond to all 'INCREMENT' actions, but never 'DECREMENT' + counterC: createFilteredReducer( + counter, + action => action.type === 'INCREMENT' + ) +}) ``` diff --git a/docs/recipes/reducers/UpdatingNormalizedData.md b/docs/recipes/reducers/UpdatingNormalizedData.md index 608043dc14..28cd29d2eb 100644 --- a/docs/recipes/reducers/UpdatingNormalizedData.md +++ b/docs/recipes/reducers/UpdatingNormalizedData.md @@ -9,17 +9,17 @@ As mentioned in [Normalizing State Shape](./NormalizingStateShape.md), the Norma One approach is to merge the contents of the action in to the existing state. In this case, we need to do a deep recursive merge, not just a shallow copy. The Lodash `merge` function can handle this for us: ```js -import merge from "lodash/merge"; +import merge from 'lodash/merge' function commentsById(state = {}, action) { - switch(action.type) { - default : { - if(action.entities && action.entities.comments) { - return merge({}, state, action.entities.comments.byId); - } - return state; - } + switch (action.type) { + default: { + if (action.entities && action.entities.comments) { + return merge({}, state, action.entities.comments.byId) + } + return state } + } } ``` @@ -33,96 +33,99 @@ If we have a nested tree of slice reducers, each slice reducer will need to know ```js // actions.js function addComment(postId, commentText) { - // Generate a unique ID for this comment - const commentId = generateId("comment"); - - return { - type : "ADD_COMMENT", - payload : { - postId, - commentId, - commentText - } - }; + // Generate a unique ID for this comment + const commentId = generateId('comment') + + return { + type: 'ADD_COMMENT', + payload: { + postId, + commentId, + commentText + } + } } - // reducers/posts.js function addComment(state, action) { - const {payload} = action; - const {postId, commentId} = payload; - - // Look up the correct post, to simplify the rest of the code - const post = state[postId]; - - return { - ...state, - // Update our Post object with a new "comments" array - [postId] : { - ...post, - comments : post.comments.concat(commentId) - } - }; + const { payload } = action + const { postId, commentId } = payload + + // Look up the correct post, to simplify the rest of the code + const post = state[postId] + + return { + ...state, + // Update our Post object with a new "comments" array + [postId]: { + ...post, + comments: post.comments.concat(commentId) + } + } } function postsById(state = {}, action) { - switch(action.type) { - case "ADD_COMMENT" : return addComment(state, action); - default : return state; - } + switch (action.type) { + case 'ADD_COMMENT': + return addComment(state, action) + default: + return state + } } function allPosts(state = [], action) { - // omitted - no work to be done for this example + // omitted - no work to be done for this example } const postsReducer = combineReducers({ - byId : postsById, - allIds : allPosts -}); - + byId: postsById, + allIds: allPosts +}) // reducers/comments.js function addCommentEntry(state, action) { - const {payload} = action; - const {commentId, commentText} = payload; - - // Create our new Comment object - const comment = {id : commentId, text : commentText}; - - // Insert the new Comment object into the updated lookup table - return { - ...state, - [commentId] : comment - }; + const { payload } = action + const { commentId, commentText } = payload + + // Create our new Comment object + const comment = { id: commentId, text: commentText } + + // Insert the new Comment object into the updated lookup table + return { + ...state, + [commentId]: comment + } } function commentsById(state = {}, action) { - switch(action.type) { - case "ADD_COMMENT" : return addCommentEntry(state, action); - default : return state; - } + switch (action.type) { + case 'ADD_COMMENT': + return addCommentEntry(state, action) + default: + return state + } } - function addCommentId(state, action) { - const {payload} = action; - const {commentId} = payload; - // Just append the new Comment's ID to the list of all IDs - return state.concat(commentId); + const { payload } = action + const { commentId } = payload + // Just append the new Comment's ID to the list of all IDs + return state.concat(commentId) } function allComments(state = [], action) { - switch(action.type) { - case "ADD_COMMENT" : return addCommentId(state, action); - default : return state; - } + switch (action.type) { + case 'ADD_COMMENT': + return addCommentId(state, action) + default: + return state + } } const commentsReducer = combineReducers({ - byId : commentsById, - allIds : allComments -}); + byId: commentsById, + allIds: allComments +}) ``` The example is a bit long, because it's showing how all the different slice reducers and case reducers fit together. Note the delegation involved here. The `postsById` slice reducer delegates the work for this case to `addComment`, which inserts the new Comment's ID into the correct Post item. Meanwhile, both the `commentsById` and `allComments` slice reducers have their own case reducers, which update the Comments lookup table and list of all Comment IDs appropriately. @@ -134,52 +137,51 @@ The example is a bit long, because it's showing how all the different slice redu Since reducers are just functions, there's an infinite number of ways to split up this logic. While using slice reducers is obviously the most common, it's also possible to organize behavior in a more task-oriented structure. Because this will often involve more nested updates, you may want to use an immutable update utility library like [dot-prop-immutable](https://github.com/debitoor/dot-prop-immutable) or [object-path-immutable](https://github.com/mariocasciaro/object-path-immutable) to simplify the update statements. Here's an example of what that might look like: ```js -import posts from "./postsReducer"; -import comments from "./commentsReducer"; -import dotProp from "dot-prop-immutable"; -import {combineReducers} from "redux"; -import reduceReducers from "reduce-reducers"; +import posts from './postsReducer' +import comments from './commentsReducer' +import dotProp from 'dot-prop-immutable' +import { combineReducers } from 'redux' +import reduceReducers from 'reduce-reducers' const combinedReducer = combineReducers({ - posts, - comments -}); - + posts, + comments +}) function addComment(state, action) { - const {payload} = action; - const {postId, commentId, commentText} = payload; - - // State here is the entire combined state - const updatedWithPostState = dotProp.set( - state, - `posts.byId.${postId}.comments`, - comments => comments.concat(commentId) - ); - - const updatedWithCommentsTable = dotProp.set( - updatedWithPostState, - `comments.byId.${commentId}`, - {id : commentId, text : commentText} - ); - - const updatedWithCommentsList = dotProp.set( - updatedWithCommentsTable, - `comments.allIds`, - allIds => allIds.concat(commentId); - ); - - return updatedWithCommentsList; + const { payload } = action + const { postId, commentId, commentText } = payload + + // State here is the entire combined state + const updatedWithPostState = dotProp.set( + state, + `posts.byId.${postId}.comments`, + comments => comments.concat(commentId) + ) + + const updatedWithCommentsTable = dotProp.set( + updatedWithPostState, + `comments.byId.${commentId}`, + { id: commentId, text: commentText } + ) + + const updatedWithCommentsList = dotProp.set( + updatedWithCommentsTable, + `comments.allIds`, + allIds => allIds.concat(commentId) + ) + + return updatedWithCommentsList } -const featureReducers = createReducer({}, { - ADD_COMMENT : addComment, -}; +const featureReducers = createReducer( + {}, + { + ADD_COMMENT: addComment + } +) -const rootReducer = reduceReducers( - combinedReducer, - featureReducers -); +const rootReducer = reduceReducers(combinedReducer, featureReducers) ``` This approach makes it very clear what's happening for the `"ADD_COMMENTS"` case, but it does require nested updating logic, and some specific knowledge of the state tree shape. Depending on how you want to compose your reducer logic, this may or may not be desired. @@ -192,95 +194,94 @@ There's a couple ways Redux-ORM can be used to perform updates. First, the Redu ```js // models.js -import {Model, many, Schema} from "redux-orm"; +import { Model, many, Schema } from 'redux-orm' export class Post extends Model { static get fields() { return { - // Define a many-sided relation - one Post can have many Comments, + // Define a many-sided relation - one Post can have many Comments, // at a field named "comments" - comments : many("Comment") - }; + comments: many('Comment') + } } - + static reducer(state, action, Post) { - switch(action.type) { - case "CREATE_POST" : { + switch (action.type) { + case 'CREATE_POST': { // Queue up the creation of a Post instance - Post.create(action.payload); - break; + Post.create(action.payload) + break } - case "ADD_COMMENT" : { - const {payload} = action; - const {postId, commentId} = payload; - // Queue up the addition of a relation between this Comment ID + case 'ADD_COMMENT': { + const { payload } = action + const { postId, commentId } = payload + // Queue up the addition of a relation between this Comment ID // and this Post instance - Post.withId(postId).comments.add(commentId); - break; + Post.withId(postId).comments.add(commentId) + break } } - + // Redux-ORM will automatically apply queued updates after this returns } } -Post.modelName = "Post"; +Post.modelName = 'Post' export class Comment extends Model { static get fields() { - return {}; + return {} } - + static reducer(state, action, Comment) { - switch(action.type) { - case "ADD_COMMENT" : { - const {payload} = action; - const {commentId, commentText} = payload; - + switch (action.type) { + case 'ADD_COMMENT': { + const { payload } = action + const { commentId, commentText } = payload + // Queue up the creation of a Comment instance - Comment.create({id : commentId, text : commentText}); - break; - } + Comment.create({ id: commentId, text: commentText }) + break + } } - + // Redux-ORM will automatically apply queued updates after this returns } } -Comment.modelName = "Comment"; +Comment.modelName = 'Comment' // Create a Schema instance, and hook up the Post and Comment models -export const schema = new Schema(); -schema.register(Post, Comment); - +export const schema = new Schema() +schema.register(Post, Comment) // main.js import { createStore, combineReducers } from 'redux' -import {schema} from "./models"; +import { schema } from './models' const rootReducer = combineReducers({ // Insert the auto-generated Redux-ORM reducer. This will // initialize our model "tables", and hook up the reducer // logic we defined on each Model subclass - entities : schema.reducer() -}); + entities: schema.reducer() +}) // Dispatch an action to create a Post instance store.dispatch({ - type : "CREATE_POST", - payload : { - id : 1, - name : "Test Post Please Ignore" + type: 'CREATE_POST', + payload: { + id: 1, + name: 'Test Post Please Ignore' } -}); +}) // Dispatch an action to create a Comment instance as a child of that Post store.dispatch({ - type : "ADD_COMMENT", - payload : { - postId : 1, - commentId : 123, - commentText : "This is a comment" + type: 'ADD_COMMENT', + payload: { + postId: 1, + commentId: 123, + commentText: 'This is a comment' } -}); +}) ``` The Redux-ORM library maintains an internal queue of updates to be applied. Those updates are then applied immutably, simplifying the update process. @@ -288,22 +289,22 @@ The Redux-ORM library maintains an internal queue of updates to be applied. Tho Another variation on this is to use Redux-ORM as an abstraction layer within a single case reducer: ```js -import {schema} from "./models"; +import { schema } from './models' -// Assume this case reducer is being used in our "entities" slice reducer, +// Assume this case reducer is being used in our "entities" slice reducer, // and we do not have reducers defined on our Redux-ORM Model subclasses function addComment(entitiesState, action) { - const session = schema.from(entitiesState); - const {Post, Comment} = session; - const {payload} = action; - const {postId, commentId, commentText} = payload; - - const post = Post.withId(postId); - post.comments.add(commentId); - - Comment.create({id : commentId, text : commentText}); - - return session.reduce(); + const session = schema.from(entitiesState) + const { Post, Comment } = session + const { payload } = action + const { postId, commentId, commentText } = payload + + const post = Post.withId(postId) + post.comments.add(commentId) + + Comment.create({ id: commentId, text: commentText }) + + return session.reduce() } ``` diff --git a/docs/recipes/reducers/UsingCombineReducers.md b/docs/recipes/reducers/UsingCombineReducers.md index 7a07f200df..f2c3063ee4 100644 --- a/docs/recipes/reducers/UsingCombineReducers.md +++ b/docs/recipes/reducers/UsingCombineReducers.md @@ -26,27 +26,29 @@ Here's an example of how use of ES6 object literal shorthand with `combineReduce ```js // reducers.js -export default theDefaultReducer = (state = 0, action) => state; +export default (theDefaultReducer = (state = 0, action) => state) -export const firstNamedReducer = (state = 1, action) => state; - -export const secondNamedReducer = (state = 2, action) => state; +export const firstNamedReducer = (state = 1, action) => state +export const secondNamedReducer = (state = 2, action) => state // rootReducer.js -import {combineReducers, createStore} from "redux"; +import { combineReducers, createStore } from 'redux' -import theDefaultReducer, {firstNamedReducer, secondNamedReducer} from "./reducers"; +import theDefaultReducer, { + firstNamedReducer, + secondNamedReducer +} from './reducers' // Use ES6 object literal shorthand syntax to define the object shape const rootReducer = combineReducers({ - theDefaultReducer, - firstNamedReducer, - secondNamedReducer -}); + theDefaultReducer, + firstNamedReducer, + secondNamedReducer +}) -const store = createStore(rootReducer); -console.log(store.getState()); +const store = createStore(rootReducer) +console.log(store.getState()) // {theDefaultReducer : 0, firstNamedReducer : 1, secondNamedReducer : 2} ``` @@ -57,19 +59,22 @@ Also, the resulting names are a bit odd. It's generally not a good practice to A better usage might look like: ```js -import {combineReducers, createStore} from "redux"; +import { combineReducers, createStore } from 'redux' // Rename the default import to whatever name we want. We can also rename a named import. -import defaultState, {firstNamedReducer, secondNamedReducer as secondState} from "./reducers"; +import defaultState, { + firstNamedReducer, + secondNamedReducer as secondState +} from './reducers' const rootReducer = combineReducers({ - defaultState, // key name same as the carefully renamed default export - firstState : firstNamedReducer, // specific key name instead of the variable name - secondState, // key name same as the carefully renamed named export -}); + defaultState, // key name same as the carefully renamed default export + firstState: firstNamedReducer, // specific key name instead of the variable name + secondState // key name same as the carefully renamed named export +}) -const reducerInitializedStore = createStore(rootReducer); -console.log(reducerInitializedStore.getState()); +const reducerInitializedStore = createStore(rootReducer) +console.log(reducerInitializedStore.getState()) // {defaultState : 0, firstState : 1, secondState : 2} ```