-
-
Notifications
You must be signed in to change notification settings - Fork 10
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
12 changed files
with
293 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
23 changes: 23 additions & 0 deletions
23
packages/suspense-website/src/examples/useCacheValue/hook.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
import { userProfileCache } from "../createCache/cache"; | ||
|
||
// REMOVE_BEFORE | ||
|
||
import { | ||
STATUS_PENDING, | ||
STATUS_REJECTED, | ||
STATUS_RESOLVED, | ||
useCacheValue, | ||
} from "suspense"; | ||
|
||
function Example({ userId }: { userId: string }) { | ||
const { error, status, value } = useCacheValue(userProfileCache, userId); | ||
|
||
switch (status) { | ||
case STATUS_PENDING: | ||
// Rending loading UI ... | ||
case STATUS_REJECTED: | ||
// Render "error" ... | ||
case STATUS_RESOLVED: | ||
// Render "value" ... | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
33 changes: 33 additions & 0 deletions
33
packages/suspense-website/src/routes/api/useCacheValue.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
import { Link } from "react-router-dom"; | ||
import Block from "../../components/Block"; | ||
import Code from "../../components/Code"; | ||
import Container from "../../components/Container"; | ||
import Header from "../../components/Header"; | ||
import Note from "../../components/Note"; | ||
import SubHeading from "../../components/SubHeading"; | ||
import { useCacheValue } from "../../examples"; | ||
|
||
export default function Route() { | ||
return ( | ||
<Container> | ||
<Block> | ||
<Header title="useCacheValue" /> | ||
</Block> | ||
<Block> | ||
<SubHeading title="Loading data without Suspense" /> | ||
<p> | ||
Data can be fetched without suspending using the{" "} | ||
<code>useCacheValue</code> hook. (This hook uses the imperative{" "} | ||
<code>readAsync</code> API, called from an effect.) | ||
</p> | ||
<Code code={useCacheValue.hook} /> | ||
</Block> | ||
<Note type="warn"> | ||
<p> | ||
This hook exists as an escape hatch. When possible, values should be | ||
loaded using the <code>read</code> API. | ||
</p> | ||
</Note> | ||
</Container> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,154 @@ | ||
/** | ||
* @jest-environment jsdom | ||
*/ | ||
|
||
import { createRoot } from "react-dom/client"; | ||
import { act } from "react-dom/test-utils"; | ||
import { createCache } from "../cache/createCache"; | ||
import { | ||
STATUS_NOT_STARTED, | ||
STATUS_PENDING, | ||
STATUS_REJECTED, | ||
STATUS_RESOLVED, | ||
} from "../constants"; | ||
import { Cache, CacheLoadOptions, Deferred, Status } from "../types"; | ||
import { createDeferred } from "../utils/createDeferred"; | ||
import { useCacheValue } from "./useCacheValue"; | ||
|
||
describe("useCacheValue", () => { | ||
let cache: Cache<[string], string>; | ||
let fetch: jest.Mock<Promise<string> | string, [string, CacheLoadOptions]>; | ||
let getCacheKey: jest.Mock<string, [string]>; | ||
let lastRenderedError: any = undefined; | ||
let lastRenderedStatus: Status | undefined = undefined; | ||
let lastRenderedValue: string | undefined = undefined; | ||
let pendingDeferred: Deferred<string>[] = []; | ||
|
||
function Component({ string }: { string: string }): any { | ||
const result = useCacheValue(cache, string); | ||
|
||
lastRenderedError = result.error; | ||
lastRenderedStatus = result.status; | ||
lastRenderedValue = result.value; | ||
|
||
return null; | ||
} | ||
|
||
beforeEach(() => { | ||
// @ts-ignore | ||
global.IS_REACT_ACT_ENVIRONMENT = true; | ||
|
||
fetch = jest.fn(); | ||
fetch.mockImplementation(async (key: string) => { | ||
const deferred = createDeferred<string>(); | ||
|
||
pendingDeferred.push(deferred); | ||
|
||
return deferred; | ||
}); | ||
|
||
getCacheKey = jest.fn(); | ||
getCacheKey.mockImplementation((key) => key.toString()); | ||
|
||
cache = createCache<[string], string>({ | ||
debugLabel: "cache", | ||
getKey: getCacheKey, | ||
load: fetch, | ||
}); | ||
|
||
lastRenderedStatus = undefined; | ||
lastRenderedStatus = undefined; | ||
lastRenderedValue = undefined; | ||
pendingDeferred = []; | ||
}); | ||
|
||
it("should return values that have already been loaded", async () => { | ||
cache.cache("cached", "test"); | ||
|
||
const container = document.createElement("div"); | ||
const root = createRoot(container); | ||
await act(async () => { | ||
root.render(<Component string="test" />); | ||
}); | ||
|
||
expect(lastRenderedError).toBeUndefined(); | ||
expect(lastRenderedStatus).toBe(STATUS_RESOLVED); | ||
expect(lastRenderedValue).toBe("cached"); | ||
}); | ||
|
||
it("should fetch values that have not yet been fetched", async () => { | ||
expect(cache.getStatus("test")).toBe(STATUS_NOT_STARTED); | ||
|
||
const container = document.createElement("div"); | ||
const root = createRoot(container); | ||
await act(async () => { | ||
root.render(<Component string="test" />); | ||
}); | ||
|
||
expect(pendingDeferred).toHaveLength(1); | ||
expect(lastRenderedStatus).toBe(STATUS_PENDING); | ||
|
||
await act(async () => pendingDeferred[0].resolve("resolved")); | ||
|
||
expect(lastRenderedError).toBeUndefined(); | ||
expect(lastRenderedStatus).toBe(STATUS_RESOLVED); | ||
expect(lastRenderedValue).toBe("resolved"); | ||
}); | ||
|
||
it("should handle values that are rejected", async () => { | ||
expect(cache.getStatus("test")).toBe(STATUS_NOT_STARTED); | ||
|
||
const container = document.createElement("div"); | ||
const root = createRoot(container); | ||
await act(async () => { | ||
root.render(<Component string="test" />); | ||
}); | ||
|
||
expect(pendingDeferred).toHaveLength(1); | ||
expect(lastRenderedStatus).toBe(STATUS_PENDING); | ||
|
||
await act(async () => pendingDeferred[0].reject("rejected")); | ||
|
||
expect(lastRenderedError).toBe("rejected"); | ||
expect(lastRenderedStatus).toBe(STATUS_REJECTED); | ||
expect(lastRenderedValue).toBeUndefined(); | ||
}); | ||
|
||
it("should wait for values that have already been loaded to be resolved", async () => { | ||
cache.readAsync("test"); | ||
expect(pendingDeferred).toHaveLength(1); | ||
|
||
const container = document.createElement("div"); | ||
const root = createRoot(container); | ||
await act(async () => { | ||
root.render(<Component string="test" />); | ||
}); | ||
|
||
expect(lastRenderedStatus).toBe(STATUS_PENDING); | ||
|
||
await act(async () => pendingDeferred[0].resolve("resolved")); | ||
|
||
expect(lastRenderedError).toBeUndefined(); | ||
expect(lastRenderedStatus).toBe(STATUS_RESOLVED); | ||
expect(lastRenderedValue).toBe("resolved"); | ||
}); | ||
|
||
it("should wait for values that have already been loaded to be rejected", async () => { | ||
cache.readAsync("test"); | ||
expect(pendingDeferred).toHaveLength(1); | ||
|
||
const container = document.createElement("div"); | ||
const root = createRoot(container); | ||
await act(async () => { | ||
root.render(<Component string="test" />); | ||
}); | ||
|
||
expect(lastRenderedStatus).toBe(STATUS_PENDING); | ||
|
||
await act(async () => pendingDeferred[0].reject("rejected")); | ||
|
||
expect(lastRenderedError).toBe("rejected"); | ||
expect(lastRenderedStatus).toBe(STATUS_REJECTED); | ||
expect(lastRenderedValue).toBeUndefined(); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
import { useEffect } from "react"; | ||
import { | ||
STATUS_NOT_STARTED, | ||
STATUS_PENDING, | ||
STATUS_REJECTED, | ||
STATUS_RESOLVED, | ||
} from "../constants"; | ||
import { Cache, StatusPending, StatusRejected, StatusResolved } from "../types"; | ||
import { useCacheStatus } from "./useCacheStatus"; | ||
|
||
export type ErrorResponse = { error: any; status: StatusRejected; value: any }; | ||
export type PendingResponse = { | ||
error: undefined; | ||
status: StatusPending; | ||
value: undefined; | ||
}; | ||
export type ResolvedResponse<Value> = { | ||
error: undefined; | ||
status: StatusResolved; | ||
value: Value; | ||
}; | ||
|
||
export function useCacheValue<Params extends any[], Value>( | ||
cache: Cache<Params, Value>, | ||
...params: Params | ||
): ErrorResponse | PendingResponse | ResolvedResponse<Value> { | ||
const status = useCacheStatus(cache, ...params); | ||
|
||
useEffect(() => { | ||
switch (status) { | ||
case STATUS_NOT_STARTED: | ||
cache.prefetch(...params); | ||
} | ||
}, [cache, status, ...params]); | ||
|
||
switch (status) { | ||
case STATUS_REJECTED: | ||
let caught; | ||
try { | ||
cache.getValue(...params); | ||
} catch (error) { | ||
caught = error; | ||
} | ||
return { error: caught, status: STATUS_REJECTED, value: undefined }; | ||
case STATUS_RESOLVED: | ||
return { | ||
error: undefined, | ||
status: STATUS_RESOLVED, | ||
value: cache.getValueIfCached(...params), | ||
}; | ||
default: | ||
return { | ||
error: undefined, | ||
status: STATUS_PENDING, | ||
value: undefined, | ||
}; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters