MY QUESTION: Why doesn't updating a property of an object in an array in my Immutable state (Map) not cause Redux to update my component?
I'm trying to create a widget that uploads files to my server, and my initial state (from inside my UploaderReducer which you will see below) object looks like this:
let initState = Map({
files: List(),
displayMode: 'grid',
currentRequests: List()
});
I have a thunk method that starts uploads and dispatches actions when an event occurs (such as a progress update). For example, the onProgress event looks like this:
onProgress: (data) => {
dispatch(fileUploadProgressUpdated({
index,
progress: data.percentage
}));
}
I'm using redux-actions to create and handle my actions, so my reducer for that action looks like this:
export default UploaderReducer = handleActions({
// Other actions...
FILE_UPLOAD_PROGRESS_UPDATED: (state, { payload }) => (
updateFilePropsAtIndex(
state,
payload.index,
{
status: FILE_UPLOAD_PROGRESS_UPDATED,
progress: payload.progress
}
)
)
}, initState);
And updateFilePropsAtIndex looks like:
export function updateFilePropsAtIndex (state, index, fileProps) {
return state.updateIn(['files', index], file => {
try {
for (let prop in fileProps) {
if (fileProps.hasOwnProperty(prop)) {
if (Map.isMap(file)) {
file = file.set(prop, fileProps[prop]);
} else {
file[prop] = fileProps[prop];
}
}
}
} catch (e) {
console.error(e);
return file;
}
return file;
});
}
So far, this all seems to work fine! In Redux DevTools, it shows up as an action as expected. However, none of my components update! Adding new items to the files array re-renders my UI with the new files added, so Redux certainly doesn't have a problem with me doing that...
My top level component that connects to the store using connect looks like this:
const mapStateToProps = function (state) {
let uploadReducer = state.get('UploaderReducer');
let props = {
files: uploadReducer.get('files'),
displayMode: uploadReducer.get('displayMode'),
uploadsInProgress: uploadReducer.get('currentRequests').size > 0
};
return props;
};
class UploaderContainer extends Component {
constructor (props, context) {
super(props, context);
// Constructor things!
}
// Some events n stuff...
render(){
return (
<div>
<UploadWidget
//other props
files={this.props.files} />
</div>
);
}
}
export default connect(mapStateToProps, uploadActions)(UploaderContainer);
uploadActions is an object with actions created using redux-actions.
A file object in the files array is basically this:
{
name: '',
progress: 0,
status
}
The UploadWidget is basically a drag n drop div and a the files array printed out on the screen.
I tried using redux-immutablejs to help out as I've seen in many posts on GitHub, but I have no idea if it helps... This is my root reducer:
import { combineReducers } from 'redux-immutablejs';
import { routeReducer as router } from 'redux-simple-router';
import UploaderReducer from './modules/UploaderReducer';
export default combineReducers({
UploaderReducer,
router
});
My app entry point looks like this:
const store = configureStore(Map({}));
syncReduxAndRouter(history, store, (state) => {
return state.get('router');
});
// Render the React application to the DOM
ReactDOM.render(
<Root history={history} routes={routes} store={store}/>,
document.getElementById('root')
);
Lastly, my <Root/> component looks like this:
import React, { PropTypes } from 'react';
import { Provider } from 'react-redux';
import { Router } from 'react-router';
export default class Root extends React.Component {
static propTypes = {
history: PropTypes.object.isRequired,
routes: PropTypes.element.isRequired,
store: PropTypes.object.isRequired
};
get content () {
return (
<Router history={this.props.history}>
{this.props.routes}
</Router>
);
}
//Prep devTools, etc...
render () {
return (
<Provider store={this.props.store}>
<div style={{ height: '100%' }}>
{this.content}
{this.devTools}
</div>
</Provider>
);
}
}
So, ultimately, if I try to update a 'progress' in the following state object, React/Redux does not update my components:
{
UploaderReducer: {
files: [{progress: 0}]
}
}
Why is this? I thought the whole idea of using Immutable.js was that it was easier to compare modified objects regardless of how deeply you update them?
It seems generally getting Immutable to work with Redux is not as simple as it seems:
How to use Immutable.js with redux?
https://github.com/reactjs/redux/issues/548
However, the touted benefits of using Immutable seem to be worth this battle and I'd LOVE to figure out what I'm doing wrong!
UPDATE April 10 2016
The selected answer told me what I was doing wrong and for the sake of completeness, my updateFilePropsAtIndex function now contains simply this:
return state.updateIn(['files', index], file =>
Object.assign({}, file, fileProps)
);
This works perfectly well! :)
Two general thoughts first:
Immutable.js is potentially useful, yes, but you can accomplish the same immutable handling of data without using it. There's a number of libraries out there that can help make immutable data updates easier to read, but still operate on plain objects and arrays. I have many of them listed on the Immutable Data page in my Redux-related libraries repo.
If a React component does not appear to be updating, it's almost always because a reducer is actually mutating data. The Redux FAQ has an answer on that topic, at http://redux.js.org/docs/FAQ.html#react-not-rerendering.
Now, given that you are using Immutable.js, I'll admit that mutation of data seems a bit unlikely. That said... the file[prop] = fileProps[prop] line in your reducer does seem awfully curious. What exactly are you expecting to be going on there? I'd take a good look at that part.
Actually, now that I look at it... I am almost 100% certain that you are mutating data. Your updater callback to state.updateIn(['files', index]) is returning the exact same file object you got as a parameter. Per the Immutable.js docs at https://facebook.github.io/immutable-js/docs/#/Map:
If the updater function returns the same value it was called with, then no change will occur. This is still true if notSetValue is provided.
So yeah. You're returning the same value you were given, your direct mutations to it are showing up in the DevTools because that object is still hanging around, but since you returned the same object Immutable.js isn't actually returning any modified objects further up the hierarchy. So, when Redux does a check on the top-level object, it sees nothing has changed, doesn't notify subscribers, and therefore your component's mapStateToProps never runs.
Clean up your reducer and return a new object from inside that updater, and it should all just work.
(A rather belated answer, but I just now saw the question, and it appears to still be open. Hopefully you actually got it fixed by now...)
Related
I was wondering why couldn't I get some of my components to work using ReactDnD and mapDispatchToProps.
I'm trying to drag and drop Services to Clients but I can't find my dispatch functions in props at my serviceSpec on the endDrag method.
Considering my mapDispatchToProps on my Service component:
const mapDispatchToProps = (dispatch) => ({
dragService: (service) => { dispatch(dragService(service)) },
dropService: (service, clientTarget) => { dispatch(dropService(service, clientTarget)) }
});
High-order functions to bond together DragSource + Service + State + Dispatch:
var reduxConnectedService = connect(mapStateToProps, mapDispatchToProps)(Service);
export default DragSource('service', serviceSpec, collect)(reduxConnectedService);
render() method:
render (){
const { isDragging, connectDragSource, service } = this.props;
return connectDragSource(
<a className="panel-block is-active">
<CategoryIcon category={service.category}/>
{service.name} | ${service.price}
</a>
)
}
The spec object used to implement the dragSource specification (here is the problem):
const serviceSpec = {
beginDrag(props) {
return props.service;
},
endDrag(props, monitor, component){
console.log(props);
}
}
The console.log at endDrag function just show my Service Object because is being returned on the beginDrag function:
{service: {…}}
But my plan was to dispatch the action dropService here on endDrag, but I couldn't. The documentation says that (http://react-dnd.github.io/react-dnd/docs/api/drag-source):
beginDrag(props, monitor, component): Required. When the dragging
starts, beginDrag is called. You must return a plain JavaScript object
describing the data being dragged. What you return is the only
information available to the drop targets about the drag source so
it's important to pick the minimal data they need to know. You may be
tempted to put a reference to the component into it, but you should
try very hard to avoid doing this because it couples the drag sources
and drop targets. It's a good idea to return something like { id:
props.id } from this method.
I don't believe that I should return the dropService(dispatch) function on the beginDrag definition. So after hours trying to make it work, I started to pass the dropService function as a prop directly through the parent component (ServiceList):
{this.props.filteredServices.map((service, index) => (
<Service service={service} key={service.name} dropService={this.props.dropService}/>
))}
Making this way I could dispatch the dropService action on the endDrag method like I wanted, the console.log can proves that:
{service: {…}, dropService: ƒ}
I could make it work but I can't understand why I couldn't get this to work using mapDispatchToProps. Is there any limitation while using React-DnD or am I making something wrong?
Any help will be appreciated, I cannot die with this doubt. Thank you in advance.
Your problem is with these two lines:
var reduxConnectedService = connect(mapStateToProps, mapDispatchToProps)(Service);
export default DragSource('service', serviceSpec, collect)(reduxConnectedService);
Note the order: you wrap Service into a Redux container. Then you wrap the Redux Container with the DragSource container. Thus, in the component tree, the drag container is the parent of the Redux container, which means it doesn't receive the Redux props from it.
To fix that, make the drag container the child of the Redux container. You can do so by simply swapping the DragSource() and connect() calls:
var dragService = DragSource('service', serviceSpec, collect)(Service);
var reduxConnectedService = connect(mapStateToProps, mapDispatchToProps)(dragService);
I am new to react-redux and I was surprised to see an example where a function, in this case being getVisiblieTodos, is called inside mapStateToProps. This function should be called in a reducer since it changes state? Is the code breaking "good form" for the sake of brevity? Is it okay to do this in general?
I am looking at code from this link
import { connect } from 'react-redux'
import { toggleTodo } from '../actions'
import TodoList from '../components/TodoList'
const getVisibleTodos = (todos, filter) => {
switch (filter) {
case 'SHOW_ALL':
return todos
case 'SHOW_COMPLETED':
return todos.filter(t => t.completed)
case 'SHOW_ACTIVE':
return todos.filter(t => !t.completed)
}
}
const mapStateToProps = (state) => {
return {
todos: getVisibleTodos(state.todos, state.visibilityFilter)
}
}
const mapDispatchToProps = (dispatch) => {
return {
onTodoClick: (id) => {
dispatch(toggleTodo(id))
}
}
}
const VisibleTodoList = connect(
mapStateToProps,
mapDispatchToProps
)(TodoList)
export default VisibleTodoList
In redux we want the store to hold the minimal data needed for the app. Everything that is derived from the base data, should be computed on the fly, to prevent cloning pieces of the store, and the need to recompute all derived data when something changes in the store.
Since the visible todos list is not part of the store, but computed using the list of todos, and the visibilityFilter, the getVisibleTodos() doesn't change the store's state. It produces the derived computed data from the those two properties.
A function that is used to get data from the store, and compute derived data is known as a selector. Using selectors, the derived data is not part of the store, and computed when needed. In addition, we can use memoized selectors, to save the computation overhead.
You may see getVisibleTodos as a reducer because it includes "switch .. case" block or/and because it has 2 arguments . However, it is not a rule.
A redux reducer ( by definition) changes store state according to dispatched action , and that's why it takes two arguments ( store state + dispatched action ) and it returns new state for the store without mutation.
getVisibleTodos here is a helper function which filter an array according to string (filter).
Also , filter is not a redux-action, it is just string that decides todos to be rendered.
I may agree with you it is something weird , and if we can see the whole application (reducers, actions,... ) we can decide if it is best practices or not .
todos in this component is a calculated property based on the state of the reducer, and it is not changing any state.
It's okay to transform properties comming from recuders that are used only by one component (they are called selectors). Imagine that you use todos in other components, you will not want to make changes in one component like filtering and seeing that in the other components. If this is the case, it's fine to do it.
Also, it is a good property of your reducer to store only the needed data. More state is more complexity in the app, and more overhead to calculate new states.
It seems to me that a function should do what its name says, nothing less, nothing more.
mapStateToProps() should just do that, ie "map", and should normally not call other functions.
I am new to React, Redux and JS overall. I want to know how can I chain actions in a component? On a listing screen, my application retrieves user's Geo Position then it fetching api url with current long and lat.
I made it with time out. But I see it's a risky way.
import {
aFetch,
aGetPosition,
} from '../../actions';
import { API_URL } from '../../config/ApiEndpoints';
class App extends Component {
componentDidMount() {
this.props.aGetPosition();
setTimeout(() => {
const { latitude, longitude } = this.props.position.data.coords;
const url = `${API_URL}=${latitude},${longitude}`;
this.props.aFetch(url);
}, 1000);
}
render(){...}
}
const mapStateToProps = (state) => {
return {
position: state.position,
items: state.items,
};
};
export default connect(mapStateToProps, { aFetch, aGetPosition })(App);
Is possible to make it using .then() or something equal?
I believe you need the method componentWillReceiveProps(). The docs provide a good explanation about it and other methods in the lifecycle of a component.
Firstly, you'll want to dispatch your redux actions. This is possible by using a mapDispatchToProps function, which will be the 2nd parameter in your connect function (instead of a plain object with your actions, which is what you have now). Docs on implementing redux within your React containers, including dispatching actions from your container: http://redux.js.org/docs/basics/UsageWithReact.html#implementing-container-components
To handle async actions and chaining actions, you'll likely want to use a middleware to help you, such as redux-thunk. Doc link: http://redux.js.org/docs/advanced/AsyncActions.html#async-action-creators (I recommend reading this whole page, but the link will go directly to the section for redux-thunk).
I'm trying to understand the connect method of react-redux, and the functions it takes as parameters. In particular mapStateToProps().
The way I understand it, the return value of mapStateToProps will be an object derived from state (as it lives in the store), whose keys will be passed to your target component (the component connect is applied to) as props.
This means that the state as consumed by your target component can have a wildly different structure from the state as it is stored on your store.
Q: Is this OK?
Q: Is this expected?
Q: Is this an anti-pattern?
Yes, it is correct. Its just a helper function to have a simpler way to access your state properties
Imagine you have a posts key in your App state.posts
state.posts //
/*
{
currentPostId: "",
isFetching: false,
allPosts: {}
}
*/
And component Posts
By default connect()(Posts) will make all state props available for the connected Component
const Posts = ({posts}) => (
<div>
{/* access posts.isFetching, access posts.allPosts */}
</div>
)
Now when you map the state.posts to your component it gets a bit nicer
const Posts = ({isFetching, allPosts}) => (
<div>
{/* access isFetching, allPosts directly */}
</div>
)
connect(
state => state.posts
)(Posts)
mapDispatchToProps
normally you have to write dispatch(anActionCreator())
with bindActionCreators you can do it also more easily like
connect(
state => state.posts,
dispatch => bindActionCreators({fetchPosts, deletePost}, dispatch)
)(Posts)
Now you can use it in your Component
const Posts = ({isFetching, allPosts, fetchPosts, deletePost }) => (
<div>
<button onClick={() => fetchPosts()} />Fetch posts</button>
{/* access isFetching, allPosts directly */}
</div>
)
Update on actionCreators..
An example of an actionCreator: deletePost
const deletePostAction = (id) => ({
action: 'DELETE_POST',
payload: { id },
})
So, bindActionCreators will just take your actions, wrap them into dispatch call. (I didn't read the source code of redux, but the implementation might look something like this:
const bindActionCreators = (actions, dispatch) => {
return Object.keys(actions).reduce(actionsMap, actionNameInProps => {
actionsMap[actionNameInProps] = (...args) => dispatch(actions[actionNameInProps].call(null, ...args))
return actionsMap;
}, {})
}
Q: Is this ok?
A: yes
Q: Is this expected?
Yes, this is expected (if you are using react-redux).
Q: Is this an anti-pattern?
A: No, this is not an anti-pattern.
It's called "connecting" your component or "making it smart". It's by design.
It allows you to decouple your component from your state an additional time which increases the modularity of your code. It also allows you to simplify your component state as a subset of your application state which, in fact, helps you comply with the Redux pattern.
Think about it this way: a store is supposed to contain the entire state of your application.
For large applications, this could contain dozens of properties nested many layers deep.
You don't want to haul all that around on each call (expensive).
Without mapStateToProps or some analog thereof, you would be tempted to carve up your state another way to improve performance/simplify.
You got the first part right:
Yes mapStateToProps has the Store state as an argument/param (provided by react-redux::connect) and its used to link the component with certain part of the store state.
By linking I mean the object returned by mapStateToProps will be provided at construction time as props and any subsequent change will be available through componentWillReceiveProps.
If you know the Observer design pattern it's exactly that or small variation of it.
An example would help make things clearer:
import React, {
Component,
} from 'react-native';
class ItemsContainer extends Component {
constructor(props) {
super(props);
this.state = {
items: props.items, //provided by connect#mapStateToProps
filteredItems: this.filterItems(props.items, props.filters),
};
}
componentWillReceiveProps(nextProps) {
this.setState({
filteredItems: this.filterItems(this.state.items, nextProps.filters),
});
}
filterItems = (items, filters) => { /* return filtered list */ }
render() {
return (
<View>
// display the filtered items
</View>
);
}
}
module.exports = connect(
//mapStateToProps,
(state) => ({
items: state.App.Items.List,
filters: state.App.Items.Filters,
//the State.App & state.App.Items.List/Filters are reducers used as an example.
})
// mapDispatchToProps, that's another subject
)(ItemsContainer);
There can be another react component called itemsFilters that handle the display and persisting the filter state into Redux Store state, the Demo component is "listening" or "subscribed" to Redux Store state filters so whenever filters store state changes (with the help of filtersComponent) react-redux detect that there was a change and notify or "publish" all the listening/subscribed components by sending the changes to their componentWillReceiveProps which in this example will trigger a refilter of the items and refresh the display due to the fact that react state has changed.
Let me know if the example is confusing or not clear enough to provide a better explanation.
As for: This means that the state as consumed by your target component can have a wildly different structure from the state as it is stored on your store.
I didn't get the question, but just know that the react state (this.setState) is totally different from the Redux Store state!
The react state is used to handle the redraw and behavior of the react component. The react state is contained to the component exclusively.
The Redux Store state is a combination of Redux reducers states, each is responsible of managing a small portion app logic. Those reducers attributes can be accessed with the help of react-redux::connect#mapStateToProps by any component! Which make the Redux store state accessible app wide while component state is exclusive to itself.
This react & redux example is based off Mohamed Mellouki's example.
But validates using prettify and linting rules. Note that we define our props
and dispatch methods using PropTypes so that our compiler doesn't scream at us.
This example also included some lines of code that had been missing in Mohamed's
example. To use connect you will need to import it from react-redux. This
example also binds the method filterItems this will prevent scope problems in
the component. This source code has been auto formatted using JavaScript Prettify.
import React, { Component } from 'react-native';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
class ItemsContainer extends Component {
constructor(props) {
super(props);
const { items, filters } = props;
this.state = {
items,
filteredItems: filterItems(items, filters),
};
this.filterItems = this.filterItems.bind(this);
}
componentWillReceiveProps(nextProps) {
const { itmes } = this.state;
const { filters } = nextProps;
this.setState({ filteredItems: filterItems(items, filters) });
}
filterItems = (items, filters) => {
/* return filtered list */
};
render() {
return <View>/*display the filtered items */</View>;
}
}
/*
define dispatch methods in propTypes so that they are validated.
*/
ItemsContainer.propTypes = {
items: PropTypes.array.isRequired,
filters: PropTypes.array.isRequired,
onMyAction: PropTypes.func.isRequired,
};
/*
map state to props
*/
const mapStateToProps = state => ({
items: state.App.Items.List,
filters: state.App.Items.Filters,
});
/*
connect dispatch to props so that you can call the methods from the active props scope.
The defined method `onMyAction` can be called in the scope of the componets props.
*/
const mapDispatchToProps = dispatch => ({
onMyAction: value => {
dispatch(() => console.log(`${value}`));
},
});
/* clean way of setting up the connect. */
export default connect(mapStateToProps, mapDispatchToProps)(ItemsContainer);
This example code is a good template for a starting place for your component.
React-Redux connect is used to update store for every actions.
import { connect } from 'react-redux';
const AppContainer = connect(
mapStateToProps,
mapDispatchToProps
)(App);
export default AppContainer;
It's very simply and clearly explained in this blog.
You can clone github project or copy paste the code from that blog to understand the Redux connect.
It's a simple concept. Redux creates a ubiquitous state object (a store) from the actions in the reducers. Like a React component, this state doesn't have to be explicitly coded anywhere, but it helps developers to see a default state object in the reducer file to visualise what is happening. You import the reducer in the component to access the file. Then mapStateToProps selects only the key/value pairs in the store that its component needs. Think of it like Redux creating a global version of a React component's
this.state = ({
cats = [],
dogs = []
})
It is impossible to change the structure of the state by using mapStateToProps(). What you are doing is choosing only the store's key/value pairs that the component needs and passing in the values (from a list of key/values in the store) to the props (local keys) in your component. You do this one value at a time in a list. No structure changes can occur in the process.
P.S. The store is local state. Reducers usually also pass state along to the database with Action Creators getting into the mix, but understand this simple concept first for this specific posting.
P.P.S. It is good practice to separate the reducers into separate files for each one and only import the reducer that the component needs.
Here's an outline/boilerplate for describing the behavior of mapStateToProps:
(This is a vastly simplified implementation of what a Redux container does.)
class MyComponentContainer extends Component {
mapStateToProps(state) {
// this function is specific to this particular container
return state.foo.bar;
}
render() {
// This is how you get the current state from Redux,
// and would be identical, no mater what mapStateToProps does
const { state } = this.context.store.getState();
const props = this.mapStateToProps(state);
return <MyComponent {...this.props} {...props} />;
}
}
and next
function buildReduxContainer(ChildComponentClass, mapStateToProps) {
return class Container extends Component {
render() {
const { state } = this.context.store.getState();
const props = mapStateToProps(state);
return <ChildComponentClass {...this.props} {...props} />;
}
}
}
Yes, you can do this. You can also even process the state and return the object.
function mapStateToProps(state){
let completed = someFunction (state);
return {
completed : completed,
}
}
This would be useful if you want to shift the logic related to state from render function to outside of it.
I would like to re-structure the statement that you mentioned which is:
This means that the state as consumed by your target component can
have a wildly different structure from the state as it is stored on
your store
You can say that the state consumed by your target component has a small portion of the state that is stored on the redux store. In other words, the state consumed by your component would be the sub-set of the state of the redux store.
As far as understanding the connect() method is concerned, it's fairly simple! connect() method has the power to add new props to your component and even override existing props. It is through this connect method that we can access the state of the redux store as well which is thrown to us by the Provider. A combination of which works in your favor and you get to add the state of your redux store to the props of your component.
Above is some theory and I would suggest you look at this video once to understand the syntax better.
import React from 'react';
import {connect} from 'react-redux';
import Userlist from './Userlist';
class Userdetails extends React.Component{
render(){
return(
<div>
<p>Name : <span>{this.props.user.name}</span></p>
<p>ID : <span>{this.props.user.id}</span></p>
<p>Working : <span>{this.props.user.Working}</span></p>
<p>Age : <span>{this.props.user.age}</span></p>
</div>
);
}
}
function mapStateToProps(state){
return {
user:state.activeUser
}
}
export default connect(mapStateToProps, null)(Userdetails);
Example code: https://github.com/d6u/example-redux-update-nested-props/blob/master/one-connect/index.js
View live demo: http://d6u.github.io/example-redux-update-nested-props/one-connect.html
How to optimize small updates to props of nested component?
I have above components, Repo and RepoList. I want to update the tag of the first repo (Line 14). So I dispatched an UPDATE_TAG action. Before I implemented shouldComponentUpdate, the dispatch takes about 200ms, which is expected since we are wasting lots of time diffing <Repo/>s that haven't changed.
After added shouldComponentUpdate, dispatch takes about 30ms. After production build React.js, the updates only cost at about 17ms. This is much better, but timeline view in Chrome dev console still indicate jank frame (longer than than 16.6ms).
Imagine if we have many updates like this, or <Repo/> is more complicated than current one, we won't be able to maintain 60fps.
My question is, for such small updates to a nested component's props, is there a more efficient and canonical way to update the content? Can I still use Redux?
I got a solution by replacing every tags with an observable inside reducer. Something like
// inside reducer when handling UPDATE_TAG action
// repos[0].tags of state is already replaced with a Rx.BehaviorSubject
get('repos[0].tags', state).onNext([{
id: 213,
text: 'Node.js'
}]);
Then I subscribe to their values inside Repo component using https://github.com/jayphelps/react-observable-subscribe. This worked great. Every dispatch only costs 5ms even with development build of React.js. But I feel like this is an anti-pattern in Redux.
Update 1
I followed the recommendation in Dan Abramov's answer and normalized my state and updated connect components
The new state shape is:
{
repoIds: ['1', '2', '3', ...],
reposById: {
'1': {...},
'2': {...}
}
}
I added console.time around ReactDOM.render to time the initial rendering.
However, the performance is worse than before (both initial rendering and updating). (Source: https://github.com/d6u/example-redux-update-nested-props/blob/master/repo-connect/index.js, Live demo: http://d6u.github.io/example-redux-update-nested-props/repo-connect.html)
// With dev build
INITIAL: 520.208ms
DISPATCH: 40.782ms
// With prod build
INITIAL: 138.872ms
DISPATCH: 23.054ms
I think connect on every <Repo/> has lots of overhead.
Update 2
Based on Dan's updated answer, we have to return connect's mapStateToProps arguments return an function instead. You can check out Dan's answer. I also updated the demos.
Below, the performance is much better on my computer. And just for fun, I also added the side effect in reducer approach I talked (source, demo) (seriously don't use it, it's for experiment only).
// in prod build (not average, very small sample)
// one connect at root
INITIAL: 83.789ms
DISPATCH: 17.332ms
// connect at every <Repo/>
INITIAL: 126.557ms
DISPATCH: 22.573ms
// connect at every <Repo/> with memorization
INITIAL: 125.115ms
DISPATCH: 9.784ms
// observables + side effect in reducers (don't use!)
INITIAL: 163.923ms
DISPATCH: 4.383ms
Update 3
Just added react-virtualized example based on "connect at every with memorization"
INITIAL: 31.878ms
DISPATCH: 4.549ms
I’m not sure where const App = connect((state) => state)(RepoList) comes from.
The corresponding example in React Redux docs has a notice:
Don’t do this! It kills any performance optimizations because TodoApp will rerender after every action.
It’s better to have more granular connect() on several components in your view hierarchy that each only
listen to a relevant slice of the state.
We don’t suggest using this pattern. Rather, each connect <Repo> specifically so it reads its own data in its mapStateToProps. The “tree-view” example shows how to do it.
If you make the state shape more normalized (right now it’s all nested), you can separate repoIds from reposById, and then only have your RepoList re-render if repoIds change. This way changes to individual repos won’t affect the list itself, and only the corresponding Repo will get re-rendered. This pull request might give you an idea of how that could work. The “real-world” example shows how you can write reducers that deal with normalized data.
Note that in order to really benefit from the performance offered by normalizing the tree you need to do exactly like this pull request does and pass a mapStateToProps() factory to connect():
const makeMapStateToProps = (initialState, initialOwnProps) => {
const { id } = initialOwnProps
const mapStateToProps = (state) => {
const { todos } = state
const todo = todos.byId[id]
return {
todo
}
}
return mapStateToProps
}
export default connect(
makeMapStateToProps
)(TodoItem)
The reason this is important is because we know IDs never change. Using ownProps comes with a performance penalty: the inner props have to be recalculate any time the outer props change. However using initialOwnProps does not incur this penalty because it is only used once.
A fast version of your example would look like this:
import React from 'react';
import ReactDOM from 'react-dom';
import {createStore} from 'redux';
import {Provider, connect} from 'react-redux';
import set from 'lodash/fp/set';
import pipe from 'lodash/fp/pipe';
import groupBy from 'lodash/fp/groupBy';
import mapValues from 'lodash/fp/mapValues';
const UPDATE_TAG = 'UPDATE_TAG';
const reposById = pipe(
groupBy('id'),
mapValues(repos => repos[0])
)(require('json!../repos.json'));
const repoIds = Object.keys(reposById);
const store = createStore((state = {repoIds, reposById}, action) => {
switch (action.type) {
case UPDATE_TAG:
return set('reposById.1.tags[0]', {id: 213, text: 'Node.js'}, state);
default:
return state;
}
});
const Repo = ({repo}) => {
const [authorName, repoName] = repo.full_name.split('/');
return (
<li className="repo-item">
<div className="repo-full-name">
<span className="repo-name">{repoName}</span>
<span className="repo-author-name"> / {authorName}</span>
</div>
<ol className="repo-tags">
{repo.tags.map((tag) => <li className="repo-tag-item" key={tag.id}>{tag.text}</li>)}
</ol>
<div className="repo-desc">{repo.description}</div>
</li>
);
}
const ConnectedRepo = connect(
(initialState, initialOwnProps) => (state) => ({
repo: state.reposById[initialOwnProps.repoId]
})
)(Repo);
const RepoList = ({repoIds}) => {
return <ol className="repos">{repoIds.map((id) => <ConnectedRepo repoId={id} key={id}/>)}</ol>;
};
const App = connect(
(state) => ({repoIds: state.repoIds})
)(RepoList);
console.time('INITIAL');
ReactDOM.render(
<Provider store={store}>
<App/>
</Provider>,
document.getElementById('app')
);
console.timeEnd('INITIAL');
setTimeout(() => {
console.time('DISPATCH');
store.dispatch({
type: UPDATE_TAG
});
console.timeEnd('DISPATCH');
}, 1000);
Note that I changed connect() in ConnectedRepo to use a factory with initialOwnProps rather than ownProps. This lets React Redux skip all the prop re-evaluation.
I also removed the unnecessary shouldComponentUpdate() on the <Repo> because React Redux takes care of implementing it in connect().
This approach beats both previous approaches in my testing:
one-connect.js: 43.272ms
repo-connect.js before changes: 61.781ms
repo-connect.js after changes: 19.954ms
Finally, if you need to display such a ton of data, it can’t fit in the screen anyway. In this case a better solution is to use a virtualized table so you can render thousands of rows without the performance overhead of actually displaying them.
I got a solution by replacing every tags with an observable inside reducer.
If it has side effects, it’s not a Redux reducer. It may work, but I suggest to put code like this outside Redux to avoid confusion. Redux reducers must be pure functions, and they may not call onNext on subjects.