Skip to content

Commit 6515a81

Browse files
committed
useRouter and children updated
1 parent 51d25cf commit 6515a81

File tree

5 files changed

+184
-23
lines changed

5 files changed

+184
-23
lines changed

README.md

Lines changed: 58 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ Supports:
1313
- **Dynamic routes**
1414
- **Error boundary**
1515
- **404 page**
16+
- **Loader support for data fetching**
1617

1718
Built for **React Router DOM** + **Vite** — simple, fast, familiar.
1819

@@ -27,6 +28,7 @@ Built for **React Router DOM** + **Vite** — simple, fast, familiar.
2728
✅ Dynamic routes with `[slug]`, `[...slug]`, `[[slug]]`\
2829
✅ Error boundaries via `error.jsx`\
2930
✅ 404 Not Found handling with `404.jsx`\
31+
✅ Loader support for data fetching `loader.jsx`\
3032
✅ Fully type-safe (TypeScript supported)
3133

3234
---
@@ -36,7 +38,6 @@ Try it on StackBlitz:
3638

3739
[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/edit/react-next-router-example?file=src%2Fmain.jsx)
3840

39-
4041
---
4142

4243
## 📦 Install
@@ -58,38 +59,57 @@ src/
5859
│ └── page.jsx # '/about'
5960
├── blog/
6061
│ ├── [slug]/
61-
│ │ └── page.jsx # '/blog/:slug'
62+
│ │ ├── page.jsx # '/blog/:slug'
63+
│ │ └── loader.jsx # Loader for data fetching
6264
│ └── layout.jsx # Layout for '/blog/*'
6365
├── (admin)/
6466
│ ├── dashboard/
6567
│ │ └── page.jsx # '/dashboard'
6668
│ └── layout.jsx # Layout for group
67-
├── error.jsx # Error boundary
68-
└── 404.jsx # Not Found page
69+
├── error.jsx # Error boundary
70+
├── 404.jsx # Not Found page
71+
├── pending.jsx # Pending component (renders while loading)
6972
```
7073

74+
75+
> You can `loader.jsx` alongside any `page.jsx` to fetch data before rendering the page.
76+
> Add a `app/pending.jsx` file to show a loading UI while the loader is running.
77+
7178
---
7279

7380
## 🚀 Usage
81+
7482
Example `src/app/page.jsx`:
7583
```jsx
76-
export default function Home() {
77-
return <h1>Home Page</h1>;
84+
export default function Home({ data }) {
85+
return <h1>Home Page {data && <span>{data.message}</span>}</h1>;
7886
}
7987
```
88+
8089
Example `src/app/layout.jsx`:
8190
```jsx
8291
export default function RootLayout({ children }) {
8392
return (
8493
<div>
8594
<header>Header Content</header>
86-
<main><Outlet></main>
95+
<main>{children}</main>
8796
</div>
8897
);
8998
}
9099
```
91-
Example `src/App.jsx`:
92100

101+
Example `src/app/loader.jsx`:
102+
```js
103+
// This loader runs before the sibling page.jsx and its return value is passed as the 'data' prop
104+
export default async function loader() {
105+
// You can fetch from any API or return any data
106+
const res = await fetch('https://api.example.com/message');
107+
const data = await res.json();
108+
return { message: data.message };
109+
}
110+
```
111+
112+
Example `src/App.jsx`:
93113
```jsx
94114
import { AppRouter } from "react-next-router";
95115

@@ -112,6 +132,22 @@ export default App;
112132
| `app/blog/[slug]/page.jsx` | `/blog/:slug` |
113133
| `app/blog/[...slug]/page.jsx` | `/blog/*` (catch-all) |
114134
| `app/blog/[[slug]]/page.jsx` | `/blog` (optional param) |
135+
| `app/blog/[slug]/loader.jsx` | Data loader for `/blog/:slug` |
136+
| `app/pending.jsx` | Loading UI while data is fetched |
137+
138+
139+
## 🧪 useAppRouter Hook
140+
You can now use the useAppRouter() hook to get a JSON structure of all matched routes. This is useful when you want to inspect or manipulate the route config manually — for example, inside a custom RouterProvider or createBrowserRouter setup.
141+
142+
```jsx
143+
import { useAppRouter } from "react-next-router";
144+
145+
const MyComponent = () => {
146+
const router = useAppRouter();
147+
console.log(router);
148+
return <div>Check the console for the matched routes!</div>;
149+
};
150+
```
115151

116152
---
117153

@@ -166,6 +202,20 @@ app/
166202

167203
---
168204

205+
## 🛠️ Loaders (Data Fetching)
206+
207+
Add a `loader.jsx` file alongside any `page.jsx` to fetch data before rendering the page. The returned value will be passed as the `data` prop to the sibling `page.jsx` component.
208+
209+
Example:
210+
```
211+
app/
212+
└── about/
213+
├── page.jsx
214+
└── loader.jsx
215+
```
216+
217+
---
218+
169219
## 📝 License
170220

171221
MIT

src/components/appRouter.tsx

Lines changed: 40 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,19 +8,32 @@ import {
88
} from "react-router-dom";
99
import { NotFoundComponent } from "./notFound";
1010
import { ErrorComponent } from "./error";
11+
import LayoutComponent from "./layout";
12+
import PageWithLoader from "./pageWithLoader";
1113

1214
type Module = { default: React.FC };
1315
const basePath = "/src/app";
1416

17+
//importing routes from app
1518
const notFoundRoute = import.meta.glob(`/src/app/404.{jsx,tsx}`, {
1619
eager: true,
1720
}) as Record<string, Module>;
1821
const errorRoute = import.meta.glob(`/src/app/error.{jsx,tsx}`, {
1922
eager: true,
2023
}) as Record<string, Module>;
21-
const routes = import.meta.glob(`/src/app/**/(page|layout).{jsx,tsx}`, {
24+
const pendingRoute = import.meta.glob(`/src/app/pending.{jsx,tsx}`, {
2225
eager: true,
2326
}) as Record<string, Module>;
27+
const routes = import.meta.glob(`/src/app/**/(page|layout|loader).{jsx,tsx}`, {
28+
eager: true,
29+
}) as Record<string, Module>;
30+
31+
//other routes
32+
const NotFound =
33+
Object.values(notFoundRoute)?.[0]?.default ?? NotFoundComponent;
34+
const ErrorElement = Object.values(errorRoute)?.[0]?.default ?? ErrorComponent;
35+
const PendingComponent =
36+
Object.values(pendingRoute)?.[0]?.default ?? NotFoundComponent;
2437

2538
const recursiveRoutes = (
2639
routePath: string[],
@@ -43,7 +56,14 @@ const recursiveRoutes = (
4356
}
4457

4558
if (routePath[1] === "layout") {
46-
acc[matchedIndex]["Component"] = Component.default;
59+
acc[matchedIndex]["Component"] = () => (
60+
<LayoutComponent Component={Component.default} />
61+
);
62+
return;
63+
}
64+
65+
if (routePath[1] === "loader") {
66+
acc[matchedIndex]["loader"] = Component.default;
4767
return;
4868
}
4969

@@ -55,13 +75,19 @@ const recursiveRoutes = (
5575
Component
5676
);
5777
} else {
78+
const RouterComponent = () => (
79+
<PageWithLoader
80+
Component={Component.default}
81+
LoadingComponent={PendingComponent}
82+
/>
83+
);
5884
if (acc[matchedIndex]?.["children"]) {
5985
acc[matchedIndex]["children"]?.push({
6086
index: true,
61-
Component: Component.default,
87+
Component: RouterComponent,
6288
});
6389
} else {
64-
acc[matchedIndex]["Component"] = Component.default;
90+
acc[matchedIndex]["Component"] = RouterComponent;
6591
}
6692
}
6793
};
@@ -85,25 +111,24 @@ const allRoutes = Object.entries(routes).reduce(
85111
[]
86112
);
87113

88-
export const AppRouter = ({ router = createBrowserRouter }) => {
89-
const NotFound =
90-
Object.values(notFoundRoute)?.[0]?.default ?? NotFoundComponent;
91-
const ErrorElement =
92-
Object.values(errorRoute)?.[0]?.default ?? ErrorComponent;
93-
const catchAllRoute = {
94-
path: "*",
95-
Component: NotFound,
96-
ErrorBoundary: ErrorElement,
97-
};
114+
const catchAllRoute = {
115+
path: "*",
116+
Component: NotFound,
117+
ErrorBoundary: ErrorElement,
118+
};
98119

120+
export const useAppRouter = () => {
99121
allRoutes.forEach((element: RouteObject) => {
100122
if (element.path === "/") {
101123
element.ErrorBoundary = ErrorElement;
102124
element?.children?.push(catchAllRoute);
103125
}
104126
});
105-
const elements = [...allRoutes, catchAllRoute] satisfies RouteObject[];
127+
return allRoutes satisfies RouteObject[];
128+
};
106129

130+
export const AppRouter = ({ router = createBrowserRouter }) => {
131+
const elements = useAppRouter();
107132
return <RouterProvider router={router(elements)} />;
108133
};
109134

src/components/layout.tsx

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import React, { PropsWithChildren } from "react";
2+
import { OutletProps, Outlet } from "react-router-dom";
3+
4+
/**
5+
* A special component that wraps a Layout component with an
6+
* `<Outlet />` so that nested routes can be rendered inside the
7+
* layout.
8+
*
9+
* @example
10+
* // app/layout.jsx
11+
* import { LayoutComponent } from "react-next-router";
12+
*
13+
* export default function Layout({children}) {
14+
* return (
15+
* <div>
16+
* <h1>My Layout</h1>
17+
* <LayoutComponent>
18+
* {children}
19+
* </LayoutComponent>
20+
* </div>
21+
* );
22+
* }
23+
*/
24+
const LayoutComponent = ({
25+
Component,
26+
}: {
27+
Component: React.FC<PropsWithChildren<OutletProps>>;
28+
}) => {
29+
return (
30+
<Component>
31+
<Outlet />
32+
</Component>
33+
);
34+
};
35+
36+
export default LayoutComponent;

src/components/pageWithLoader.tsx

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import React, { FC, PropsWithChildren } from "react";
2+
import { useLoaderData, useNavigation, LoaderFunction } from "react-router-dom";
3+
4+
type PageWithLoaderProps = {
5+
Component: React.FC<PropsWithChildren<{ data: any }>>;
6+
LoadingComponent: React.FC;
7+
};
8+
/**
9+
* A special component that wraps a Page component with a
10+
* loader. It waits for the data to be fetched and then
11+
* renders the Page component with the fetched data.
12+
*
13+
* @example
14+
15+
* // app/loader.jsx
16+
* export const loader: LoaderFunction = async () => {
17+
* // fetch data
18+
* return data;
19+
* }
20+
*
21+
* // app/page.jsx
22+
* export default function MyPage({data}) {
23+
* return (
24+
* <div>
25+
* <h2>My Page</h2>
26+
* <p>{data.message}</p>
27+
* </div>
28+
* );
29+
* }
30+
*
31+
* // app/pending.jsx
32+
* export default function MyLoadingComponent() {
33+
* return <div>Loading...</div>;
34+
* }
35+
*/
36+
const PageWithLoader: FC<PageWithLoaderProps> = ({
37+
Component,
38+
LoadingComponent,
39+
}) => {
40+
const navigation = useNavigation();
41+
const loaderData: any = useLoaderData();
42+
return navigation.state === "loading" ? (
43+
<LoadingComponent />
44+
) : (
45+
<Component data={loaderData} />
46+
);
47+
};
48+
49+
export default PageWithLoader;

src/index.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
import AppRouter from "./components/appRouter";
2+
export * from "./components/appRouter";
23
export default AppRouter;

0 commit comments

Comments
 (0)