Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a simple but reasonably fast Redux version #1

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"mobx-react-devtools": "^4.2.10",
"react": "^15.4.1",
"react-dom": "^15.4.1",
"react-redux": "next",
"react-redux": "^4.4.6",
"redux": "^3.6.0"
},
"scripts": {
Expand Down
4 changes: 3 additions & 1 deletion src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,14 @@ const MobXCanvas = require('bundle?lazy&name=MobXCanvas!./MobXCanvas')
const ReduxCanvas = require('bundle?lazy&name=ReduxCanvas!./ReduxCanvas')
const ReduxCanvasV2 = require('bundle?lazy&name=ReduxCanvasV2!./ReduxCanvasV2')
const ReduxCanvasV3 = require('bundle?lazy&name=ReduxCanvasV3!./ReduxCanvasV3')
const ReduxCanvasV4 = require('bundle?lazy&name=ReduxCanvasV4!./ReduxCanvasV4')

const availableExperiments = {
MobXCanvas,
ReduxCanvas,
ReduxCanvasV2,
ReduxCanvasV3
ReduxCanvasV3,
ReduxCanvasV4
}

class App extends Component {
Expand Down
61 changes: 61 additions & 0 deletions src/ReduxCanvasV4.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import Immutable from 'immutable'
import React from 'react'
import { Provider, connect } from 'react-redux'
import createStore from './createStore'
import Pixel from './Pixel'

// Reducer
const store = createStore((state = Immutable.Map(), action) => {
if (action.type === 'TOGGLE') {
const key = action.i + ',' + action.j
return state.set(key, !state.get(key))
}
return state
})

const ACTIVE_PROPS = { active: true }
const INACTIVE_PROPS = { active: false }

// Connected pixel
const ConnectedPixel = connect(
(initialState, initialProps) => {
const { i, j } = initialProps
return state => {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We know i and j never change so we use a fast path: return a selector that doesn't look at its prop. Selectors that look at ownProps are much slower.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @gaearon, not really getting this change, you're still using i and j here, how is this not looking at ownProps?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because it pulls i and j out of ownProps from the initial call, but then returns a function that only uses a single (state) argument. That way, connect will only run the returned function when the store state has actually changed, and not when the component's props have changed.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@markerikson Surely if props have changed then the whole mapStateToProps would be re-run returning a new instance of that function?

Is the fact that the function returns always the same ACTIVE_PROPS or INACTIVE_PROPS of any significance in all of that?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, that's the point of the "factory function" syntax.

If the first call to mapState returns a function instead of an object, then connect will use the returned function instead of the original function.

In either case, if the final mapState function has a signature of (state, ownProps), it will be called whenever either the store state or the wrapper's props have changed. If it has a signature of (state), it will only be called when the store state has changed.

So, in this example, the first mapState function has a (state, ownProps) signature, but the returned function only has a (state) signature, so it will be called less often.

As far as actually re-rendering, connect just checks to see if the object returned from mapState is shallow-equal to the previous result. Defining these results as constant references doesn't really matter, other than avoiding an object allocation each time.

Please see the React-Redux docs on using mapState functions for more details, as well as my blog post Idiomatic Redux: The Implementation and History of React-Redux.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@markerikson Many thanks for quick response. I was confused thinking that the optimisation is done by returning ACTIVE_PROPS and INACTIVE_PROPS instead of the object, whereas the real optimisation is turned on by using a mapStateToProps function taking one argument instead of two.

I understand the same optimization would apply if mapStateToProps returned an arrow function taking one argument instead of two, as was the case originally?

You are right, it's all written in the documentation but I read it so many times yet failed to understand how the optimization works. I guess it's because it's the first time I am seeing some functionality/optimization enabled by the fact that a callback takes one instead of two arguments.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, that's the case.

const active = state.get(i + ',' + j) || false
return active ? ACTIVE_PROPS : INACTIVE_PROPS
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Memoization. Could do the same with a library like reselect but this seemed simpler.

}
},
(initialState, initialProps) => (dispatch) => {
Copy link

@WiNloSt WiNloSt Dec 12, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm just curious. Should the first parameter be dispatch? I see even though the mapDispatch is a factory, in the Redux source code it would still be passed dispatch in the first argument. If that's the case can we do this instead?

(dispatch, initialProps) => () => {
  const { i, j } = initialProps
  return {
    onToggle() {
      dispatch({ type: 'TOGGLE', i, j })
    }
  };
}

I just want to make sure I didn't miss anything :D

Copy link
Owner

@dtinth dtinth Dec 12, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah. To benefit from memoization the most, should the function be generated outside the returned function?

(dispatch, initialProps) => {
  const { i, j } = initialProps
  const props = {
    onToggle() {
      dispatch({ type: 'TOGGLE', i, j })
    }
  }
  return () => props
}

EDIT: See this comment ↓

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm pretty sure that the () => props is a case of the "factory function" syntax, so that props and onToggle are only generated on the first call. Normally mapDispatch is only called when the component is created, unless you provide that second argument of ownProps, in which case connect will call it every time the incoming props change.

Since the returned function doesn't have 2 arguments defined, connect will detect that it doesn't need to be called repeatedly, as far as I know.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh that’s right. Thanks for catching this! 😄

We’re just using the factory function just so that doDispatchPropsDependOnOwnProps is false and can let react-redux do its optimization.

const { i, j } = initialProps
return {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same thing: no use recomputing this when props change.

onToggle() {
dispatch({ type: 'TOGGLE', i, j })
}
};
},
(stateProps, dispatchProps, ownProps) => ({
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This wasn't essential but since we know all the keys it might be more efficient to hand-roll it.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Curious why the need for a mergeProps usage here?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The default one has to loop over keys of each object when assigning, this one knows the shape exactly.

i: ownProps.i,
j: ownProps.j,
active: stateProps.active,
onToggle: dispatchProps.onToggle
})
)(Pixel);

// Root component
function ReduxCanvas () {
const items = [ ]
for (let i = 0; i < 127; i++) {
for (let j = 0; j < 127; j++) {
items.push(<ConnectedPixel i={i} j={j} key={i + ',' + j} />)
}
}
return (
<Provider store={store}>
<div>
{items}
</div>
</Provider>
)
}

export default ReduxCanvas
7 changes: 5 additions & 2 deletions src/createStore.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import { createStore as originalCreateStore } from 'redux'

export function createStore (reducer) {
const extension = window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
return originalCreateStore(reducer, extension)
let enhancer
if (process.env.NODE_ENV !== 'production' && window.__REDUX_DEVTOOLS_EXTENSION__) {
enhancer = window.__REDUX_DEVTOOLS_EXTENSION__()
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You don't want to slow down the app in production even if user has DevTools

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree for real apps.

I kept it here so that the readers can try out the DevTools right on gh-pages without having to compile and run the app themselves. I also kept it in MobX version.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair enough as long as we don't measure with them enabled. I don't think anybody optimized Redux DevTools for perf.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. I have DevTools turned off when I measure it. I only enabled the DevTools before publishing to GitHub pages.

}
return originalCreateStore(reducer, enhancer)
}

export default createStore