Skip to content

Commit 601d48f

Browse files
feat(overmind-react): pass new object of overmind prop only when a state change has occured
BREAKING CHANGE: HOC no longer uses PureComponent
1 parent ec69c18 commit 601d48f

File tree

4 files changed

+229
-57
lines changed

4 files changed

+229
-57
lines changed

packages/node_modules/overmind-react/src/index.test.tsx

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,4 +134,55 @@ describe('React', () => {
134134
expect(ConnectedFoo.name).toBe('ConnectFooComponent')
135135
expect(ConnectedBar.name).toBe('ConnectBarComponent')
136136
})
137+
138+
test('should allow using shouldComponentUpdate', () => {
139+
let renderCount = 0
140+
141+
const doThis: Action = ({ state }) => {
142+
state.foo = 'bar2'
143+
}
144+
const config = {
145+
state: {
146+
foo: 'bar',
147+
},
148+
actions: {
149+
doThis,
150+
},
151+
}
152+
153+
type IConfig = {
154+
state: {
155+
foo: typeof config.state.foo
156+
}
157+
actions: {
158+
doThis: typeof doThis
159+
}
160+
}
161+
162+
const app = new Overmind(config)
163+
164+
type Action<Input = void> = TAction<IConfig, Input>
165+
166+
const connect = createConnect(app)
167+
168+
class FooComponent extends React.Component<TConnect<typeof config>> {
169+
shouldComponentUpdate(nextProps) {
170+
return this.props.overmind !== nextProps.overmind
171+
}
172+
render() {
173+
renderCount++
174+
return <h1>{this.props.overmind.state.foo}</h1>
175+
}
176+
}
177+
178+
const ConnectedFoo = connect(FooComponent)
179+
180+
const tree = renderer.create(<ConnectedFoo />).toJSON()
181+
182+
expect(renderCount).toBe(1)
183+
184+
app.actions.doThis()
185+
186+
expect(renderCount).toBe(2)
187+
})
137188
})

packages/node_modules/overmind-react/src/index.ts

Lines changed: 118 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@ import {
33
// @ts-ignore
44
__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED,
55
ClassicComponentClass,
6+
Component,
67
ComponentClass,
78
createElement,
8-
PureComponent,
99
StatelessComponent,
1010
// @ts-ignore
1111
useEffect,
@@ -160,72 +160,137 @@ export const createConnect = <A extends Overmind<Configuration>>(
160160
}
161161
}
162162

163-
class HOC extends PureComponent {
164-
tree = (overmind as any).proxyStateTree.getTrackStateTree()
165-
componentInstanceId = componentInstanceId++
166-
currentFlushId = 0
167-
componentDidMount() {
168-
overmind.eventHub.emitAsync(EventType.COMPONENT_ADD, {
169-
componentId: populatedComponent.__componentId,
170-
componentInstanceId: this.componentInstanceId,
171-
name,
172-
paths: Array.from(this.tree.pathDependencies) as any,
173-
})
174-
}
175-
componentDidUpdate() {
176-
overmind.eventHub.emitAsync(EventType.COMPONENT_UPDATE, {
177-
componentId: populatedComponent.__componentId,
178-
componentInstanceId: this.componentInstanceId,
179-
name,
180-
flushId: this.currentFlushId,
181-
paths: Array.from(this.tree.pathDependencies as Set<string>),
182-
})
183-
}
184-
componentWillUnmount() {
185-
;(overmind as any).proxyStateTree.disposeTree(this.tree)
186-
overmind.eventHub.emitAsync(EventType.COMPONENT_REMOVE, {
187-
componentId: populatedComponent.__componentId,
188-
componentInstanceId: this.componentInstanceId,
189-
name,
190-
})
191-
}
192-
onUpdate = (mutatons, paths, flushId) => {
193-
this.currentFlushId = flushId
194-
this.forceUpdate()
195-
}
196-
render() {
197-
if (isClassComponent) {
163+
if (IS_PRODUCTION) {
164+
class HOC extends Component {
165+
tree = (overmind as any).proxyStateTree.getTrackStateTree()
166+
state: {
167+
overmind: any
168+
}
169+
constructor(props) {
170+
super(props)
171+
this.state = {
172+
overmind: {
173+
state: this.tree.state,
174+
actions: overmind.actions,
175+
addMutationListener: overmind.addMutationListener,
176+
onUpdate: this.onUpdate,
177+
tree: this.tree,
178+
},
179+
}
180+
}
181+
componentWillUnmount() {
182+
;(overmind as any).proxyStateTree.disposeTree(this.tree)
183+
}
184+
onUpdate = () => {
185+
this.setState({
186+
overmind: {
187+
state: this.tree.state,
188+
actions: overmind.actions,
189+
addMutationListener: overmind.addMutationListener,
190+
onUpdate: this.onUpdate,
191+
tree: this.tree,
192+
},
193+
})
194+
}
195+
render() {
196+
if (isClassComponent) {
197+
return createElement(component, {
198+
...this.props,
199+
overmind: this.state.overmind,
200+
} as any)
201+
}
202+
203+
this.tree.track(this.onUpdate)
204+
198205
return createElement(component, {
199206
...this.props,
207+
overmind: this.state.overmind,
208+
} as any)
209+
}
210+
}
211+
212+
return HOC as any
213+
} else {
214+
class HOC extends Component {
215+
tree = (overmind as any).proxyStateTree.getTrackStateTree()
216+
componentInstanceId = componentInstanceId++
217+
currentFlushId = 0
218+
state: {
219+
overmind: any
220+
}
221+
constructor(props) {
222+
super(props)
223+
this.state = {
200224
overmind: {
201225
state: this.tree.state,
202226
actions: overmind.actions,
203227
addMutationListener: overmind.addMutationListener,
204228
onUpdate: this.onUpdate,
205229
tree: this.tree,
206230
},
207-
} as any)
231+
}
232+
}
233+
componentDidMount() {
234+
overmind.eventHub.emitAsync(EventType.COMPONENT_ADD, {
235+
componentId: populatedComponent.__componentId,
236+
componentInstanceId: this.componentInstanceId,
237+
name,
238+
paths: Array.from(this.tree.pathDependencies) as any,
239+
})
240+
}
241+
componentDidUpdate() {
242+
overmind.eventHub.emitAsync(EventType.COMPONENT_UPDATE, {
243+
componentId: populatedComponent.__componentId,
244+
componentInstanceId: this.componentInstanceId,
245+
name,
246+
flushId: this.currentFlushId,
247+
paths: Array.from(this.tree.pathDependencies as Set<string>),
248+
})
249+
}
250+
componentWillUnmount() {
251+
;(overmind as any).proxyStateTree.disposeTree(this.tree)
252+
overmind.eventHub.emitAsync(EventType.COMPONENT_REMOVE, {
253+
componentId: populatedComponent.__componentId,
254+
componentInstanceId: this.componentInstanceId,
255+
name,
256+
})
208257
}
258+
onUpdate = (mutatons, paths, flushId) => {
259+
this.currentFlushId = flushId
260+
this.setState({
261+
overmind: {
262+
state: this.tree.state,
263+
actions: overmind.actions,
264+
addMutationListener: overmind.addMutationListener,
265+
onUpdate: this.onUpdate,
266+
tree: this.tree,
267+
},
268+
})
269+
}
270+
render() {
271+
if (isClassComponent) {
272+
return createElement(component, {
273+
...this.props,
274+
overmind: this.state.overmind,
275+
} as any)
276+
}
209277

210-
this.tree.track(this.onUpdate)
278+
this.tree.track(this.onUpdate)
211279

212-
return createElement(component, {
213-
...this.props,
214-
overmind: {
215-
state: this.tree.state,
216-
actions: overmind.actions,
217-
addMutationListener: overmind.addMutationListener,
218-
},
219-
} as any)
280+
return createElement(component, {
281+
...this.props,
282+
overmind: this.state.overmind,
283+
} as any)
284+
}
220285
}
221-
}
222286

223-
Object.defineProperties(HOC, {
224-
name: {
225-
value: 'Connect' + (component.displayName || component.name || ''),
226-
},
227-
})
287+
Object.defineProperties(HOC, {
288+
name: {
289+
value: 'Connect' + (component.displayName || component.name || ''),
290+
},
291+
})
228292

229-
return HOC as any
293+
return HOC as any
294+
}
230295
}
231296
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
export default (ts) =>
2+
ts
3+
? [
4+
{
5+
fileName: 'components/App.tsx',
6+
code: `
7+
import * as React from 'react'
8+
import { connect, Connect } from '../overmind'
9+
10+
type Props = {} & Connect
11+
12+
class App extends React.Component<Props> {
13+
shouldComponentUpdate(nextProps: Props) {
14+
return this.props.overmind !== nextProps.overmind
15+
}
16+
render() {
17+
const { state, actions } = this.props.overmind
18+
19+
return <div />
20+
}
21+
}
22+
23+
export default connect(App)
24+
`,
25+
},
26+
]
27+
: [
28+
{
29+
fileName: 'components/App.jsx',
30+
code: `
31+
import React from 'react'
32+
import { connect } from '../overmind'
33+
34+
class App extends React.Component {
35+
shouldComponentUpdate(nextProps) {
36+
return this.props.overmind !== nextProps.overmind
37+
}
38+
render() {
39+
const { state, actions } = this.props.overmind
40+
41+
return <div />
42+
}
43+
}
44+
45+
export default connect(App)
46+
`,
47+
},
48+
]

