Skip to content

Commit 0efa42d

Browse files
committed
[Doc] Add tutorial for React Router Framework
1 parent 8d3f662 commit 0efa42d

File tree

2 files changed

+250
-0
lines changed

2 files changed

+250
-0
lines changed

docs/ReactRouterFramework.md

Lines changed: 249 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,249 @@
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) is a Node.js framework for server-side-rendered React apps. But even if react-admin is designed to build Single-Page Applications, it uses React Router under the hood and integrates seamlessly.
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+
Add the `react-admin` npm package, as well as 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+
**Note**: `react-admin` requires the `react-router-dom` package which is not needed anymore in standard React Router applications but is still published for backward compatibility. Check the version of React Router that has been installed by `create-react-router` and use the 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 [email protected] react-admin ra-data-json-server
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 `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/`, and you can use the Remix routing system to add more pages.
73+
74+
## Adding an API
75+
76+
[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.
77+
78+
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.
79+
80+
In such cases, the React Router API can only serve as a Proxy to authenticate client queries and pass them down to Supabase.
81+
82+
Let's see an example in practice.
83+
84+
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:
85+
86+
- `posts` with fields: `id`, `title`, and `body`
87+
- `comments` with fields: `id`, `name`, `body`, and `postId` (a foreign key to the `posts.id` field)
88+
89+
You can populate these tables via the Supabse UI if you want. Supabase exposes a REST API at `https://YOUR_INSTANCE.supabase.co/rest/v1`.
90+
91+
Next, create a configuration to let the Remix app connect to Supabase. As Remix supports [`dotenv`](https://dotenv.org/) by default in `development` mode, you just need to create a `.env` file:
92+
93+
```sh
94+
# In `.env`
95+
SUPABASE_URL="https://MY_INSTANCE.supabase.co"
96+
SUPABASE_SERVICE_ROLE="MY_SERVICE_ROLE_KEY"
97+
```
98+
99+
**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.
100+
101+
Time to bootstrap the API Proxy. Create a new route in `app/routes.ts`:
102+
103+
```ts
104+
import { type RouteConfig, index, route } from "@react-router/dev/routes";
105+
106+
export default [
107+
index("routes/home.tsx"),
108+
route("/admin/*", "routes/admin.tsx"),
109+
route("/admin/api/*", "routes/admin.api.tsx"),
110+
] satisfies RouteConfig;
111+
```
112+
113+
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.
114+
115+
```tsx
116+
// in /app/routes/admin.api.$.tsx
117+
import type { Route } from "./+types/admin.api";
118+
119+
// handle read requests (getOne, getList, getMany, getManyReference)
120+
export const loader = ({ request }: Route.LoaderArgs) => {
121+
const apiUrl = getSupabaseUrlFromRequestUrl(request.url);
122+
123+
return fetch(apiUrl, {
124+
headers: {
125+
prefer: request.headers.get("prefer") ?? "",
126+
accept: request.headers.get("accept") ?? "application/json",
127+
"Accept-Encoding": "",
128+
apiKey: `${process.env.SUPABASE_SERVICE_ROLE}`,
129+
Authorization: `Bearer ${process.env.SUPABASE_SERVICE_ROLE}`,
130+
},
131+
});
132+
};
133+
134+
// handle write requests (create, update, delete, updateMany, deleteMany)
135+
export const action = ({ request }: Route.ActionArgs) => {
136+
const apiUrl = getSupabaseUrlFromRequestUrl(request.url);
137+
138+
return fetch(apiUrl, {
139+
method: request.method,
140+
body: request.body,
141+
// @ts-expect-error The types for fetch don't support duplex but it is required and works
142+
duplex: "half",
143+
headers: {
144+
prefer: request.headers.get("prefer") ?? "",
145+
accept: request.headers.get("accept") ?? "application/json",
146+
"Accept-Encoding": "",
147+
apiKey: `${process.env.SUPABASE_SERVICE_ROLE}`,
148+
Authorization: `Bearer ${process.env.SUPABASE_SERVICE_ROLE}`,
149+
},
150+
});
151+
};
152+
153+
const ADMIN_PREFIX = "/admin/api";
154+
155+
const getSupabaseUrlFromRequestUrl = (url: string) => {
156+
const startOfRequest = url.indexOf(ADMIN_PREFIX);
157+
const query = url.substring(startOfRequest + ADMIN_PREFIX.length);
158+
return `${process.env.SUPABASE_URL}/rest/v1${query}`;
159+
};
160+
```
161+
162+
**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.
163+
164+
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):
165+
166+
```sh
167+
npm add @raphiniert/ra-data-postgrest
168+
```
169+
170+
Update your `vite.config.ts` to add `@raphiniert/ra-data-postgrest` to the `noExternal` array:
171+
172+
173+
```diff
174+
import { vitePlugin as remix } from "@remix-run/dev";
175+
import { defineConfig } from "vite";
176+
import tsconfigPaths from "vite-tsconfig-paths";
177+
178+
export default defineConfig({
179+
plugins: [
180+
remix({
181+
future: {
182+
v3_fetcherPersist: true,
183+
v3_relativeSplatPath: true,
184+
v3_throwAbortReason: true,
185+
},
186+
}),
187+
tsconfigPaths(),
188+
],
189+
+ ssr: {
190+
+ noExternal: ['@raphiniert/ra-data-postgrest']
191+
+ },
192+
});
193+
```
194+
195+
Finally, update your Admin dataProvider:
196+
197+
```jsx
198+
// in app/routes/admin.$.tsx
199+
import { Admin, Resource, ListGuesser, fetchUtils } from "react-admin";
200+
import postgrestRestProvider, { defaultPrimaryKeys, defaultSchema } from '@raphiniert/ra-data-postgrest';
201+
202+
const dataProvider = postgrestRestProvider({
203+
apiUrl: '/admin/api',
204+
httpClient: fetchUtils.fetchJson,
205+
defaultListOp: 'eq',
206+
primaryKeys: defaultPrimaryKeys,
207+
schema: defaultSchema
208+
});
209+
210+
export default function App() {
211+
return (
212+
<Admin basename="/admin" dataProvider={dataProvider}>
213+
<Resource name="posts" list={ListGuesser} />
214+
<Resource name="comments" list={ListGuesser} />
215+
</Admin>
216+
);
217+
}
218+
```
219+
220+
That's it! Now Remix 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`.
221+
222+
**Note**: you may have a blank page if your database does not have any record yet. Make sure to create some using Supabase Studio.
223+
224+
Note that the Supabase credentials never leave the server. It's up to you to add your own authentication to the API proxy.
225+
226+
## Sourcemaps in production
227+
228+
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.
229+
Should you prefer to have the TypeScript sources, you'll have to configure some Vite aliases:
230+
231+
```tsx
232+
// in vite.config.ts
233+
import { defineConfig } from "vite";
234+
import path from "path";
235+
import react from "@vitejs/plugin-react";
236+
237+
const alias = [
238+
{ find: 'react-admin', replacement: path.resolve(__dirname, './node_modules/react-admin/src') },
239+
{ find: 'ra-core', replacement: path.resolve(__dirname, './node_modules/ra-core/src') },
240+
{ find: 'ra-ui-materialui', replacement: path.resolve(__dirname, './node_modules/ra-ui-materialui/src') },
241+
// add any other react-admin packages you have
242+
]
243+
244+
export default defineConfig({
245+
plugins: [react()],
246+
build: { sourcemap: true },
247+
resolve: { alias },
248+
});
249+
```

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)