Skip to content

Commit ad00f21

Browse files
authored
Merge pull request #11100 from marmelab/tuto-react-router-v7
[Doc] Add tutorial for React Router Framework
2 parents 8d3f662 + cfabe20 commit ad00f21

File tree

3 files changed

+278
-28
lines changed

3 files changed

+278
-28
lines changed

docs/ReactRouterFramework.md

Lines changed: 250 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
1+
---
2+
layout: default
3+
title: "React Router Framework Integration"
4+
---
5+
6+
# React Router Framework Integration
7+
8+
[React Router Framework](https://reactrouter.com/start/framework/installation) (a.k.a. Remix v3) is a Node.js framework for server-side-rendered React apps. React-admin uses React Router under the hood and integrates seamlessly with React Router Framework applications.
9+
10+
These instructions are targeting React Router v7 in Framework mode.
11+
12+
## Setting Up React Router
13+
14+
Let's start by creating a new React Router project. Run the following command:
15+
16+
```sh
17+
npx create-react-router@latest
18+
```
19+
20+
This script will ask you for more details about your project. You can use the following options:
21+
22+
- The name you want to give to your project, e.g. `react-router-admin`
23+
- Initialize a new git repository? Choose Yes
24+
- Install dependencies with npm? Choose Yes
25+
26+
## Setting Up React-Admin In React Router
27+
28+
Next, add the required dependencies. In addition to the `react-admin` npm package, you will need a data provider package. In this example, we'll use `ra-data-json-server` to connect to a test API provided by [JSONPlaceholder](https://jsonplaceholder.typicode.com).
29+
30+
`react-admin` also depends on the `react-router-dom` package. It used to be a direct dependency of `react-router`, but it's not anymore in v7 so you'll have to add it manually. Check the version of React Router that has been installed by `create-react-router` and **use the exact same version**. At the time of writing this tutorial, it is `7.10.1`.
31+
32+
```sh
33+
cd react-router-admin
34+
npm add react-admin ra-data-json-server [email protected]
35+
```
36+
37+
## Adding React-Admin In A Sub Route
38+
39+
In many cases, the admin is only a part of the application. For instance, you may want to render the admin in a subpath like `/admin`.
40+
41+
To do so, add a route for all `/admin` subpath in the `app/routes.ts` file:
42+
43+
```jsx
44+
import { type RouteConfig, index, route } from "@react-router/dev/routes";
45+
46+
export default [
47+
index("routes/home.tsx"),
48+
route("/admin/*", "routes/admin.tsx"),
49+
] satisfies RouteConfig;
50+
```
51+
52+
Now create the `app/routes/admin.tsx` file:
53+
54+
```tsx
55+
import { Admin, Resource, ListGuesser } from "react-admin";
56+
import jsonServerProvider from "ra-data-json-server";
57+
58+
const dataProvider = jsonServerProvider("https://jsonplaceholder.typicode.com");
59+
60+
export default function App() {
61+
return (
62+
<Admin basename="/admin" dataProvider={dataProvider}>
63+
<Resource name="posts" list={ListGuesser} />
64+
<Resource name="comments" list={ListGuesser} />
65+
</Admin>
66+
);
67+
}
68+
```
69+
70+
**Tip**: Don't forget to set the `<Admin basename>` prop, so that react-admin generates links relative to the "/admin/" subpath:
71+
72+
You can now start the app in development mode with `npm run dev`. The admin should render at <http://localhost:5173/admin/>.
73+
74+
**Tip**: If you're getting a `ReferenceError: document is not defined`error at this stage, it's probably because the versions of `react-router` and `react-router-dom` are mismatched. Make sure to use the exact same version for both packages.
75+
76+
## Adding an API
77+
78+
[React Router allows to serve an API](https://reactrouter.com/how-to/resource-routes) from the same server. You *could* use this to build a CRUD API by hand. However, we consider that building a CRUD API on top of a relational database is a solved problem and that developers shouldn't spend time reimplementing it.
79+
80+
For instance, if you store your data in a [PostgreSQL](https://www.postgresql.org/) database, you can use [PostgREST](https://postgrest.org/en/stable/) to expose the data as a REST API with zero configuration. Even better, you can use a Software-as-a-Service like [Supabase](https://supabase.com/) to do that for you.
81+
82+
In such cases, the React Router API can only serve as a Proxy to authenticate client queries and pass them down to Supabase.
83+
84+
Let's see an example in practice.
85+
86+
First, create a Supabase REST API and its associated PostgreSQL database directly on the [Supabase website](https://app.supabase.com/) (it's free for tests and low usage). Once the setup is finished, use the Supabase manager to add the following tables:
87+
88+
- `posts` with fields: `id`, `title`, and `body`
89+
- `comments` with fields: `id`, `name`, `body`, and `postId` (a foreign key to the `posts.id` field)
90+
91+
You can populate these tables via the Supabse UI if you want.
92+
93+
Supabase exposes a REST API at `https://YOUR_INSTANCE.supabase.co/rest/v1`.
94+
95+
Next, create a configuration to let the React-Router app connect to Supabase. As React Router supports [`dotenv`](https://dotenv.org/) by default in `development` mode, you just need to create a `.env` file:
96+
97+
```sh
98+
# In `.env`
99+
SUPABASE_URL="https://MY_INSTANCE.supabase.co"
100+
SUPABASE_SERVICE_ROLE="MY_SERVICE_ROLE_KEY"
101+
```
102+
103+
**Tip**: This example uses the **service role key** here and not the anonymous role. This allows mutations without dealing with authorization. **You shouldn't do this in production**, but use the [Supabase authorization](https://supabase.com/docs/guides/auth) feature instead.
104+
105+
Time to bootstrap the API Proxy. Create a new route in `app/routes.ts`:
106+
107+
```ts
108+
import { type RouteConfig, index, route } from "@react-router/dev/routes";
109+
110+
export default [
111+
index("routes/home.tsx"),
112+
route("/admin/*", "routes/admin.tsx"),
113+
route("/admin/api/*", "routes/admin.api.tsx"),
114+
] satisfies RouteConfig;
115+
```
116+
117+
Then create the `app/routes/admin.api.tsx` file. Inside this file, a `loader` function should convert the GET requests into Supabase API calls, and an `action` function should do the same for POST, PUT, and DELETE requests.
118+
119+
```tsx
120+
// in app/routes/admin.api.tsx
121+
import type { Route } from "./+types/admin.api";
122+
123+
// handle read requests (getOne, getList, getMany, getManyReference)
124+
export const loader = ({ request }: Route.LoaderArgs) => {
125+
const apiUrl = getSupabaseUrlFromRequestUrl(request.url);
126+
127+
return fetch(apiUrl, {
128+
headers: {
129+
prefer: request.headers.get("prefer") ?? "",
130+
accept: request.headers.get("accept") ?? "application/json",
131+
"Accept-Encoding": "",
132+
apiKey: `${process.env.SUPABASE_SERVICE_ROLE}`,
133+
Authorization: `Bearer ${process.env.SUPABASE_SERVICE_ROLE}`,
134+
},
135+
});
136+
};
137+
138+
// handle write requests (create, update, delete, updateMany, deleteMany)
139+
export const action = ({ request }: Route.ActionArgs) => {
140+
const apiUrl = getSupabaseUrlFromRequestUrl(request.url);
141+
142+
return fetch(apiUrl, {
143+
method: request.method,
144+
body: request.body,
145+
// @ts-expect-error The types for fetch don't support duplex but it is required and works
146+
duplex: "half",
147+
headers: {
148+
prefer: request.headers.get("prefer") ?? "",
149+
accept: request.headers.get("accept") ?? "application/json",
150+
"Accept-Encoding": "",
151+
apiKey: `${process.env.SUPABASE_SERVICE_ROLE}`,
152+
Authorization: `Bearer ${process.env.SUPABASE_SERVICE_ROLE}`,
153+
},
154+
});
155+
};
156+
157+
const ADMIN_PREFIX = "/admin/api";
158+
159+
const getSupabaseUrlFromRequestUrl = (url: string) => {
160+
const startOfRequest = url.indexOf(ADMIN_PREFIX);
161+
const query = url.substring(startOfRequest + ADMIN_PREFIX.length);
162+
return `${process.env.SUPABASE_URL}/rest/v1${query}`;
163+
};
164+
```
165+
166+
**Tip**: Some of this code is really PostgREST-specific. The `prefer` header is required to let PostgREST return one record instead of an array containing one record in response to `getOne` requests. A proxy for another CRUD API will require different parameters.
167+
168+
Update the react-admin data provider to use the Supabase adapter instead of the JSON Server one. As Supabase provides a PostgREST endpoint, we'll use [`ra-data-postgrest`](https://github.com/raphiniert-com/ra-data-postgrest):
169+
170+
```sh
171+
npm add @raphiniert/ra-data-postgrest
172+
```
173+
174+
Update your `vite.config.ts` to add `@raphiniert/ra-data-postgrest` to the `noExternal` array:
175+
176+
```diff
177+
import { reactRouter } from "@react-router/dev/vite";
178+
import tailwindcss from "@tailwindcss/vite";
179+
import { defineConfig } from "vite";
180+
import tsconfigPaths from "vite-tsconfig-paths";
181+
182+
export default defineConfig({
183+
plugins: [tailwindcss(), reactRouter(), tsconfigPaths()],
184+
+ ssr: {
185+
+ noExternal: ['@raphiniert/ra-data-postgrest']
186+
+ },
187+
});
188+
```
189+
190+
Finally, update your Admin dataProvider:
191+
192+
```jsx
193+
// in app/routes/admin.tsx
194+
import { Admin, Resource, ListGuesser, fetchUtils } from "react-admin";
195+
import postgrestRestProvider, { defaultPrimaryKeys, defaultSchema } from '@raphiniert/ra-data-postgrest';
196+
197+
const dataProvider = postgrestRestProvider({
198+
apiUrl: '/admin/api',
199+
httpClient: fetchUtils.fetchJson,
200+
defaultListOp: 'eq',
201+
primaryKeys: defaultPrimaryKeys,
202+
schema: defaultSchema
203+
});
204+
205+
export default function App() {
206+
return (
207+
<Admin basename="/admin" dataProvider={dataProvider}>
208+
<Resource name="posts" list={ListGuesser} />
209+
<Resource name="comments" list={ListGuesser} />
210+
</Admin>
211+
);
212+
}
213+
```
214+
215+
That's it! Now React Router both renders the admin app and serves as a proxy to the Supabase API. You can test the app by visiting `http://localhost:5173/admin/`, and the API Proxy by visiting `http://localhost:5173/admin/api/posts`.
216+
217+
**Note**: You may have a blank page if your database does not have any record yet. Make sure to create some using Supabase Studio.
218+
219+
Note that the Supabase credentials never leave the server. It's up to you to add your own authentication to the API proxy.
220+
221+
## Sourcemaps in production
222+
223+
By default, Vite won't include the TypeScript sourcemaps in production builds. This means you'll only have the react-admin ESM builds for debugging.
224+
225+
Should you prefer to have the TypeScript sources, you'll have to configure some Vite aliases:
226+
227+
```tsx
228+
// in vite.config.ts
229+
import { reactRouter } from "@react-router/dev/vite";
230+
import tailwindcss from "@tailwindcss/vite";
231+
import { defineConfig } from "vite";
232+
import tsconfigPaths from "vite-tsconfig-paths";
233+
import path from "path";
234+
235+
const alias = [
236+
{ find: 'react-admin', replacement: path.resolve(__dirname, './node_modules/react-admin/src') },
237+
{ find: 'ra-core', replacement: path.resolve(__dirname, './node_modules/ra-core/src') },
238+
{ find: 'ra-ui-materialui', replacement: path.resolve(__dirname, './node_modules/ra-ui-materialui/src') },
239+
// add any other react-admin packages you have
240+
]
241+
242+
export default defineConfig({
243+
plugins: [tailwindcss(), reactRouter(), tsconfigPaths()],
244+
ssr: {
245+
noExternal: ['@raphiniert/ra-data-postgrest']
246+
},
247+
build: { sourcemap: true },
248+
resolve: { alias },
249+
});
250+
```

docs/Remix.md

Lines changed: 27 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ title: "Remix Integration"
77

88
[Remix](https://remix.run/) is a Node.js framework for server-side-rendered React apps. But even if react-admin is designed to build Single-Page Applications, Remix and react-admin integrate seamlessly.
99

10-
These instructions are targeting Remix v2.
10+
These instructions are targeting Remix v2. For Remix v3 check out the [React Router Framework Integration](ReactRouterFramework.md) guide.
1111

1212
## Setting Up Remix
1313

@@ -44,19 +44,19 @@ import { defineConfig } from "vite";
4444
import tsconfigPaths from "vite-tsconfig-paths";
4545

4646
export default defineConfig({
47-
plugins: [
48-
remix({
49-
future: {
50-
v3_fetcherPersist: true,
51-
v3_relativeSplatPath: true,
52-
v3_throwAbortReason: true,
53-
},
54-
}),
55-
tsconfigPaths(),
56-
],
57-
+ ssr: {
58-
+ noExternal: ['ra-data-json-server'] // or the dataProvider you are using
59-
+ },
47+
plugins: [
48+
remix({
49+
future: {
50+
v3_fetcherPersist: true,
51+
v3_relativeSplatPath: true,
52+
v3_throwAbortReason: true,
53+
},
54+
}),
55+
tsconfigPaths(),
56+
],
57+
+ ssr: {
58+
+ noExternal: ['ra-data-json-server'] // or the dataProvider you are using
59+
+ },
6060
});
6161
```
6262

@@ -189,26 +189,25 @@ npm add @raphiniert/ra-data-postgrest
189189

190190
Update your `vite.config.ts` to add `@raphiniert/ra-data-postgrest` to the `noExternal` array:
191191

192-
193192
```diff
194193
import { vitePlugin as remix } from "@remix-run/dev";
195194
import { defineConfig } from "vite";
196195
import tsconfigPaths from "vite-tsconfig-paths";
197196

198197
export default defineConfig({
199-
plugins: [
200-
remix({
201-
future: {
202-
v3_fetcherPersist: true,
203-
v3_relativeSplatPath: true,
204-
v3_throwAbortReason: true,
205-
},
206-
}),
207-
tsconfigPaths(),
208-
],
209-
+ ssr: {
210-
+ noExternal: ['@raphiniert/ra-data-postgrest']
211-
+ },
198+
plugins: [
199+
remix({
200+
future: {
201+
v3_fetcherPersist: true,
202+
v3_relativeSplatPath: true,
203+
v3_throwAbortReason: true,
204+
},
205+
}),
206+
tsconfigPaths(),
207+
],
208+
+ ssr: {
209+
+ noExternal: ['@raphiniert/ra-data-postgrest']
210+
+ },
212211
});
213212
```
214213

docs/navigation.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
<li {% if page.path == 'Vite.md' %} class="active" {% endif %}><a class="nav-link" href="./Vite.html">Vite</a></li>
1515
<li {% if page.path == 'NextJs.md' %} class="active" {% endif %}><a class="nav-link" href="./NextJs.html">Next.js</a></li>
1616
<li {% if page.path == 'Remix.md' %} class="active" {% endif %}><a class="nav-link" href="./Remix.html">Remix</a></li>
17+
<li {% if page.path == 'ReactRouterFramework.md' %} class="active" {% endif %}><a class="nav-link" href="./ReactRouterFramework.html">React Router Framework</a></li>
1718
<li {% if page.path == 'Deploy.md' %} class="active beginner" {% endif %}><a class="nav-link" href="./Deploy.html">Deployment</a></li>
1819
</ul>
1920

0 commit comments

Comments
 (0)