Skip to content

Commit c5235ab

Browse files
os-zhuangclaude
andcommitted
refactor(console): fetch book/doc once at a /docs layout route
Previously every docs page (index, book landing, reader) called useBookData independently, so a single page issued the doc+book list fetch from several components and every in-section navigation re-fetched. Hoist the fetch to a DocsLayout route that loads once and shares the result via context; the pages read it with useBookData (now a context consumer). Nesting the /docs child routes under the layout (top-level + app-scoped) means navigating within the section issues no further book/doc requests — only the reader's single-doc content fetch. Browser-verified: a full /docs → /docs/manual → reader walk now hits /meta/doc and /meta/book once each (×2 is dev StrictMode only), down from ~8 and ~6 before. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent f0e588c commit c5235ab

5 files changed

Lines changed: 79 additions & 26 deletions

File tree

apps/console/src/App.tsx

Lines changed: 14 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ import SharedRecordPage from './pages/SharedRecordPage';
4545
import DocPage from './pages/DocPage';
4646
import DocsIndex from './pages/DocsIndex';
4747
import DocsSlug from './pages/DocsSlug';
48+
import DocsLayout from './pages/DocsLayout';
4849
import { LoginPage } from './pages/auth/LoginPage';
4950
import { RegisterPage } from './pages/auth/RegisterPage';
5051
import { ForgotPasswordPage } from './pages/auth/ForgotPasswordPage';
@@ -185,25 +186,22 @@ export function App() {
185186
* lists every installed `doc` (grouped by package), and one
186187
* viewer route renders any item; cross-references between docs
187188
* resolve to that same viewer route. Both are app-independent. */}
189+
{/* Docs portal (ADR-0046 §6). The layout fetches book + doc once
190+
* and shares it with every child route via context. Children:
191+
* index → book index
192+
* :slug → a book landing, or a flat-doc permalink that
193+
* redirects to its canonical /docs/<book>/<name>
194+
* :slug/:name → in-book reader (doc identity stays single-
195+
* coordinate; the book segment is derived nav). */}
188196
<Route path="/docs" element={
189197
<ProtectedRoute>
190-
<DocsIndex />
198+
<DocsLayout />
191199
</ProtectedRoute>
192-
} />
193-
{/* One portal segment: a book landing (slug) or a flat-doc permalink
194-
* that redirects to its canonical /docs/<book>/<name> (ADR-0046 §6). */}
195-
<Route path="/docs/:slug" element={
196-
<ProtectedRoute>
197-
<DocsSlug />
198-
</ProtectedRoute>
199-
} />
200-
{/* In-book reader: <slug> = book, <name> = doc (doc identity stays
201-
* single-coordinate; the book segment is derived navigation). */}
202-
<Route path="/docs/:slug/:name" element={
203-
<ProtectedRoute>
204-
<DocPage />
205-
</ProtectedRoute>
206-
} />
200+
}>
201+
<Route index element={<DocsIndex />} />
202+
<Route path=":slug" element={<DocsSlug />} />
203+
<Route path=":slug/:name" element={<DocPage />} />
204+
</Route>
207205
<Route path="/home" element={
208206
<ProtectedRoute>
209207
<HomeRoute />

apps/console/src/AppContent.tsx

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ const IntegrationsPage = lazy(() => import('./pages/developer/IntegrationsPage')
3636
const DocPage = lazy(() => import('./pages/DocPage'));
3737
const AppDocsIndex = lazy(() => import('./pages/AppDocsIndex'));
3838
const DocsSlug = lazy(() => import('./pages/DocsSlug'));
39+
const DocsLayout = lazy(() => import('./pages/DocsLayout'));
3940

4041
// Note: marketplace routes (`system/marketplace`, `system/marketplace/:packageId`)
4142
// are registered by DefaultAppContent in @object-ui/app-shell so they're
@@ -92,11 +93,14 @@ const systemRoutes = (
9293
<Route path="developer/integrations" element={<Suspense fallback={<LoadingScreen />}><IntegrationsPage /></Suspense>} />
9394
{/* ADR-0048 — package docs under the package container: app-scoped index
9495
(/apps/:packageId/docs) + single-doc viewer (/apps/:packageId/docs/:name) */}
95-
<Route path="docs" element={<Suspense fallback={<LoadingScreen />}><AppDocsIndex /></Suspense>} />
96-
{/* ADR-0046 §6 — one segment resolves to a book landing or redirects a flat
97-
doc to its canonical /docs/<book>/<name>; then the in-book reader. */}
98-
<Route path="docs/:slug" element={<Suspense fallback={<LoadingScreen />}><DocsSlug /></Suspense>} />
99-
<Route path="docs/:slug/:name" element={<Suspense fallback={<LoadingScreen />}><DocPage /></Suspense>} />
96+
{/* ADR-0046 §6 — the docs layout fetches book + doc once and shares it with
97+
the app-scoped index, book landings, and reader (one segment resolves to
98+
a book landing or redirects a flat doc to /docs/<book>/<name>). */}
99+
<Route path="docs" element={<Suspense fallback={<LoadingScreen />}><DocsLayout /></Suspense>}>
100+
<Route index element={<Suspense fallback={<LoadingScreen />}><AppDocsIndex /></Suspense>} />
101+
<Route path=":slug" element={<Suspense fallback={<LoadingScreen />}><DocsSlug /></Suspense>} />
102+
<Route path=":slug/:name" element={<Suspense fallback={<LoadingScreen />}><DocPage /></Suspense>} />
103+
</Route>
100104
{/* Legacy URL redirects → metadata-admin engine (zero-cost compatibility). */}
101105
<Route path="system/objects" element={<ObjectRedirect />} />
102106
<Route path="system/objects/:objectName" element={<ObjectRedirect />} />
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/**
2+
* ObjectUI
3+
* Copyright (c) 2024-present ObjectStack Inc.
4+
*
5+
* This source code is licensed under the MIT license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
import { Outlet } from 'react-router-dom';
10+
import { BookDataContext, useBookDataFetch } from './use-book-data';
11+
12+
/**
13+
* Layout route for the docs portal (`/docs/*`). Fetches the book + doc
14+
* metadata ONCE and shares it with every child route (index, book landing,
15+
* reader) via context, so navigating within the section doesn't re-fetch and a
16+
* single page never issues the same request from several components.
17+
*/
18+
export default function DocsLayout() {
19+
const data = useBookDataFetch();
20+
return (
21+
<BookDataContext.Provider value={data}>
22+
<Outlet />
23+
</BookDataContext.Provider>
24+
);
25+
}

apps/console/src/pages/docs-portal.test.tsx

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,17 +61,21 @@ vi.mock('@object-ui/i18n', () => ({
6161
}));
6262

6363
// Imported AFTER the mocks so the pages pick up the mocked modules.
64+
import DocsLayout from './DocsLayout';
6465
import DocsIndex from './DocsIndex';
6566
import DocsSlug from './DocsSlug';
6667
import DocPage from './DocPage';
6768

69+
// Mirrors the real route table: the layout fetches once and shares the data.
6870
function Harness({ entry }: { entry: string }) {
6971
return (
7072
<MemoryRouter initialEntries={[entry]}>
7173
<Routes>
72-
<Route path="/docs" element={<DocsIndex />} />
73-
<Route path="/docs/:slug" element={<DocsSlug />} />
74-
<Route path="/docs/:slug/:name" element={<DocPage />} />
74+
<Route path="/docs" element={<DocsLayout />}>
75+
<Route index element={<DocsIndex />} />
76+
<Route path=":slug" element={<DocsSlug />} />
77+
<Route path=":slug/:name" element={<DocPage />} />
78+
</Route>
7579
</Routes>
7680
</MemoryRouter>
7781
);

apps/console/src/pages/use-book-data.ts

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
* LICENSE file in the root directory of this source tree.
77
*/
88

9-
import { useEffect, useMemo, useState } from 'react';
9+
import { createContext, useContext, useEffect, useMemo, useState } from 'react';
1010
import { useAdapter } from '@object-ui/app-shell';
1111
import { buildPortalBooks, type Book, type ResolverDoc } from './book-nav';
1212

@@ -77,7 +77,12 @@ interface RawState {
7777
error?: string;
7878
}
7979

80-
export function useBookData(): BookData {
80+
/**
81+
* Fetch the book + doc metadata once. Used by {@link BookDataProvider} at the
82+
* `/docs` layout route so the whole docs section shares a single fetch, rather
83+
* than each page (index, book landing, reader) re-fetching independently.
84+
*/
85+
function useBookDataFetch(): BookData {
8186
const adapter = useAdapter();
8287
const [data, setData] = useState<RawState>({ authoredBooks: [], docs: [], state: 'loading' });
8388

@@ -125,3 +130,20 @@ export function useBookData(): BookData {
125130

126131
return { ...data, books };
127132
}
133+
134+
const LOADING: BookData = { authoredBooks: [], books: [], docs: [], state: 'loading' };
135+
136+
/**
137+
* Shared book/doc data, provided once at the `/docs` layout route (see
138+
* DocsLayout) and consumed by {@link useBookData}. Exported so the layout's
139+
* provider — which lives in a `.tsx` file — can supply it.
140+
*/
141+
export const BookDataContext = createContext<BookData | null>(null);
142+
143+
/** The one-time fetcher the layout drives. Internal to the docs section. */
144+
export { useBookDataFetch };
145+
146+
/** Read the shared book/doc data. Returns a loading state outside the provider. */
147+
export function useBookData(): BookData {
148+
return useContext(BookDataContext) ?? LOADING;
149+
}

0 commit comments

Comments
 (0)