Skip to content

Commit 2cb5c78

Browse files
author
Lenz Weber
committed
prevent Async context consumers to be used outside of Async components
1 parent c40d5d8 commit 2cb5c78

File tree

2 files changed

+52
-6
lines changed

2 files changed

+52
-6
lines changed

packages/react-async/src/Async.spec.js

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,36 @@ describe("Async", () => {
5757
})
5858
})
5959

60+
describe("rendering context consumers without provider should throw an error", () => {
61+
for (const Component of [
62+
Async.Initial,
63+
Async.Pending,
64+
Async.Fulfilled,
65+
Async.Rejected,
66+
Async.Settled,
67+
]) {
68+
test("does not throw an error when rendered within <Async>", () => {
69+
expect(() =>
70+
render(
71+
<Async>
72+
<Component>{() => {}}</Component>
73+
</Async>
74+
)
75+
).not.toThrowError()
76+
})
77+
test("does throw an error when not rendered within <Async>", () => {
78+
// Prevent the thrown error from showing up in test output by mocking console.error.
79+
jest.spyOn(console, "error")
80+
global.console.error.mockImplementation(() => {})
81+
82+
expect(() => render(<Component>{() => {}}</Component>)).toThrowError()
83+
84+
// Restore the original console.error so other tests will still print errors that occur.
85+
global.console.error.mockRestore()
86+
})
87+
}
88+
})
89+
6090
describe("Async.Fulfilled", () => {
6191
test("renders only after the promise is resolved", async () => {
6292
const promiseFn = () => resolveTo("ok")

packages/react-async/src/Async.tsx

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,23 @@ export const createInstance = <T extends {}>(
6666
defaultProps: AsyncProps<T> = {},
6767
displayName = "Async"
6868
): AsyncConstructor<T> => {
69-
const { Consumer, Provider } = React.createContext<AsyncState<T>>(undefined as any)
69+
const { Consumer: UnguardedConsumer, Provider } = React.createContext<AsyncState<T> | undefined>(
70+
undefined
71+
)
72+
function Consumer({ children }: { children: (value: AsyncState<T>) => React.ReactNode }) {
73+
return (
74+
<UnguardedConsumer>
75+
{value => {
76+
if (!value) {
77+
throw new Error(
78+
"this component should only be used within an associated <Async> component!"
79+
)
80+
}
81+
return children(value)
82+
}}
83+
</UnguardedConsumer>
84+
)
85+
}
7086

7187
type Props = AsyncProps<T>
7288

@@ -266,19 +282,19 @@ export const createInstance = <T extends {}>(
266282
if (propTypes) (Async as React.ComponentClass).propTypes = propTypes.Async
267283

268284
const AsyncInitial: AsyncConstructor<T>["Initial"] = props => (
269-
<Consumer>{(st: AsyncState<T>) => <IfInitial {...props} state={st} />}</Consumer>
285+
<Consumer>{st => <IfInitial {...props} state={st} />}</Consumer>
270286
)
271287
const AsyncPending: AsyncConstructor<T>["Pending"] = props => (
272-
<Consumer>{(st: AsyncState<T>) => <IfPending {...props} state={st} />}</Consumer>
288+
<Consumer>{st => <IfPending {...props} state={st} />}</Consumer>
273289
)
274290
const AsyncFulfilled: AsyncConstructor<T>["Fulfilled"] = props => (
275-
<Consumer>{(st: AsyncState<T>) => <IfFulfilled {...props} state={st} />}</Consumer>
291+
<Consumer>{st => <IfFulfilled {...props} state={st} />}</Consumer>
276292
)
277293
const AsyncRejected: AsyncConstructor<T>["Rejected"] = props => (
278-
<Consumer>{(st: AsyncState<T>) => <IfRejected {...props} state={st} />}</Consumer>
294+
<Consumer>{st => <IfRejected {...props} state={st} />}</Consumer>
279295
)
280296
const AsyncSettled: AsyncConstructor<T>["Settled"] = props => (
281-
<Consumer>{(st: AsyncState<T>) => <IfSettled {...props} state={st} />}</Consumer>
297+
<Consumer>{st => <IfSettled {...props} state={st} />}</Consumer>
282298
)
283299

284300
AsyncInitial.displayName = `${displayName}.Initial`

0 commit comments

Comments
 (0)