Skip to content

Commit e7efc33

Browse files
authored
Fix suspense in useState and useReducer initializers (#41)
* Add failing "suspense in state initialiser" test * Fix operation in useReducer hook - Apply initial state if hook is uninitialised - Ensure that queue/dispatch are initialised before applying updates
1 parent a8f19e3 commit e7efc33

File tree

2 files changed

+63
-39
lines changed

2 files changed

+63
-39
lines changed

src/__tests__/suspense.test.js

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,33 @@ describe('renderPrepass', () => {
224224
})
225225
})
226226

227+
it("handles suspenses thrown in useState's initialState initialiser", () => {
228+
const getValue = jest
229+
.fn()
230+
.mockImplementationOnce(() => {
231+
throw Promise.resolve()
232+
})
233+
.mockImplementation(() => 'test')
234+
235+
const Inner = jest.fn(props => {
236+
expect(props.state).toBe('test')
237+
})
238+
239+
const Outer = jest.fn(() => {
240+
const [state] = useState(() => {
241+
// This will trigger suspense:
242+
return getValue()
243+
})
244+
245+
return <Inner state={state} />
246+
})
247+
248+
return renderPrepass(<Outer />).then(() => {
249+
expect(Outer).toHaveBeenCalled()
250+
expect(Inner).toHaveBeenCalled()
251+
})
252+
})
253+
227254
it('ignores thrown non-promises', () => {
228255
const Outer = () => {
229256
throw new Error('test')
@@ -435,7 +462,7 @@ describe('renderPrepass', () => {
435462
})
436463
})
437464

438-
it('supports rendering previouslt resolved lazy components', () => {
465+
it('supports rendering previously resolved lazy components', () => {
439466
const Inner = jest.fn(() => null)
440467
const loadInner = jest.fn().mockResolvedValueOnce(Inner)
441468
const Outer = React.lazy(loadInner)

src/internals/dispatcher.js

Lines changed: 35 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -175,34 +175,10 @@ function useReducer<S, I, A>(
175175
const id = getCurrentIdentity()
176176
workInProgressHook = createWorkInProgressHook()
177177

178-
if (isReRender) {
179-
// This is a re-render. Apply the new render phase updates to the previous
180-
// current hook.
181-
const queue: UpdateQueue<A> = (workInProgressHook.queue: any)
182-
const dispatch: Dispatch<A> = (queue.dispatch: any)
183-
if (renderPhaseUpdates !== null) {
184-
// Render phase updates are stored in a map of queue -> linked list
185-
const firstRenderPhaseUpdate = renderPhaseUpdates.get(queue)
186-
if (firstRenderPhaseUpdate !== undefined) {
187-
renderPhaseUpdates.delete(queue)
188-
let newState = workInProgressHook.memoizedState
189-
let update = firstRenderPhaseUpdate
190-
do {
191-
// Process this render phase update. We don't have to check the
192-
// priority because it will always be the same as the current
193-
// render's.
194-
const action = update.action
195-
newState = reducer(newState, action)
196-
update = update.next
197-
} while (update !== null)
198-
199-
workInProgressHook.memoizedState = newState
200-
201-
return [newState, dispatch]
202-
}
203-
}
204-
return [workInProgressHook.memoizedState, dispatch]
205-
} else {
178+
// In the case of a re-render after a suspense, the initial state
179+
// may not be set, so instead of initialising if `!isRerender`, we
180+
// check whether `queue` is set
181+
if (workInProgressHook.queue === null) {
206182
let initialState
207183
if (reducer === basicStateReducer) {
208184
// Special case for `useState`.
@@ -214,18 +190,39 @@ function useReducer<S, I, A>(
214190
initialState =
215191
init !== undefined ? init(initialArg) : ((initialArg: any): S)
216192
}
193+
217194
workInProgressHook.memoizedState = initialState
218-
const queue: UpdateQueue<A> = (workInProgressHook.queue = {
219-
last: null,
220-
dispatch: null
221-
})
222-
const dispatch: Dispatch<A> = (queue.dispatch = (dispatchAction.bind(
223-
null,
224-
id,
225-
queue
226-
): any))
227-
return [workInProgressHook.memoizedState, dispatch]
228195
}
196+
197+
const queue: UpdateQueue<A> =
198+
workInProgressHook.queue ||
199+
(workInProgressHook.queue = { last: null, dispatch: null })
200+
const dispatch: Dispatch<A> =
201+
queue.dispatch || (queue.dispatch = dispatchAction.bind(null, id, queue))
202+
203+
if (isReRender && renderPhaseUpdates !== null) {
204+
// This is a re-render. Apply the new render phase updates to the previous
205+
// current hook.
206+
// Render phase updates are stored in a map of queue -> linked list
207+
const firstRenderPhaseUpdate = renderPhaseUpdates.get(queue)
208+
if (firstRenderPhaseUpdate !== undefined) {
209+
renderPhaseUpdates.delete(queue)
210+
let newState = workInProgressHook.memoizedState
211+
let update = firstRenderPhaseUpdate
212+
do {
213+
// Process this render phase update. We don't have to check the
214+
// priority because it will always be the same as the current
215+
// render's.
216+
const action = update.action
217+
newState = reducer(newState, action)
218+
update = update.next
219+
} while (update !== null)
220+
221+
workInProgressHook.memoizedState = newState
222+
}
223+
}
224+
225+
return [workInProgressHook.memoizedState, dispatch]
229226
}
230227

231228
function useMemo<T>(nextCreate: () => T, deps: Array<mixed> | void | null): T {

0 commit comments

Comments
 (0)