Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Doc] Add installation instructions for CRA, Next.js and Remix #7921

Merged
merged 4 commits into from
Jul 5, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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