Skip to content

Commit 5dafba7

Browse files
committed
add support for canary's use function
1 parent f058238 commit 5dafba7

File tree

7 files changed

+178
-185
lines changed

7 files changed

+178
-185
lines changed

package.json

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@
6666
"@arethetypeswrong/cli": "^0.15.2",
6767
"@size-limit/preset-small-lib": "^11.1.2",
6868
"@testing-library/jest-dom": "^6.4.2",
69-
"@testing-library/react": "^14.2.2",
69+
"@testing-library/react": "^15",
7070
"@types/eslint": "^8",
7171
"@types/react": "^18",
7272
"@types/react-dom": "^18",
@@ -81,12 +81,19 @@
8181
"lint-staged": "^15.2.2",
8282
"prettier": "^3.2.5",
8383
"publint": "^0.2.7",
84-
"react": "^18.2.0",
85-
"react-dom": "^18.2.0",
84+
"react": "canary",
85+
"react-dom": "canary",
8686
"rimraf": "^5.0.5",
8787
"size-limit": "^11.1.2",
8888
"tsup": "^8.0.2",
8989
"typescript": "^5.4.3",
9090
"vitest": "^1.4.0"
91+
},
92+
"resolutions": {
93+
"react": "npm:canary",
94+
"react-dom": "npm:canary"
95+
},
96+
"dependencies": {
97+
"@testing-library/user-event": "^14.5.2"
9198
}
9299
}

src/canary.test.tsx

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import { render, screen } from "@testing-library/react";
2+
import userEvent from "@testing-library/user-event";
3+
import React, { useState } from "react";
4+
import { describe, expect, it } from "vitest";
5+
import { use } from "./canary";
6+
import { createRequiredContext } from ".";
7+
8+
describe("canary support", () => {
9+
describe("use", () => {
10+
const { TestContext, TestProvider } = createRequiredContext<string>().with({
11+
name: "test",
12+
});
13+
14+
function TestComponent() {
15+
const value = use(TestContext);
16+
return <div>{value}</div>;
17+
}
18+
19+
it("throws when value is not set", () => {
20+
expect(() => render(<TestComponent />)).toThrow(
21+
"use: context value is not set. Use TestProvider to set the value.",
22+
);
23+
});
24+
25+
it("does not throw when value is set", () => {
26+
expect(() =>
27+
render(
28+
<TestProvider test="Hello">
29+
<TestComponent />
30+
</TestProvider>,
31+
),
32+
).not.toThrow();
33+
34+
expect(screen.getByText("Hello")).toBeInTheDocument();
35+
});
36+
37+
const user = userEvent.setup();
38+
39+
function Button() {
40+
const [clicked, setClicked] = useState(false);
41+
const text = clicked ? use(TestContext) : "Click me";
42+
return (
43+
<button
44+
onClick={() => {
45+
setClicked(true);
46+
}}
47+
>
48+
{text}
49+
</button>
50+
);
51+
}
52+
53+
it("can be called conditionally", async () => {
54+
render(
55+
<TestProvider test="Clicked">
56+
<Button />
57+
</TestProvider>,
58+
);
59+
60+
const button = screen.getByRole("button", { name: "Click me" });
61+
62+
expect(button).toBeInTheDocument();
63+
64+
await user.click(button);
65+
66+
expect(button).toHaveTextContent("Clicked");
67+
});
68+
69+
it("still throws if called conditionally", async () => {
70+
render(<Button />);
71+
72+
const button = screen.getByRole("button", { name: "Click me" });
73+
74+
expect(button).toBeInTheDocument();
75+
76+
try {
77+
await user.click(button);
78+
79+
expect.unreachable("Should throw");
80+
} catch (error) {
81+
expect(error).toBeInstanceOf(Error);
82+
expect((error as Error).message).toBe(
83+
"use: context value is not set. Use TestProvider to set the value.",
84+
);
85+
}
86+
});
87+
});
88+
});

src/canary.tsx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// eslint-disable-next-line import/no-unresolved
2+
/// <reference types="react/canary" />
3+
import { use as originalUse } from "react";
4+
import type { RequiredContext } from "./types";
5+
import { UNSET_VALUE } from "./types";
6+
import { notSet } from ".";
7+
8+
export function use<T>(context: RequiredContext<T>): T {
9+
const value = originalUse(context);
10+
if (value === UNSET_VALUE) {
11+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
12+
throw new Error(notSet("use", context.providerName ?? "UnknownProvider"));
13+
}
14+
return value;
15+
}

src/index.tsx

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import type { PropsWithChildren, ConsumerProps } from "react";
22
import React, { createContext, useContext, useDebugValue } from "react";
3-
import type { Names, NamedRequiredContext } from "./types";
3+
import type { Names, NamedRequiredContext, RequiredContext } from "./types";
44
import { UNSET_VALUE } from "./types";
55
import { assert, capitalise } from "./util";
66

77
export { UNSET_VALUE };
88

9-
const notSet = (caller: string, providerName: string) =>
9+
export const notSet = (caller: string, providerName: string) =>
1010
`${caller}: context value is not set. Use ${providerName} to set the value.`;
1111

1212
export function createRequiredContext<T>() {
@@ -35,8 +35,13 @@ export function createRequiredContext<T>() {
3535
hookName.startsWith("use"),
3636
`createRequiredContext: hookName must start with "use". Got: ${hookName}`,
3737
);
38-
const Context = createContext<T | typeof UNSET_VALUE>(UNSET_VALUE);
39-
Context.displayName = contextName;
38+
const Context: RequiredContext<T> = Object.assign(
39+
createContext<T | typeof UNSET_VALUE>(UNSET_VALUE),
40+
{
41+
providerName,
42+
displayName: contextName,
43+
},
44+
);
4045
return {
4146
[contextName]: Context,
4247
[providerName](props: PropsWithChildren<Record<string, T>>) {

src/types.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,13 @@ type GetHookName<N extends Names> = GetName<
4444
`use${Capitalize<N["name"]>}`
4545
>;
4646

47+
export interface RequiredContext<T> extends Context<T | typeof UNSET_VALUE> {
48+
displayName: string;
49+
providerName: string;
50+
}
51+
4752
export type NamedRequiredContext<T, N extends Names> = Compute<
48-
Record<GetContextName<N>, Context<T | typeof UNSET_VALUE>> &
53+
Record<GetContextName<N>, RequiredContext<T>> &
4954
Record<
5055
GetProviderName<N>,
5156
FC<PropsWithChildren<Record<GetProviderProp<N>, T>>>

src/util.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,6 @@ export function assert(
1414
throw new Error(typeof message === "string" ? message : message());
1515
}
1616
}
17+
18+
export const safeAssign: <T>(target: T, source: Partial<T>) => T =
19+
Object.assign;

0 commit comments

Comments
 (0)