Skip to content

Commit

Permalink
Docs/extracting data with mapstate (#1036)
Browse files Browse the repository at this point in the history
* Fix typo and remove unnecessary comments from `GettingStarted.md`

* Rename getting started to kebab

* Add mapState piece

* Restructure the mapState page content

* Tweak sidebar title and add memoization note
  • Loading branch information
wgao19 authored and markerikson committed Oct 7, 2018
1 parent 588c565 commit f930a76
Show file tree
Hide file tree
Showing 4 changed files with 261 additions and 11 deletions.
6 changes: 3 additions & 3 deletions docs/README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# Table of Contents



- [Getting Started: adding React-Redux to a React todo app](./GettingStarted.md)
- [Getting Started: adding React-Redux to a React todo app](./getting-started.md)
- Using React-Redux
- [Connect: Extracting Data with `mapStateToProps`](./connect-extracting-data-with-mapStateToProps.md)
- [API](api.md#api)
- [`<Provider store>`](api.md#provider-store)
- [`connect([mapStateToProps], [mapDispatchToProps], [mergeProps], [options])`](api.md#connectmapstatetoprops-mapdispatchtoprops-mergeprops-options)
Expand Down
254 changes: 254 additions & 0 deletions docs/connect-extracting-data-with-mapStateToProps.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,254 @@
---
id: connect-extracting-data-with-mapStateToProps
title: Connect: Extracting Data with mapStateToProps
hide_title: true
sidebar_label: Connect: Extracting Data with mapStateToProps
---

# Connect: Extracting Data with `mapStateToProps`
As the first argument passed in to `connect`, `mapStateToProps` is used for selecting the part of the data from the store that the connected component needs. It’s frequently referred to as just `mapState` for short.

- It is called every time the store state changes.
- It receives the entire store state, and should return an object of data this component needs.


## Defining `mapStateToProps`

`mapStateToProps` should be defined as a function:

```js
function mapStateToProps(state, ownProps?)
```
It should take a first argument called `state`, optionally a second argument called `ownProps`, and return a plain object containing the data that the connected component needs.
This function should be passed as the first argument to `connect`, and will be will be called every time when the Redux store state changes. If you do not wish to subscribe to the store, pass `null` or `undefined` to `connect` in place of `mapStateToProps`.
**It does not matter if a `mapStateToProps` function is written using the `function` keyword (`function mapState(state) { }` ) or as an arrow function (`const mapState = (state) => { }` )** - it will work the same either way.
### Arguments
#### `state`
The first argument to a `mapStateToProps` function is the entire Redux store state (the same value returned by a call to `store.getState()`). Because of this, the first argument is traditionally just called `state`. (While you can give the argument any name you want, calling it `store` would be incorrect - it's the "state value", not the "store instance".)
The `mapStateToProps` function should always be written with at least `state` passed in.
```js
// TodoList.js

function mapStateToProps(state) {
const { todos } = state;
return { todoList: todos.allIds };
};

export default connect(mapStateToProps)(TodoList);
```
#### `ownProps` (optional)
You may define the function with a second argument, `ownProps`, if your component needs the data from its own props to retrieve data from the store. This argument will contain all of the props given to the wrapper component that was generated by `connect`.
```js
// Todo.js

function mapStateToProps(state, ownProps) {
const { visibilityFilter } = state;
const { id } = ownProps;
const todo = getTodoById(state, id);

// component receives additionally:
return { todo, visibilityFilter };
};

// Later, in your application, a parent component renders:
<ConnectedTodo id=123} />
// and your component receives props.id, props.todo, and props.visibilityFilter
```
You do not need to include values from `ownProps` in the object returned from `mapStateToProps`. `connect` will automatically merge those different prop sources into a final set of props.
### Return
Your `mapStateToProps` function should return a plain object that contains the data the component needs:
- Each field in the object will become a prop for your actual component
- The values in the fields will be used to determine if your component needs to re-render
For example:
```js
function mapStateToProps(state) {
return {
a : 42,
todos : state.todos,
filter : state.visibilityFilter
}
}

// component will receive: props.a, props.todos, and props.filter
```
> Note: In advanced scenarios where you need more control over the rendering performance, `mapStateToProps` can also return a function. In this case, that function will be used as the final `mapStateToProps` for a particular component instance. This allows you to do per-instance memoization. See the [Advanced Usage]() section of the docs for more details, as well as [PR #279](https://github.com/reduxjs/react-redux/pull/279) and the tests it adds. Most apps never need this.
## Usage Guidelines
### Let `mapStateToProps` Reshape the Data from the Store
`mapStateToProps` functions can, and should, do a lot more than just `return state.someSlice`. **They have the responsibility of "re-shaping" store data as needed for that component.** This may include returning a value as a specific prop name, combining pieces of data from different parts of the state tree, and transforming the store data in different ways.
### Use Selector Functions to Extract and Transform Data
We highly encourage the use of "selector" functions to help encapsulate the process of extracting values from specific locations in the state tree. Memoized selector functions also play a key role in improving application performance (see the following sections in this page and the [Advanced Usage: Performance]() page for more details on why and how to use selectors.)
### `mapStateToProps` Functions Should Be Fast
Whenever the store changes, all of the `mapStateToProps` functions of all of the connected components will run. Because of this, your `mapStateToProps` functions should run as fast as possible. This also means that a slow `mapStateToProps` function can be a potential bottleneck for your application.
As part of the "re-shaping data" idea, `mapStateToProps` functions frequently need to transform data in various ways (such as filtering an array, mapping an array of IDs to their corresponding objects, or extracting plain JS values from Immutable.js objects). These transformations can often be expensive, both in terms of cost to execute the transformation, and whether the component re-renders as a result. If performance is a concern, ensure that these transformations are only run if the input values have changed.
### `mapStateToProps` Functions Should Be Pure and Synchronous
Much like a Redux reducer, a `mapStateToProps` function should always be 100% pure and synchronous. It should simply take `state` (and `ownProps`) as arguments, and return the data the component needs as props. It should _not_ be used to trigger asynchronous behavior like AJAX calls for data fetching, and the functions should not be declared as `async`.
## `mapStateToProps` and Performance
### Return Values Determine If Your Component Re-Renders
React-Redux internally implements the `shouldComponentUpdate` method such that the wrapper component re-renders precisely when the data your component needs has changed. By default, React-Redux decides whether the contents of the object returned from `mapStateToProps` are different using `===` comparison (a "shallow equality" check) on each fields of the returned object. If any of the fields have changed, then your component will be re-rendered so it can receive the updated values as props. Note that returning a mutated object of the same reference is a common mistake that can result in your component not re-rendering when expected.
To summarize the behavior of the component wrapped by `connect` with `mapStateToProps` to extract data from the store:
| | `(state) => stateProps` | `(state, ownProps) => stateProps` |
| ---------------------------- | -------------------------------------- | -------------------------------------------------------------------------------------------- |
| `mapStateToProps` runs when: | store `state` is `===` different | store `state` changes <br /> or <br />any field of `ownProps` is different |
| component re-renders when: | any field of `stateProps` is different | any field of `stateProps` is different <br /> or <br /> any field of `ownProps` is different |
### Only Return New Object References If Needed
React-Redux does shallow comparisons to see if the `mapState` results have changed. It’s easy to accidentally return new object or array references every time, which would cause your component to re-render even if the data is actually the same.
Many common operations result in new object or array references being created:
- Creating new arrays with `someArray.map()` or `someArray.filter()`
- Merging arrays with `array.concat`
- Copying values with `Object.assign`
- Copying values with the spread operator `{ ...oldState, ...newData }`
Put these operations in [memoized selector functions]() to ensure that they only run if the input values have changed. This will also ensure that if the input values _haven't_ changed, `mapState` will still return the same result values as before, and `connect` can skip re-rendering.
### Only Perform Expensive Operations When Data Changes
Transforming data can often be expensive (_and_ usually results in new object references being created). In order for your `mapStateToProps` function to be as fast as possible, you should only re-run these complex transformations when the relevant data has changed.
There are a few ways to approach this:
- Some transformations could be calculated in an action creator or reducer, and the transformed data could be kept in the store
- Transformations can also be done in a component's `render()` method
- If the transformation does need to be done in a `mapStateToProps` function, then we recommend using [memoized selector functions]() to ensure the transformation is only run when the input values have changed.
#### Immutable.js Performance Concerns
Immutable.js author Lee Byron on Twitter [explicitly advises avoiding `toJS` when performance is a concern](https://twitter.com/leeb/status/746733697093668864?lang=en):
> Perf tip for #immutablejs: avoid .toJS() .toObject() and .toArray() all slow full-copy operations which render structural sharing useless.
There's several other performance concerns to take into consideration with Immutable.js - see the list of links at the end of this page for more information.
## Behavior and Gotchas
### `mapStateToProps` Will Not Run if the Store State is the Same
The wrapper component generated by `connect` subscribes to the Redux store. Every time an action is dispatched, it calls `store.getState()` and checks to see if `lastState === currentState`. If the two state values are identical by reference, then it will _not_ re-run your `mapStateToProps` function, because it assumes that the rest of the store state hasn't changed either.
The Redux `combineReducers` utility function tries to optimize for this. If none of the slice reducers returned a new value, then `combineReducers` returns the old state object instead of a new one. This means that mutation in a reducer can lead to the root state object not being updated, and thus the UI won't re-render.
### The Number of Declared Arguments Affects Behavior
With just `(state)`, the function runs whenever the root store state object is different. With `(state, ownProps)`, it runs any time the store state is different and ALSO whenever the wrapper props have changed.
This means that **you should not add the `ownProps` argument unless you actually need to use it**, or your `mapStateToProps` function will run more often than it needs to.
There are some edge cases around this behavior. **The number of mandatory arguments determines whether `mapStateToProps` will receive `ownProps`**.
If the formal definition of the function contains one mandatory parameter, `mapStateToProps` will _not_ receive `ownProps`:
```js
function mapStateToProps(state) {
console.log(state); // state
console.log(arguments[1]); // undefined
}
const mapStateToProps = (state, ownProps = {}) => {
console.log(state); // state
console.log(ownProps); // undefined
}
```
It _will_ receive `ownProps` when the formal definition of the function contains zero or two mandatory parameters:
```js
function mapStateToProps(state, ownProps) {
console.log(state); // state
console.log(ownProps); // ownProps
}

function mapStateToProps() {
console.log(arguments[0]); // state
console.log(arguments[1]); // ownProps
}

function mapStateToProps(...args) {
console.log(args[0]); // state
console.log(args[1]); // ownProps
}
```
<!--
## Next Up
- Connect: Dispatching Actions with `mapDispatchToProps`
- Further optimizing `mapStateToProps` performances by writing memoized selectors or using Reselect →
- Understanding whys and hows →
-->
## Links and References
**Tutorials**
- [Practical Redux Series, Part 6: Connected Lists, Forms, and Performance](https://blog.isquaredsoftware.com/2017/01/practical-redux-part-6-connected-lists-forms-and-performance/)
- [Idiomatic Redux: Using Reselect Selectors for Encapsulation and Performance](https://blog.isquaredsoftware.com/2017/12/idiomatic-redux-using-reselect-selectors/)
**Performance**
- [Lee Byron's Tweet Suggesting to avoid `toJS`, `toArray` and `toObject` for Performance](https://twitter.com/leeb/status/746733697093668864)
- [Improving React and Redux performance with Reselect](https://blog.rangle.io/react-and-redux-performance-with-reselect/)
- [Immutable data performance links](https://github.com/markerikson/react-redux-links/blob/master/react-performance.md#immutable-data)
**Q&A**
- [Why Is My Component Re-Rendering Too Often?](https://redux.js.org/faq/reactredux#why-is-my-component-re-rendering-too-often)
- [Why isn't my component re-rendering, or my mapStateToProps running](https://redux.js.org/faq/reactredux#why-isnt-my-component-re-rendering-or-my-mapstatetoprops-running)
- [How can I speed up my mapStateToProps?](https://redux.js.org/faq/reactredux#why-is-my-component-re-rendering-too-often)
- [Should I only connect my top component, or can I connect multiple components in my tree?](https://redux.js.org/faq/reactredux#why-is-my-component-re-rendering-too-often)
11 changes: 3 additions & 8 deletions docs/GettingStarted.md → docs/getting-started.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
---
id: getting-started
title: Getting started
title: Getting Started
hide_title: true
sidebar_label: Getting started
sidebar_label: Getting Started
---

# Getting Started
Expand Down Expand Up @@ -308,12 +308,8 @@ export const getTodoList = store =>
getTodosState(store) ? getTodosState(store).allIds : [];

export const getTodoById = (store, id) =>
getTodoState(store) ? { ...getTodosState(store).byIds[id], id } : {};
getTodosState(store) ? { ...getTodosState(store).byIds[id], id } : {};

/**
* example of a slightly more complex selector
* select from store combining information from multiple reducers
*/
export const getTodos = store =>
getTodoList(store).map(id => getTodoById(store, id));
```
Expand Down Expand Up @@ -462,7 +458,6 @@ Meanwhile, we also need to update our `<TodoList />` component to filter todos a
// redux/selectors.js

// ... other selectors

export const getTodosByVisibilityFilter = (store, visibilityFilter) => {
const allTodos = getTodos(store);
switch (visibilityFilter) {
Expand Down
1 change: 1 addition & 0 deletions website/sidebars.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"docs": {
"Introduction": ["getting-started"],
"Using React Redux": ["connect-extracting-data-with-mapStateToProps"],
"API Reference": ["api", "api/provider"],
"Guides": ["troubleshooting"]
}
Expand Down

0 comments on commit f930a76

Please sign in to comment.