So let’s do the same with our app.

We’ll subdivide our canvas into four quadrants.

When we need to change a pixel,

We can update just the relevant quadrant, leaving other 3 quadrants alone.

Instead of re-rendering all 16,384 pixels, we can re-render just 64×64=4096 pixels. This is 75% savings in performance.

4,096 is still a large number. So what we’ll do is we’ll subdivide our canvas recursively until we reach a 1×1 pixel canvas.

To be able to update the component this way, we need to structure our state in the same way too, so that when the state changes, we can use the === operator to determine if the quadrant’s state has been changed or not.

Here’s the code to (recursively) generate an initial state:

const generateInitialState = (size) => (size === 1

? false

: Immutable.List([

generateInitialState(size / 2),

generateInitialState(size / 2),

generateInitialState(size / 2),

generateInitialState(size / 2)

])

)

Now that our state is a recursively-nested tree, instead of referring to each pixel by its coordinate like (58, 52), we’re need to refer to each pixel by its path like (1, 3, 3, 2, 0, 2, 1) instead.

But to present them on the screen, we need to be able to figure out the coordinates from the path:

function keyPathToCoordinate (keyPath) {

let i = 0

let j = 0

for (const quadrant of keyPath) {

i <<= 1

j <<= 1

switch (quadrant) {

case 0: j |= 1; break

case 2: i |= 1; break

case 3: i |= 1; j |= 1; break

default:

}

}

return [ i, j ]

}

And we also need to do the inverse:

function coordinateToKeyPath (i, j) {

const keyPath = [ ]

for (let threshold = 64; threshold > 0; threshold >>= 1) {

keyPath.push(i < threshold

? j < threshold ? 1 : 0

: j < threshold ? 2 : 3

)

i %= threshold

j %= threshold

}

return keyPath

}

Now we can change our reducer to look like this:

const store = createStore(

function reducer (state = generateInitialState(128), action) {

if (action.type === 'TOGGLE') {

const keyPath = coordinateToKeyPath(action.i, action.j)

return state.updateIn(keyPath, (active) => !active)

// |

// This is why I use Immutable.js:

// So that I can use this method.

}

return state

}

)

Then we create a component to traverse this tree and put everything in place. The GridContainer connects to the store and renders the outermost Grid .

function ReduxCanvas () {

return <Provider store={store}><GridContainer /></Provider>

} const GridContainer = connect(

(state, ownProps) => ({ state }),

(dispatch) => ({ onToggle: (i, j) => dispatch(toggle(i, j)) })

)(function GridContainer ({ state, onToggle }) {

return <Grid keyPath={[ ]} state={state} onToggle={onToggle} />

})

Then each Grid renders a smaller version of itself recursively until it reaches a leaf (a white/black 1x1 pixel canvas).

class Grid extends React.PureComponent {

constructor (props) {

super(props)

this.handleToggle = this.handleToggle.bind(this)

}

shouldComponentUpdate (nextProps) {

// Required since we construct a new `keyPath` every render

// but we know that each grid instance will be rendered with

// a constant `keyPath`. Otherwise we need to memoize the

// `keyPath` for each children we render to remove this

// "escape hatch."

return this.props.state !== nextProps.state

}

handleToggle () {

const [ i, j ] = keyPathToCoordinate(this.props.keyPath)

this.props.onToggle(i, j)

}

render () {

const { keyPath, state } = this.props

if (typeof state === 'boolean') {

const [ i, j ] = keyPathToCoordinate(keyPath)

return <Pixel

i={i}

j={j}

active={state}

onToggle={this.handleToggle}

/>

} else {

return <div>

<Grid onToggle={this.props.onToggle} keyPath={[ ...keyPath, 0 ]} state={state.get(0)} />

<Grid onToggle={this.props.onToggle} keyPath={[ ...keyPath, 1 ]} state={state.get(1)} />

<Grid onToggle={this.props.onToggle} keyPath={[ ...keyPath, 2 ]} state={state.get(2)} />

<Grid onToggle={this.props.onToggle} keyPath={[ ...keyPath, 3 ]} state={state.get(3)} />

</div>

}

}

}

Here’s the result.

Phew, we are back to speed! It feels as fast as the MobX version. Plus you can do hot-reloading and time-travel as well.

Our DOM tree also looks more tree-ish:

Compared to all previous approaches: