Skip to content

Commit

Permalink
Merge pull request #7921 from marmelab/remix
Browse files Browse the repository at this point in the history
[Doc] Add installation instructions for CRA, Next.js and Remix
  • Loading branch information
djhi authored Jul 5, 2022
2 parents 0a91419 + 768e9e3 commit 4223623
Show file tree
Hide file tree
Showing 6 changed files with 331 additions and 8 deletions.
68 changes: 68 additions & 0 deletions docs/CreateReactApp.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
---
layout: default
title: "Create_React-App Integration"
---

# Create-React-App Integration

[Create-React-App](https://create-react-app.dev/) is the standard way to bootstrap single-page React applications. That's also the recommended way to install and run react-admin.

## Setting Up Create React App

Create a new Create React App (CRA) project with the command line:

```sh
yarn create react-app my-admin
```

We recommend using the TypeScript template:

```sh
yarn create react-app my-admin --template typescript
```

## Setting Up React-Admin

Add the `react-admin` 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).

```sh
cd my-admin
yarn add react-admin ra-data-json-server
```

Next, create the admin app component in `src/admin/index.tsx`:

```jsx
// in src/admin/index.tsx
import { Admin, Resource, ListGuesser } from "react-admin";
import jsonServerProvider from "ra-data-json-server";

const dataProvider = jsonServerProvider("https://jsonplaceholder.typicode.com");

const App = () => (
<Admin dataProvider={dataProvider}>
<Resource name="posts" list={ListGuesser} />
<Resource name="comments" list={ListGuesser} />
</Admin>
);

export default App;
```

This is a minimal admin for 2 resources. React-admin should be able to render a list of posts and a list of comments, guessing the data structure from the API response.

Next, replace the `App.tsx` component with the following:

```jsx
import MyAdmin from "./admin";

const App = () => <MyAdmin />;

export default App;
```

Now, start the server with `yarn start`, browse to `http://localhost:3000/`, and you should see the working admin:

![Working Page](./img/nextjs-react-admin.webp)

