Skip to content

Commit 552c80f

Browse files
authored
docs: fix inconsistencies between docs and implementation (#20)
1 parent 6b87804 commit 552c80f

File tree

3 files changed

+124
-68
lines changed

3 files changed

+124
-68
lines changed

README.md

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ pnpm test
4646
## Quick Start
4747

4848
```tsx
49-
import { Router, Outlet, useParams } from "@funstack/router";
49+
import { Router, Outlet } from "@funstack/router";
5050
import type { RouteDefinition } from "@funstack/router";
5151

5252
function Layout() {
@@ -70,9 +70,8 @@ function Users() {
7070
return <h1>Users</h1>;
7171
}
7272

73-
function UserDetail() {
74-
const { id } = useParams<{ id: string }>();
75-
return <h1>User {id}</h1>;
73+
function UserDetail({ params }: { params: { id: string } }) {
74+
return <h1>User {params.id}</h1>;
7675
}
7776

7877
const routes: RouteDefinition[] = [
@@ -104,10 +103,11 @@ The root component that provides routing context.
104103
<Router routes={routes} />
105104
```
106105

107-
| Prop | Type | Description |
108-
| ---------- | ------------------- | ------------------------------------------- |
109-
| `routes` | `RouteDefinition[]` | Array of route definitions |
110-
| `children` | `ReactNode` | Optional children rendered alongside routes |
106+
| Prop | Type | Description |
107+
| ------------ | -------------------- | -------------------------------------------------------------------- |
108+
| `routes` | `RouteDefinition[]` | Array of route definitions |
109+
| `onNavigate` | `OnNavigateCallback` | Optional callback invoked before navigation is intercepted |
110+
| `fallback` | `FallbackMode` | Fallback mode when Navigation API is unavailable (default: `"none"`) |
111111

112112
#### `<Outlet>`
113113

@@ -151,7 +151,7 @@ const location = useLocation();
151151

152152
#### `useParams()`
153153

154-
Returns the current route's path parameters.
154+
Returns the current route's path parameters. Note that route components also receive `params` as a prop, so this hook is mainly useful for non-route components that need access to params.
155155

156156
```tsx
157157
// Route: /users/:id
@@ -182,10 +182,31 @@ setSearchParams((prev) => {
182182

183183
#### `RouteDefinition`
184184

185+
Route components receive a `params` prop with the matched path parameters. Use the `route()` helper for type-safe route definitions:
186+
187+
```typescript
188+
import { route } from "@funstack/router";
189+
190+
// Route without loader - component receives params prop
191+
route({
192+
path: "users/:id",
193+
component: UserDetail, // receives { params: { id: string } }
194+
});
195+
196+
// Route with loader - component receives both data and params props
197+
route({
198+
path: "users/:id",
199+
loader: async ({ params }) => fetchUser(params.id),
200+
component: UserDetail, // receives { data: Promise<User>, params: { id: string } }
201+
});
202+
```
203+
204+
You can also define routes as plain objects (without type inference):
205+
185206
```typescript
186207
type RouteDefinition = {
187208
path: string;
188-
component?: React.ComponentType;
209+
component?: React.ComponentType<{ params: Record<string, string> }>;
189210
children?: RouteDefinition[];
190211
};
191212
```

docs/DATA_LOADER_ARCHITECTURE.md

Lines changed: 76 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -13,19 +13,19 @@ The router passes whatever the loader returns (Promise or value) to the componen
1313
### Route Definition
1414

1515
```typescript
16-
type RouteDefinition<TData = unknown> = {
17-
path: string;
16+
type RouteDefinition<TPath extends string = string, TData = unknown> = {
17+
path: TPath;
1818
children?: RouteDefinition[];
1919
} & (
2020
| {
21-
// Route with loader - component receives data prop
21+
// Route with loader - component receives data and params props
2222
loader: (args: LoaderArgs) => TData;
23-
component: ComponentType<{ data: TData }>;
23+
component: ComponentType<{ data: TData; params: PathParams<TPath> }>;
2424
}
2525
| {
26-
// Route without loader - component receives no data prop
26+
// Route without loader - component receives params prop only
2727
loader?: never;
28-
component?: ComponentType;
28+
component?: ComponentType<{ params: PathParams<TPath> }>;
2929
}
3030
);
3131

@@ -38,25 +38,25 @@ type LoaderArgs = {
3838

3939
### Route Definition Helper
4040

41-
Using `RouteDefinition[]` directly loses type inference for each route's `TData`. A helper function preserves the inferred types:
41+
Using `RouteDefinition[]` directly loses type inference for each route's `TData` and params. A helper function preserves the inferred types:
4242

4343
```typescript
44-
// Helper for routes with loader - infers TData from loader return type
45-
function route<TData>(definition: {
46-
path: string;
44+
// Helper for routes with loader - infers TData from loader return type, params from path
45+
function route<TPath extends string, TData>(definition: {
46+
path: TPath;
4747
loader: (args: LoaderArgs) => TData;
48-
component: ComponentType<{ data: TData }>;
48+
component: ComponentType<{ data: TData; params: PathParams<TPath> }>;
4949
children?: RouteDefinition[];
50-
}): RouteDefinition<TData> {
50+
}): RouteDefinition<TPath, TData> {
5151
return definition;
5252
}
5353

54-
// Helper for routes without loader
55-
function route(definition: {
56-
path: string;
57-
component?: ComponentType;
54+
// Helper for routes without loader - infers params from path
55+
function route<TPath extends string>(definition: {
56+
path: TPath;
57+
component?: ComponentType<{ params: PathParams<TPath> }>;
5858
children?: RouteDefinition[];
59-
}): RouteDefinition {
59+
}): RouteDefinition<TPath> {
6060
return definition;
6161
}
6262
```
@@ -72,45 +72,50 @@ const routes = [
7272
const res = await fetch(`/api/users/${params.userId}`, { signal });
7373
return res.json() as User;
7474
},
75-
// ✅ TypeScript knows component must accept { data: Promise<User> }
75+
// ✅ TypeScript knows component must accept { data: Promise<User>, params: { userId: string } }
7676
}),
7777
route({
7878
path: "settings",
7979
component: SettingsPage,
8080
loader: () => getSettingsFromLocalStorage(),
81-
// ✅ TypeScript infers TData from loader return type
81+
// ✅ TypeScript infers TData from loader return type, params from path
8282
}),
8383
route({
8484
path: "about",
8585
component: AboutPage,
86-
// ✅ No loader, no data prop required
86+
// ✅ No loader, component receives { params: {} }
8787
}),
8888
];
8989
```
9090

91-
Without the helper, all routes would have `TData = unknown`, breaking type safety between loader and component.
91+
Without the helper, all routes would have `TData = unknown` and `params = Record<string, string>`, breaking type safety between loader and component.
9292

9393
### Component Access
9494

95-
Components receive the loader result directly as a `data` prop:
95+
Components receive the loader result directly as a `data` prop, along with a `params` prop containing the matched path parameters:
9696

9797
```typescript
98-
// Async loader - component receives Promise
99-
function UserDetail({ data }: { data: Promise<User> }) {
98+
// Async loader - component receives Promise and params
99+
function UserDetail({ data, params }: { data: Promise<User>; params: { userId: string } }) {
100100
const user = use(data); // Suspends until resolved
101-
return <div>{user.name}</div>;
101+
return <div>{user.name} (ID: {params.userId})</div>;
102102
}
103103

104-
// Sync loader - component receives value directly
105-
function SettingsPage({ data }: { data: Settings }) {
104+
// Sync loader - component receives value and params directly
105+
function SettingsPage({ data, params }: { data: Settings; params: {} }) {
106106
return <div>{data.theme}</div>;
107107
}
108+
109+
// No loader - component receives only params
110+
function AboutPage({ params }: { params: {} }) {
111+
return <div>About</div>;
112+
}
108113
```
109114
110115
**Advantages of props over hooks**:
111116
112117
1. Explicit dependency - component signature shows what it receives
113-
2. Better TypeScript inference - prop type tied to loader's return type
118+
2. Better TypeScript inference - prop types tied to loader's return type and path pattern
114119
3. No context lookup overhead
115120
4. Simpler implementation
116121
5. Flexible - works with both sync and async loaders
@@ -139,10 +144,20 @@ function App() {
139144
);
140145
}
141146

142-
// Component receives Promise, uses `use()` to suspend
143-
function UserDetail({ data }: { data: Promise<User> }) {
147+
// Component receives Promise and params, uses `use()` to suspend
148+
function UserDetail({
149+
data,
150+
params,
151+
}: {
152+
data: Promise<User>;
153+
params: { userId: string };
154+
}) {
144155
const user = use(data);
145-
return <div>{user.name}</div>;
156+
return (
157+
<div>
158+
{user.name} (ID: {params.userId})
159+
</div>
160+
);
146161
}
147162
```
148163

@@ -157,7 +172,7 @@ const routes: RouteDefinition[] = [
157172
];
158173

159174
// No Suspense needed for sync loaders
160-
function SettingsPage({ data }: { data: Settings }) {
175+
function SettingsPage({ data, params }: { data: Settings; params: {} }) {
161176
return <div>{data.theme}</div>;
162177
}
163178
```
@@ -227,12 +242,18 @@ type MatchedRouteWithData = MatchedRoute & {
227242
type LoaderFunction<TData = unknown> = (args: LoaderArgs) => TData;
228243

229244
// Component prop type (when loader is defined)
230-
type RouteComponentProps<TData = unknown> = {
245+
type RouteComponentProps<TPath extends string, TData = unknown> = {
231246
data: TData;
247+
params: PathParams<TPath>;
248+
};
249+
250+
// Component prop type (when loader is not defined)
251+
type RouteComponentPropsNoLoader<TPath extends string> = {
252+
params: PathParams<TPath>;
232253
};
233254
```
234255
235-
Note: RouteContext remains unchanged - it doesn't need to store the data since it's passed directly as a prop.
256+
Note: RouteContext remains unchanged - it doesn't need to store the data since it's passed directly as a prop. Params are also passed as props for consistency and type safety.
236257
237258
### 3. Loader Execution Strategy
238259
@@ -279,8 +300,12 @@ function RouteRenderer({
279300
return (
280301
<RouteContext.Provider value={routeContextValue}>
281302
{Component ? (
282-
// Pass data as prop if loader exists
283-
data !== undefined ? <Component data={data} /> : <Component />
303+
// Always pass params, pass data only if loader exists
304+
route.loader ? (
305+
<Component data={data} params={params} />
306+
) : (
307+
<Component params={params} />
308+
)
284309
) : (
285310
outlet
286311
)}
@@ -488,19 +513,24 @@ const data = useLoaderData(); // Returns resolved data
488513
489514
### FUNSTACK Router (This Design)
490515
491-
FUNSTACK passes loader result as a prop, component handles it:
516+
FUNSTACK passes loader result and params as props, component handles them:
492517
493518
```typescript
494-
// FUNSTACK - async loader, component receives Promise
495-
function UserDetail({ data }: { data: Promise<User> }) {
519+
// FUNSTACK - async loader, component receives Promise and params
520+
function UserDetail({ data, params }: { data: Promise<User>; params: { userId: string } }) {
496521
const user = use(data); // Component chooses when to suspend
497-
return <div>{user.name}</div>;
522+
return <div>{user.name} (ID: {params.userId})</div>;
498523
}
499524

500-
// FUNSTACK - sync loader, component receives value directly
501-
function SettingsPage({ data }: { data: Settings }) {
525+
// FUNSTACK - sync loader, component receives value and params directly
526+
function SettingsPage({ data, params }: { data: Settings; params: {} }) {
502527
return <div>{data.theme}</div>;
503528
}
529+
530+
// FUNSTACK - no loader, component receives only params
531+
function AboutPage({ params }: { params: {} }) {
532+
return <div>About</div>;
533+
}
504534
```
505535
506536
**Advantages of our approach**:
@@ -517,7 +547,8 @@ function SettingsPage({ data }: { data: Settings }) {
517547
The data loader feature adds:
518548
519549
1. `loader` property on route definitions (can return Promise or value)
520-
2. `data` prop passed to route components
521-
3. Loader result caching to prevent duplicate execution
550+
2. `data` prop passed to route components (when loader is defined)
551+
3. `params` prop passed to all route components (with types inferred from path pattern)
552+
4. Loader result caching to prevent duplicate execution
522553
523-
Components receive loader results as props and handle Promises with React's `use()` hook when needed, giving maximum flexibility while keeping the router simple.
554+
Components receive loader results and params as props and handle Promises with React's `use()` hook when needed, giving maximum flexibility while keeping the router simple.

docs/architecture.md

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -206,8 +206,10 @@ const page = searchParams.get("page");
206206
```typescript
207207
// Main router context
208208
type RouterContextValue = {
209-
// Current navigation state
210-
currentEntry: NavigationHistoryEntry;
209+
// Current location entry (abstracted for fallback mode compatibility)
210+
locationEntry: LocationEntry;
211+
// Current URL
212+
url: URL;
211213
// Navigation function
212214
navigate: (to: string, options?: NavigateOptions) => void;
213215
};
@@ -278,6 +280,8 @@ src/
278280
├── index.ts # Public exports
279281
├── Router.tsx # <Router> provider component
280282
├── Outlet.tsx # <Outlet> component
283+
├── route.ts # Route definition helper with type inference
284+
├── types.ts # Shared type definitions
281285
├── hooks/
282286
│ ├── useNavigate.ts
283287
│ ├── useLocation.ts
@@ -286,10 +290,14 @@ src/
286290
├── context/
287291
│ ├── RouterContext.ts # Main router context
288292
│ └── RouteContext.ts # Route matching context
289-
├── core/
290-
│ ├── matchRoutes.ts # Route matching logic
291-
│ └── pathPattern.ts # Path pattern parsing
292-
└── types.ts # Shared type definitions
293+
└── core/
294+
├── matchRoutes.ts # Route matching logic
295+
├── loaderCache.ts # Loader result caching
296+
├── RouterAdapter.ts # Adapter interface for navigation modes
297+
├── NavigationAPIAdapter.ts # Navigation API implementation
298+
├── StaticAdapter.ts # Fallback static mode implementation
299+
├── NullAdapter.ts # Null adapter (no-op)
300+
└── createAdapter.ts # Adapter factory
293301
```
294302

295303
## Browser Support
@@ -298,19 +306,15 @@ The Navigation API is supported in:
298306

299307
- Chrome 102+
300308
- Edge 102+
309+
- Safari 26.2+
301310
- Opera 88+
302311

303-
For unsupported browsers (Firefox, Safari), a polyfill or fallback strategy will be needed. Options:
312+
Firefox does not yet support the Navigation API.
304313

305-
1. Use the `navigation-api-polyfill` package
306-
2. Provide a History API fallback mode
307-
3. Require polyfill as peer dependency
308-
309-
**Initial implementation will target Navigation API-supporting browsers only.**
314+
For unsupported browsers, use the `fallback="static"` option on the Router component, which renders matched routes without SPA navigation capabilities (links cause full page loads).
310315

311316
## Future Considerations
312317

313-
- **Data loading**: Route-based data fetching (loaders)
314318
- **Pending UI**: Navigation state for loading indicators
315319
- **View Transitions**: Integration with View Transitions API
316320
- **Scroll restoration**: Automatic scroll position management

0 commit comments

Comments
 (0)