-
Notifications
You must be signed in to change notification settings - Fork 2.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
One more idea for the profiler #11379
Changes from 9 commits
562c3bb
841a9ef
ed0d2fc
895792c
ee44633
2a433a4
e5efcc9
c39cba0
1e64614
02aa46c
61443bd
d8c129e
4914048
87bcbb9
a683081
5c562e4
5e8aadc
79e00f5
659f884
2a5a487
836c1c4
4d27a07
2b64982
5df75f9
c408bed
554ecac
891183c
ce05f8a
ccde7cb
0d141df
4b9a0ec
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -43,7 +43,12 @@ import { LoadableQueryHookFetchPolicy } from "../../types/types"; | |
import { QueryReference } from "../../../react"; | ||
import { FetchMoreFunction, RefetchFunction } from "../useSuspenseQuery"; | ||
import invariant, { InvariantError } from "ts-invariant"; | ||
import { profile, profileHook, spyOnConsole } from "../../../testing/internal"; | ||
import { | ||
Profiler, | ||
createTestProfiler, | ||
spyOnConsole, | ||
useTrackComponentRender, | ||
} from "../../../testing/internal"; | ||
|
||
interface SimpleQueryData { | ||
greeting: string; | ||
|
@@ -149,28 +154,34 @@ function usePaginatedQueryCase() { | |
return { query, link, client }; | ||
} | ||
|
||
function createDefaultProfiledComponents<TData = unknown>() { | ||
const SuspenseFallback = profile({ | ||
Component: function SuspenseFallback() { | ||
return <p>Loading</p>; | ||
}, | ||
}); | ||
function createDefaultProfiledComponents< | ||
Snapshot extends { | ||
result: UseReadQueryResult<any> | null; | ||
error?: Error | null; | ||
}, | ||
TData = Snapshot["result"] extends UseReadQueryResult<infer TData> | null | ||
? TData | ||
: unknown, | ||
>(profiler: Profiler<Snapshot>) { | ||
function SuspenseFallback() { | ||
useTrackComponentRender(); | ||
return <p>Loading</p>; | ||
} | ||
|
||
const ReadQueryHook = profileHook< | ||
UseReadQueryResult<TData>, | ||
{ queryRef: QueryReference<TData> } | ||
>(({ queryRef }) => useReadQuery(queryRef), { displayName: "UseReadQuery" }); | ||
function ReadQueryHook({ queryRef }: { queryRef: QueryReference<TData> }) { | ||
useTrackComponentRender(); | ||
profiler.mergeSnapshot({ | ||
result: useReadQuery(queryRef), | ||
} as Partial<Snapshot>); | ||
|
||
const ErrorFallback = profile<{ error: Error | null }, { error: Error }>({ | ||
Component: function Fallback({ error }) { | ||
ErrorFallback.replaceSnapshot({ error }); | ||
return null; | ||
} | ||
|
||
return <div>Oops</div>; | ||
}, | ||
initialSnapshot: { | ||
error: null, | ||
}, | ||
}); | ||
function ErrorFallback({ error }: { error: Error }) { | ||
profiler.mergeSnapshot({ error } as Partial<Snapshot>); | ||
|
||
return <div>Oops</div>; | ||
} | ||
|
||
function ErrorBoundary({ children }: { children: React.ReactNode }) { | ||
return ( | ||
|
@@ -219,98 +230,120 @@ function renderWithClient( | |
it("loads a query and suspends when the load query function is called", async () => { | ||
const { query, mocks } = useSimpleQueryCase(); | ||
|
||
const Profiler = createTestProfiler({ | ||
initialSnapshot: { | ||
result: null as UseReadQueryResult<SimpleQueryData> | null, | ||
}, | ||
}); | ||
|
||
const { SuspenseFallback, ReadQueryHook } = | ||
createDefaultProfiledComponents<SimpleQueryData>(); | ||
createDefaultProfiledComponents(Profiler); | ||
|
||
const App = profile({ | ||
Component: () => { | ||
const [loadQuery, queryRef] = useLoadableQuery(query); | ||
function App() { | ||
useTrackComponentRender(); | ||
const [loadQuery, queryRef] = useLoadableQuery(query); | ||
|
||
return ( | ||
<> | ||
<button onClick={() => loadQuery()}>Load query</button> | ||
<Suspense fallback={<SuspenseFallback />}> | ||
{queryRef && <ReadQueryHook queryRef={queryRef} />} | ||
</Suspense> | ||
</> | ||
); | ||
}, | ||
}); | ||
return ( | ||
<> | ||
<button onClick={() => loadQuery()}>Load query</button> | ||
<Suspense fallback={<SuspenseFallback />}> | ||
{queryRef && <ReadQueryHook queryRef={queryRef} />} | ||
</Suspense> | ||
</> | ||
); | ||
} | ||
|
||
const { user } = renderWithMocks(<App />, { mocks }); | ||
const { user } = renderWithMocks( | ||
<Profiler> | ||
<App /> | ||
</Profiler>, | ||
{ mocks } | ||
); | ||
|
||
expect(SuspenseFallback).not.toHaveRendered(); | ||
{ | ||
const { renderedComponents } = await Profiler.takeRender(); | ||
|
||
expect(renderedComponents).toStrictEqual([App]); | ||
} | ||
|
||
await act(() => user.click(screen.getByText("Load query"))); | ||
|
||
expect(SuspenseFallback).toHaveRendered(); | ||
expect(ReadQueryHook).not.toHaveRendered(); | ||
expect(App).toHaveRenderedTimes(2); | ||
{ | ||
const { renderedComponents } = await Profiler.takeRender(); | ||
|
||
const snapshot = await ReadQueryHook.takeSnapshot(); | ||
expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); | ||
} | ||
|
||
expect(snapshot).toEqual({ | ||
data: { greeting: "Hello" }, | ||
error: undefined, | ||
networkStatus: NetworkStatus.ready, | ||
}); | ||
{ | ||
const { snapshot, renderedComponents } = await Profiler.takeRender(); | ||
|
||
expect(snapshot.result).toEqual({ | ||
data: { greeting: "Hello" }, | ||
error: undefined, | ||
networkStatus: NetworkStatus.ready, | ||
}); | ||
|
||
expect(SuspenseFallback).toHaveRenderedTimes(1); | ||
expect(ReadQueryHook).toHaveRenderedTimes(1); | ||
expect(App).toHaveRenderedTimes(3); | ||
expect(renderedComponents).toStrictEqual([ReadQueryHook]); | ||
} | ||
}); | ||
|
||
it("loads a query with variables and suspends by passing variables to the loadQuery function", async () => { | ||
const { query, mocks } = useVariablesQueryCase(); | ||
|
||
const { SuspenseFallback, ReadQueryHook } = | ||
createDefaultProfiledComponents<VariablesCaseData>(); | ||
|
||
const App = profile({ | ||
Component: function App() { | ||
const [loadQuery, queryRef] = useLoadableQuery(query); | ||
|
||
return ( | ||
<> | ||
<button onClick={() => loadQuery({ id: "1" })}>Load query</button> | ||
<Suspense fallback={<SuspenseFallback />}> | ||
{queryRef && <ReadQueryHook queryRef={queryRef} />} | ||
</Suspense> | ||
</> | ||
); | ||
const Profiler = createTestProfiler({ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This pattern makes it easier for me to extract this into a helper if need-be. In fact, 90% of the tests in this suite operate on the same snapshot shape, which means I could extract this to a reusable function to avoid some repetition in tests. For example: function createLoadableQueryProfiler<TData>() {
return createTestProfiler({
initialSnapshot: {
lastError: null as Error | null,
result: null as UseReadQueryResult<TData> | null
}
})
} then in my tests: it('checks something about useLoadableQuery', async () => {
const Profiler = createLoadableQueryProfiler();
// snapshot is now of shape { lastError, result }
const { snapshot } = await Profiler.takeRender()
}) |
||
initialSnapshot: { | ||
result: null as UseReadQueryResult<VariablesCaseData> | null, | ||
}, | ||
}); | ||
|
||
const { user } = renderWithMocks(<App />, { mocks }); | ||
const { SuspenseFallback, ReadQueryHook } = | ||
createDefaultProfiledComponents(Profiler); | ||
|
||
{ | ||
const { renderedComponents } = await App.takeRender(); | ||
expect(renderedComponents).toStrictEqual(["App"]); | ||
function App() { | ||
useTrackComponentRender(); | ||
const [loadQuery, queryRef] = useLoadableQuery(query); | ||
|
||
return ( | ||
<> | ||
<button onClick={() => loadQuery({ id: "1" })}>Load query</button> | ||
<Suspense fallback={<SuspenseFallback />}> | ||
{queryRef && <ReadQueryHook queryRef={queryRef} />} | ||
</Suspense> | ||
</> | ||
); | ||
} | ||
|
||
await act(() => user.click(screen.getByText("Load query"))); | ||
const { user } = renderWithMocks( | ||
<Profiler> | ||
<App /> | ||
</Profiler>, | ||
{ mocks } | ||
); | ||
|
||
{ | ||
const { renderedComponents } = await App.takeRender(); | ||
expect(renderedComponents).toStrictEqual(["App", "SuspenseFallback"]); | ||
const { renderedComponents } = await Profiler.takeRender(); | ||
expect(renderedComponents).toStrictEqual([App]); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Now that we enforce a single profiler, it makes it more difficult to avoid stepping through each render like I originally did with the nested profiler approach. |
||
} | ||
|
||
await act(() => user.click(screen.getByText("Load query"))); | ||
|
||
{ | ||
const { renderedComponents } = await App.takeRender(); | ||
expect(renderedComponents).toStrictEqual(["UseReadQuery"]); | ||
const { renderedComponents } = await Profiler.takeRender(); | ||
expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); | ||
} | ||
|
||
{ | ||
const snapshot = await ReadQueryHook.takeSnapshot(); | ||
const { snapshot, renderedComponents } = await Profiler.takeRender(); | ||
|
||
expect(snapshot).toEqual({ | ||
expect(renderedComponents).toStrictEqual([ReadQueryHook]); | ||
expect(snapshot.result).toEqual({ | ||
data: { character: { id: "1", name: "Spider-Man" } }, | ||
networkStatus: NetworkStatus.ready, | ||
error: undefined, | ||
}); | ||
} | ||
|
||
await expect(App).not.toRerender(); | ||
await expect(Profiler).not.toRerender(); | ||
}); | ||
|
||
it("changes variables on a query and resuspends when passing new variables to the loadQuery function", async () => { | ||
|
@@ -319,7 +352,7 @@ it("changes variables on a query and resuspends when passing new variables to th | |
const { SuspenseFallback, ReadQueryHook } = | ||
createDefaultProfiledComponents<VariablesCaseData>(); | ||
|
||
const App = profile({ | ||
const App = createTestProfiler({ | ||
Component: () => { | ||
const [loadQuery, queryRef] = useLoadableQuery(query); | ||
|
||
|
@@ -660,7 +693,7 @@ it("returns initial cache data followed by network data when the fetch policy is | |
hello: string; | ||
}>(); | ||
|
||
const App = profile({ | ||
const App = createTestProfiler({ | ||
Component: () => { | ||
const [loadQuery, queryRef] = useLoadableQuery(query, { | ||
fetchPolicy: "cache-and-network", | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't worry about looking through all of these. I tried to convert a variety of use cases to ensure the updates to the API held up in each case.