Skip to content

Commit 622545d

Browse files
committed
fix timing issue in component updates due to consecutive dispatches
1 parent 9803028 commit 622545d

File tree

2 files changed

+54
-1
lines changed

2 files changed

+54
-1
lines changed

src/components/connectAdvanced.js

+6-1
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,7 @@ export default function connectAdvanced(
253253
const lastChildProps = useRef()
254254
const lastWrapperProps = useRef(wrapperProps)
255255
const childPropsFromStoreUpdate = useRef()
256+
const renderIsScheduled = useRef(false)
256257

257258
const actualChildProps = usePureOnlyMemo(() => {
258259
// Tricky logic here:
@@ -282,6 +283,7 @@ export default function connectAdvanced(
282283
// We want to capture the wrapper props and child props we used for later comparisons
283284
lastWrapperProps.current = wrapperProps
284285
lastChildProps.current = actualChildProps
286+
renderIsScheduled.current = false
285287

286288
// If the render was from a store update, clear out that reference and cascade the subscriber update
287289
if (childPropsFromStoreUpdate.current) {
@@ -328,14 +330,17 @@ export default function connectAdvanced(
328330

329331
// If the child props haven't changed, nothing to do here - cascade the subscription update
330332
if (newChildProps === lastChildProps.current) {
331-
notifyNestedSubs()
333+
if (!renderIsScheduled.current) {
334+
notifyNestedSubs()
335+
}
332336
} else {
333337
// Save references to the new child props. Note that we track the "child props from store update"
334338
// as a ref instead of a useState/useReducer because we need a way to determine if that value has
335339
// been processed. If this went into useState/useReducer, we couldn't clear out the value without
336340
// forcing another re-render, which we don't want.
337341
lastChildProps.current = newChildProps
338342
childPropsFromStoreUpdate.current = newChildProps
343+
renderIsScheduled.current = true
339344

340345
// If the child props _did_ change (or we caught an error), this wrapper component needs to re-render
341346
forceComponentUpdateDispatch({

test/components/connect.spec.js

+48
Original file line numberDiff line numberDiff line change
@@ -3182,6 +3182,54 @@ describe('React', () => {
31823182

31833183
expect(reduxCountPassedToMapState).toEqual(3)
31843184
})
3185+
3186+
it('should ensure top-down updates for consecutive batched updates', () => {
3187+
const INC = 'INC'
3188+
const reducer = (c = 0, { type }) => (type === INC ? c + 1 : c)
3189+
const store = createStore(reducer)
3190+
3191+
let executionOrder = []
3192+
let expectedExecutionOrder = [
3193+
'parent map',
3194+
'parent render',
3195+
'child map',
3196+
'child render'
3197+
]
3198+
3199+
const ChildImpl = () => {
3200+
executionOrder.push('child render')
3201+
return <div>child</div>
3202+
}
3203+
3204+
const Child = connect(state => {
3205+
executionOrder.push('child map')
3206+
return { state }
3207+
})(ChildImpl)
3208+
3209+
const ParentImpl = () => {
3210+
executionOrder.push('parent render')
3211+
return <Child />
3212+
}
3213+
3214+
const Parent = connect(state => {
3215+
executionOrder.push('parent map')
3216+
return { state }
3217+
})(ParentImpl)
3218+
3219+
rtl.render(
3220+
<ProviderMock store={store}>
3221+
<Parent />
3222+
</ProviderMock>
3223+
)
3224+
3225+
executionOrder = []
3226+
rtl.act(() => {
3227+
store.dispatch({ type: INC })
3228+
store.dispatch({ type: '' })
3229+
})
3230+
3231+
expect(executionOrder).toEqual(expectedExecutionOrder)
3232+
})
31853233
})
31863234

31873235
it("should enforce top-down updates to ensure a deleted child's mapState doesn't throw errors", () => {

0 commit comments

Comments
 (0)