packages/overmind-website/guides/beginner/06_usingovermindwithreact.md

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
There are two different ways to connect Overmind to React. You can either use a traditional **Higher Order Component** or you can use the new **hooks** api to expose state and actions.
44

5-
When you connect Overmind to a component you ensure that whenever any tracked state changes only components interested in that state will rerender and they will rerender "at their point in the UI structure". That means we remove a lot of unnecessary work from React. There is no reason for the whole React component tree to rerender when only one component is interested in a change.
5+
When you connect Overmind to a component you ensure that whenever any tracked state changes only components interested in that state will rerender and they will rerender "at their point in the component tree". That means we remove a lot of unnecessary work from React. There is no reason for the whole React component tree to rerender when only one component is interested in a change.
66

77
## With HOC
88
```marksy
@@ -11,7 +11,15 @@ h(Example, { name: "guide/usingovermindwithreact/hoc" })
1111

1212
### Rendering
1313

14-
When you connect a component with the **connect** HOC it will automatically optimize with **PureComponent**, meaning that the only way your connected component can render is if the parent passes a changed prop or the state that is tracked changes.
14+
When you connect a component with the **connect HOC** it will be responsible for tracking and trigger a render when the tracked state is updated. The **overmind** prop passed to the component you defined holds the state and actions. If you want to detect inside your component that it was indeed an Overmind state change causing the render you can compare the **overmind** prop itself.
15+
16+
```marksy
17+
h(Example, { name: "guide/usingovermindwithreact/hoc_compareprop" })
18+
```
19+
20+
You will not be able to compare a previous state value in Overmind with the new. That is simply because Overmind is not immutable and it should not be. You will not use **shouldComponentUpdate** to compare state in Overmind, though you can of course still use it to compare props from a parent. This is a bit of a mindshift if you come from Redux, but it actually removes the mental burden of doing this stuff.
21+
22+
If you previously used **componentDidUpdate** to trigger an effect, that is no longer necessary either. You rather listen to state changes in Overmind using **addMutationListener** specified below in *effects*.
1523

1624
### Passing state as props
1725

@@ -23,7 +31,7 @@ h(Example, { name: "guide/usingovermindwithreact/hoc_passprop" })
2331

2432
### Effects
2533

26-
To run effects in components based on changes to state you use the **subscribe** function in the lifecycle hooks of React.
34+
To run effects in components based on changes to state you use the **addMutationListener** function in the lifecycle hooks of React.
2735

2836
```marksy
2937
h(Example, { name: "guide/usingovermindwithreact/hoc_effect" })
@@ -36,7 +44,7 @@ h(Example, { name: "guide/usingovermindwithreact/hook" })
3644

3745
### Rendering
3846

39-
When you use the Overmind hook it will ensure that the component will render when any tracked state changes. It will not optimize related to checking if parent props has changed. That means whenever the parent renders, this component renders as well. You will need to wrap your component with [**React.memo**](https://reactjs.org/docs/react-api.html#reactmemo).
47+
When you use the Overmind hook it will ensure that the component will render when any tracked state changes. It will not do anything related to the props passed to the component. That means whenever the parent renders, this component renders as well. You will need to wrap your component with [**React.memo**](https://reactjs.org/docs/react-api.html#reactmemo) to optimize rendering caused by a parent.
4048

4149
### Passing state as props
4250

0 commit comments

Comments
 (0)