Your app is now up and running, you can start tweaking it.
10 changes: 5 additions & 5 deletions docs/NextJs.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ const Home: NextPage = () => {
export default Home;
```

**Tip**: Why the dynamic import? React-admin is designed as a Single-Page Application, rendered on the client side. It comes with its own [routing sytem](./Routing.md), which conflicts with the Next.js routing system. So we must prevent Next.js from rendering the react-admin component on the server-side. Using `dynamic` allows to disable Server-Side Rendering for the `<App>` component.
**Tip**: Why the dynamic import? React-admin is designed as a Single-Page Application, rendered on the client-side. It comes with its own [routing sytem](./Routing.md), which conflicts with the Next.js routing system. So we must prevent Next.js from rendering the react-admin component on the server-side. Using `dynamic` allows disabling Server-Side Rendering for the `<App>` component.

Now, start the server with `yarn dev`, browse to `http://localhost:3000/`, and you should see the working admin:

Expand Down Expand Up @@ -99,11 +99,11 @@ Now the admin renders at `http://localhost:3000/admin`, and you can use the Next

## Adding an API

[Next.js allows to serve an API](https://nextjs.org/docs/api-routes/introduction) 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 reimplemeting it.
[Next.js allows to serve an API](https://nextjs.org/docs/api-routes/introduction) 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.

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.

In such cases, the Next.js API can only serve as a Proxy to authenticate client queries, and pass them down to Supabase.
In such cases, the Next.js API can only serve as a Proxy to authenticate client queries and pass them down to Supabase.

Let's see an example in practice.

Expand All @@ -122,9 +122,9 @@ SUPABASE_URL="https://MY_INSTANCE.supabase.co"
SUPABASE_SERVICE_ROLE="MY_SERVICE_ROLE_KEY"
```

**Tip**: This example uses the **service role key** here and not the anonymous role. This allows write operations without dealing with authorization. **You shouldn't do this in production**, but use the [Supabase authorization](https://supabase.com/docs/guides/auth) feature instead.
**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.

Create [a "catch all" API route](https://nextjs.org/docs/api-routes/dynamic-api-routes#optional-catch-all-api-routes) in the Next.js app by adding a `pages/api/admin/[[...slug]].ts` file. This API route redirects all calls from the react-admin app to the Supabase CRUD API:
Create [a "catch-all" API route](https://nextjs.org/docs/api-routes/dynamic-api-routes#optional-catch-all-api-routes) in the Next.js app by adding a `pages/api/admin/[[...slug]].ts` file. This API route redirects all calls from the react-admin app to the Supabase CRUD API:

```jsx
// in pages/api/admin/[[...slug]].ts
Expand Down
249 changes: 249 additions & 0 deletions docs/Remix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
---
layout: default
title: "Remix Integration"
---

# Remix Integration

[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.

## Setting Up Remix

Let's start by creating a new Remix project. Run the following command:

```sh
npx create-remix@latest
```

This script will ask you for more details about your project. You can use the following options:

- The name you want to give to your project, e.g. `remix-supabase-react-admin`
- "Just the basics"
- "Remix App Server"
- "TypeScript"
- "Don't run npm install"

The project structure should look something like this:

![Remix project structure](./img/remix-structure.png)

## Setting Up React-Admin

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).

```sh
cd remix-supabase-react-admin
yarn add react-admin ra-data-json-server
```

Next, create the admin app component in `app/components/App.tsx`:

```jsx
// in app/components/App.tsx
import { Admin, Resource, ListGuesser } from "react-admin";
import jsonServerProvider from "ra-data-json-server";

const dataProvider = jsonServerProvider("https://jsonplaceholder.typicode.com");

const App = () => (
<Admin dataProvider={dataProvider}>
<Resource name="posts" list={ListGuesser} />
<Resource name="comments" list={ListGuesser} />
</Admin>
);

export default App;
```

This is a minimal admin for 2 resources. React-admin should be able to render a list of posts and a list of comments, guessing the data structure from the API response.

## Using React-Admin As The Root Application

If you want to serve the admin app component in the root path ('/'), edit the file called `routes/index.tsx`, and replace the content with the following:

```jsx
// in app/routes/index.tsx
import App from "../components/App";
import styles from "~/styles/app.css";

export function links() {
return [{ rel: "stylesheet", href: styles }];
}

export default App;
```

The stylesheet link is necessary to reset the default styles of the admin app. Create it in `app/styles/app.css`:

```css
body { margin: 0; }
```

Remix and react-admin both use [react-router](https://reactrouter.com/) for routing. React-admin detects when it is included inside an existing React Router context and reuses it. This is problematic because Remix uses file-based routing. So when react-admin changes the route to `/posts` for instance, Remix will look for a corresponding `app/routes/posts.tsx` file. As it doesn't exist, Remix will render a 404.

The solution is to create a [splat route](https://remix.run/docs/en/v1/api/conventions#splat-routes), i.e. a route that matches all URLs. A splat route is named `$.tsx`. Duplicate the `app/routes/index.tsx` code into the `app/routes/$.tsx` file:

```jsx
// in app/routes/$.tsx
import App from "../components/App";
import styles from "~/styles/app.css";

export function links() {
return [{ rel: "stylesheet", href: styles }];
}

export default App;
```

**Tip**: Remix doesn't let splat routes catch requests to the index page ('/'), so you must have both the `app/routes/index.tsx` and `app/routes/$.tsx` routes to correctly render the admin app.

Now, start the server with `yarn dev`, browse to `http://localhost:3000/`, and you should see the working admin:

![Working Page](./img/nextjs-react-admin.webp)

## Rendering React-Admin In A Sub Route

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`.

To do so, add a [splat route](https://remix.run/docs/en/v1/api/conventions#splat-routes), i.e. a route that matches all URLs inside a sub path. A splat route is named `$.tsx`. Create a file called `app/routes/admin/$.tsx` file with the following content:

```jsx
// in app/routes/$.tsx
import App from "../../components/App";
import styles from "~/styles/app.css";

export function links() {
return [{ rel: "stylesheet", href: styles }];
}

export default App;
```

The stylesheet link is necessary to reset the default styles of the admin app. Create it in `app/styles/app.css`:

```css
body { margin: 0; }
```

And finally, update the react-admin app to specify the `<Admin basename>` prop, so that react-admin generates links relative to the "/admin" subpath:

```diff
// in app/components/App.tsx
import { Admin, Resource, ListGuesser } from "react-admin";
import jsonServerProvider from "ra-data-json-server";

const dataProvider = jsonServerProvider("https://jsonplaceholder.typicode.com");

const App = () => (
- <Admin dataProvider={dataProvider}>
+ <Admin basename="/admin" dataProvider={dataProvider}>
<Resource name="posts" list={ListGuesser} />
<Resource name="comments" list={ListGuesser} />
</Admin>
);

export default App;
```

Now the admin renders at `http://localhost:3000/admin`, and you can use the Remix routing system to add more pages.

## Adding an API

[Remix allows to serve an API](https://remix.run/docs/en/v1/guides/api-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.

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.

In such cases, the Remix API can only serve as a Proxy to authenticate client queries and pass them down to Supabase.

Let's see an example in practice.

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:

- `posts` with fields: `id`, `title`, and `body`
- `comments` with fields: `id`, `name`, `body`, and `postId` (a foreign key to the `posts.id` field)

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`.

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:

```sh
# In `.env`
SUPABASE_URL="https://MY_INSTANCE.supabase.co"
SUPABASE_SERVICE_ROLE="MY_SERVICE_ROLE_KEY"
```

**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.

Time to bootstrap the API Proxy. Create a new Remix route at `app/routes/admin/api/$.tsx`. 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.

```jsx
// in app/routes/admin/api/$.tsx
import type { ActionFunction, LoaderFunction } from '@remix-run/node';

// handle read requests (getOne, getList, getMany, getManyReference)
export const loader: LoaderFunction = ({ request }) => {
const apiUrl = getSupabaseUrlFromRequestUrl(request.url);

return fetch(apiUrl, {
headers: {
prefer: request.headers.get('prefer') ?? '',
accept: request.headers.get('accept') ?? 'application/json',
apiKey: `${process.env.SUPABASE_SERVICE_ROLE}`,
Authorization: `Bearer ${process.env.SUPABASE_SERVICE_ROLE}`,
},
});
};

// handle write requests (create, update, delete, updateMany, deleteMany)
export const action: ActionFunction = ({ request }) => {
const apiUrl = getSupabaseUrlFromRequestUrl(request.url);

return fetch(apiUrl, {
method: request.method,
body: request.body,
headers: {
prefer: request.headers.get('prefer') ?? '',
accept: request.headers.get('accept') ?? 'application/json',
'apiKey': `${process.env.SUPABASE_SERVICE_ROLE}`,
'Authorization': `Bearer ${process.env.SUPABASE_SERVICE_ROLE}`,
}
});
}

const ADMIN_PREFIX = "/admin/api";

const getSupabaseUrlFromRequestUrl = (url: string) => {
const startOfRequest = url.indexOf(ADMIN_PREFIX);
const query = url.substring(startOfRequest + ADMIN_PREFIX.length);
return `${process.env.SUPABASE_URL}/rest/v1${query}`;
};
```
**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.
Finally, 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/promitheus7/ra-data-postgrest):
```sh
yarn add @promitheus/ra-data-postgrest
```
```jsx
// in app/components/App.tsx
import { Admin, Resource, ListGuesser } from 'react-admin';
import postgrestRestProvider from "@promitheus/ra-data-postgrest";

const dataProvider = postgrestRestProvider("/admin/api");

const App = () => (
<Admin basename="/admin" dataProvider={dataProvider}>
<Resource name="posts" list={ListGuesser} />
<Resource name="comments" list={ListGuesser} />
</Admin>
);

export default App;
```
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:3000/admin`, and the API Proxy by visiting `http://localhost:3000/admin/api/posts`.

Note that the Supabase credentials never leave the server. It's up to you to add your own authentication to the API proxy.
4 changes: 2 additions & 2 deletions docs/documentation.html
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,8 @@
<div class="docBlocks">
<a href="./Tutorial.html">
<div class="docBlock">
<h2>Tutorial</h2>
Get the basics in 30 mins
<h2>Getting Started</h2>
30 minutes tutorial, installation instructions
</div>
<div class="material-icons">&#xe425;</div>
</a>
Expand Down
Binary file added docs/img/remix-structure.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 7 additions & 1 deletion docs/navigation.html
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
<li {% if page.path == 'Tutorial.md' %} class="active" {% endif %}><a class="nav-link" href="./Tutorial.html">Tutorial</a></li>
<li><a class="nav-link external" href="https://github.com/marmelab/react-admin/releases" target="_blank">What's new?</a></li>
<li {% if page.path == 'Upgrade.md' %} class="active" {% endif %}><a class="nav-link" href="./Upgrade.html">Upgrading to v4</a></li>

<ul><div>Getting Started</div>
<li {% if page.path == 'Tutorial.md' %} class="active" {% endif %}><a class="nav-link" href="./Tutorial.html">Tutorial</a></li>
<li {% if page.path == 'CreateReactApp.md' %} class="active" {% endif %}><a class="nav-link" href="./CreateReactApp.html">Create React App</a></li>
<li {% if page.path == 'NextJs.md' %} class="active" {% endif %}><a class="nav-link" href="./NextJs.html">Next.js</a></li>
<li {% if page.path == 'Remix.md' %} class="active" {% endif %}><a class="nav-link" href="./Remix.html">Remix</a></li>
</ul>

<ul><div>App Configuration</div>
<li {% if page.path == 'Admin.md' %} class="active" {% endif %}><a class="nav-link" href="./Admin.html"><code>&lt;Admin&gt;</code></a></li>
<li {% if page.path == 'Resource.md' %} class="active" {% endif %}><a class="nav-link" href="./Resource.html"><code>&lt;Resource&gt;</code></a></li>
Expand Down

0 comments on commit 4223623

Please sign in to comment.