Skip to main content
Version: 4.xx.xx

Remix

Refine provides router bindings and utilities for Remix. This package will provide easy integration between Refine and Remix for both existing projects and new projects without giving up the benefits of Remix.

npm i @refinedev/remix-router

We recommend using create refine-app to initialize your Refine projects. It configures the project according to your needs including SSR with Remix!

npm create refine-app@latest -- -o refine-remix my-refine-remix-app

Refer to the Router Provider documentation for detailed information.

Legacy Router:

@refinedev/remix-router also exports the legacy router provider and it will be available until the next major version of Refine. It is recommended to use the new router provider instead of the legacy one.

If you are using the legacy router provider, it can be imported from @refinedev/remix-router/legacy and passed to the legacyRouterProvider prop of the Refine component.

Usage

We'll use the routerProvider from @refinedev/remix-router to set up the router bindings for Refine. We'll define the action routes for our resources in the resources array and define our routes in app/routes directory.

We'll create four pages for our resources:

  • app/routes/posts._index.tsx - List page for posts
  • app/routes/posts.show.$id.tsx - Detail page for posts
  • app/routes/categories._index.tsx - List page for categories
  • app/routes/categories.show.$id.tsx - Detail page for categories

And we'll create one page for the index route and use it to redirect to the posts resource:

  • app/routes/_index.tsx - Index page

Notice that we're using the V2 for route file naming convention for our routes. You can find more information about it in the Remix documentation.

Currently, to enable that you need to add the following line to your remix.config.js file:

remix.config.js
module.exports = {
future: {
v2_routeConvention: true,
},
};

Let's start with the initialization of the Refine app in the app/root.tsx file:

app/root.tsx
import {
Links,
LiveReload,
Meta,
Outlet,
Scripts,
ScrollRestoration,
} from "@remix-run/react";

import { Refine } from "@refinedev/core";
import dataProvider from "@refinedev/simple-rest";
import routerProvider from "@refinedev/remix-router";

import { Layout } from "components/Layout";

export default function App(): JSX.Element {
return (
<html lang="en">
<head>
<Meta />
<Links />
</head>
<body>
<Refine
dataProvider={dataProvider("https://api.fake-rest.refine.dev")}
routerProvider={routerProvider}
resources={[
{
name: "posts",
list: "/posts",
show: "/posts/show/:id",
},
{
name: "categories",
list: "/categories",
show: "/categories/show/:id",
},
]}
>
<Layout>
<Outlet />
</Layout>
</Refine>
<ScrollRestoration />
<Scripts />
<LiveReload />
</body>
</html>
);
}
Good to know:

Remix uses the $ symbol to indicate parameters but Refine uses the colon syntax (:param) for route parameters. This won't cause any problems since Refine only uses the colon syntax as an indicator for route parameters and the communication between Refine and the router is handled by the routerProvider prop.

Your action definitions in the resources can contain additional parameters and nested routes. Passing these parameters when navigating to the pages are handled by the current available parameters and the meta props of the related hooks and components.

Refine supports route parameters defined with :param syntax. You can use these parameters in your action definitions and create your routes accordingly. For example, if you have a posts resource and you want to create a route for the show action of a specific post, you can define the show action as /posts/show/:id and use the id parameter in your component.

Now we can create our pages in the routes directory:

app/routes/posts._index.tsx
import { useTable } from "@refinedev/core";
import { NavLink } from "@remix-run/react";

type IPost = {
id: string;
title: string;
description: string;
};

export default function PostList() {
// `posts` resource will be inferred from the route.
// Because we've defined `/posts` as the `list` action of the `posts` resource.
const {
tableQuery: { data, isLoading },
} = useTable<IPost>();

const getToPath = useGetToPath();

const tableData = data?.data;

return (
<div>
{isLoading && <p>Loading...</p>}
{!isLoading && (
<ul>
{tableData?.map((post) => (
<li key={post.id}>
<NavLink
to={getToPath({
resource: "categories",
action: "show",
meta: { id: category.id },
})}
>
{post.title}
</NavLink>
</li>
))}
</ul>
)}
</div>
);
}
app/routes/posts.show.$id.tsx
import { useShow } from "@refinedev/core";

type IPost = {
id: string;
title: string;
description: string;
}

export default function PostShow() {
// `posts` resource and the `id` will be inferred from the route.
// Because we've defined `/posts/show/:id` as the `show` action of the `posts` resource.
const { queryResult: { data, isLoading } } = useShow<IPost>();

const postData = data?.data;

return (
<div>
{isLoading && <p>Loading...</p>}
{!isLoading && (
<h1>{postData?.title}</h1>
<p>{postData?.description}</p>
)}
</div>
);
}
app/routes/categories._index.tsx
import { useTable, getToPath } from "@refinedev/core";
import { NavLink } from "@remix-run/react";

type ICategory = {
id: string;
label: string;
};

export default function CategoryList() {
// `categories` resource will be inferred from the route.
// Because we've defined `/categories` as the `list` action of the `categories` resource.
const {
tableQuery: { data, isLoading },
} = useTable<ICategory>();

const getToPath = useGetToPath();

const tableData = data?.data;

return (
<div>
{isLoading && <p>Loading...</p>}
{!isLoading && (
<ul>
{tableData?.map((category) => (
<li key={category.id}>
<NavLink
to={getToPath({
resource: "categories",
action: "show",
meta: { id: category.id },
})}
>
{category.label}
</NavLink>
</li>
))}
</ul>
)}
</div>
);
}
app/routes/categories.show.$id.tsx
import { useShow } from "@refinedev/core";

type ICategory = {
id: string;
label: string;
};

export default function CategoryShow() {
// `categories` resource and the `id` will be inferred from the route.
// Because we've defined `/categories/show/:id` as the `show` action of the `categories` resource.
const {
queryResult: { data, isLoading },
} = useShow<ICategory>();

const categoryData = data?.data;

return (
<div>
<h1>{categoryData?.label}</h1>
</div>
);
}

Now, we'll use NavigateToResource component to redirect to the posts resource when the user visits the home page.

Even though we're using the NavigateToResource component, when using Remix it's better to handle such redirect operations in the server side rather than the client side. You can use the loader function to redirect the user to the /posts.

app/routes/_index.tsx
import { NavigateToResource } from "@refinedev/remix-router";

export default function Index() {
return <NavigateToResource />;
}

Additional Components

@refinedev/remix-router package also includes some additional components that can be useful in some cases.

A basic component to navigate to a resource page. It is useful when you want to navigate to a resource page at the index route of your app.

app/routes/index.tsx
import { NavigateToResource } from "@refinedev/remix-router";

export default function IndexPage() {
return <NavigateToResource />;
}

Properties

resource (optional) - The name of the resource to navigate to. It will redirect to the first list route in the resources array if not provided.

meta (optional) - The meta object to use if the route has parameters in it. The parameters in the current location will also be used to compose the route but meta will take precedence.

UnsavedChangesNotifier

This component enables the warnWhenUnsavedChanges feature of Refine. It will show a warning message when user tries to navigate away from the current page without saving the changes. Also checks for beforeunload event to warn the user when they try to close the browser tab or window.

Place this component inside the <Refine> components children to enable this feature.

app/root.tsx
import { Refine } from "@refinedev/core";
import { UnsavedChangesNotifier } from "@refinedev/remix-router";

export default function App(): JSX.Element {
return (
<Refine
/* ... */
>
<Outlet />
<UnsavedChangesNotifier />
</Refine>
);
}

Properties

translationKey (optional) - The translation key for the warning message. Default value is warnWhenUnsavedChanges. Useful when you use an i18n provider.

message (optional) - The warning message. Default value is Are you sure you want to leave? You have unsaved changes. Useful when you don't use an i18n provider.

parseTableParams

This function can be used to parse the query parameters of a table page. It can be useful when you want to use the query parameters in your server side functions (loader) to fetch the data such as persisting the table state

Authentication

In Remix you can achieve authentication control in multiple ways;

On the client-side Authenticated component from @refinedev/core can be used to protect your pages from unauthenticated access.

On the server-side authProvider's check function inside server side functions (loader) to redirect unauthorized users to other pages using redirect from @remix-run/node.

Implementation Tips:

For page level access control, server-side approach is recommended.

Server Side Authentication with createCookieSessionStorage

First, let's create our AuthProvider. For more information on AuthProvider, visit our AuthProvider documentation.

app/authProvider.ts
import { AuthProvider } from "@refinedev/core";

const mockUsers = [
{
username: "admin",
roles: ["admin"],
},
{
username: "editor",
roles: ["editor"],
},
];

export const authProvider: AuthProvider = {
login: async ({ username, password, remember }) => {
// Suppose we actually send a request to the back end here.
const user = mockUsers.find((item) => item.username === username);

if (user) {
return {
success: true,
redirectTo: "/",
};
}

return {
success: false,
error: {
message: "Login failed",
name: "Invalid email or password",
},
};
},
logout: async () => {
return {
success: true,
redirectTo: "/",
};
},
onError: async (error) => {
if (error && error.statusCode === 401) {
return {
logout: true,
redirectTo: "/login",
};
}

return {};
},
check: async ({ request, storage }) => {
const session = await storage.getSession(request.headers.get("Cookie"));

const user = session.get("user");

if (!user) {
return {
authenticated: false,
logout: true,
redirectTo: "/login",
};
}
return {
authenticated: true,
};
},
getPermissions: async () => {
return null;
},
getIdentity: async () => {
return null;
},
};

Next, let's create the app/session.server.ts file as mentioned in the Jokes App tutorial

app/session.server.ts
import { createCookieSessionStorage, redirect } from "@remix-run/node";
import { authProvider } from "./authProvider";

type LoginForm = {
username: string;
password: string;
};

// normally you want this to be env variable
const sessionSecret = "SUPER_SECRET_SESSION"; //process.env.SESSION_SECRET;

if (!sessionSecret) {
throw new Error("SESSION_SECRET must be set");
}

const storage = createCookieSessionStorage({
cookie: {
name: "RJ_session",
// normally you want this to be `secure: true`
// but that doesn't work on localhost for Safari
// https://web.dev/when-to-use-local-https/
secure: process.env.NODE_ENV === "production",
secrets: [sessionSecret],
sameSite: "lax",
path: "/",
maxAge: 60 * 60 * 24 * 30,
httpOnly: true,
},
});

export async function login({ username, password }: LoginForm) {
try {
const user = await authProvider.login({ username, password });
if (user) {
return { user };
}
} catch (error) {
return error;
}
}

export async function requireUserId(
request: Request,
redirectTo: string = new URL(request.url).pathname,
) {
try {
const user = await authProvider.check?.({ request, storage });
return user;
} catch (error) {
const searchParams = new URLSearchParams([["to", redirectTo]]);
throw redirect(`/login?${searchParams}`);
}
}

export async function createUserSession(user: object, redirectTo: string) {
const session = await storage.getSession();
session.set("user", { ...user });
return redirect(redirectTo, {
headers: {
"Set-Cookie": await storage.commitSession(session),
},
});
}

export async function logout(request: Request) {
const session = await storage.getSession(request.headers.get("Cookie"));
return redirect("/login", {
headers: {
"Set-Cookie": await storage.destroySession(session),
},
});
}

In the login and requireUserId functions, we call the corresponding functions of our AuthProvider.

Now let's create our login page

app/routes/login.tsx
import React from "react";
import { useTranslate } from "@refinedev/core";

import { login, createUserSession } from "~/session.server";
import { ActionFunction } from "@remix-run/node";
import { useSearchParams } from "@remix-run/react";

export interface ILoginForm {
username: string;
password: string;
}

const LoginPage: React.FC = () => {
const translate = useTranslate();
const [searchParams] = useSearchParams();

return (
<>
<h1>{translate("pages.login.title", "Sign in your account")}</h1>
<form method="post">
<input
type="hidden"
name="redirectTo"
value={searchParams.get("to") ?? undefined}
/>
<table>
<tbody>
<tr>
<td>
{translate("pages.login.username", undefined, "username")}:
</td>
<td>
<input
name="username"
type="text"
size={20}
autoCorrect="off"
spellCheck={false}
autoCapitalize="off"
autoFocus
required
/>
</td>
</tr>
<tr>
<td>
{translate("pages.login.password", undefined, "password")}:
</td>
<td>
<input type="password" name="password" required size={20} />
</td>
</tr>
</tbody>
</table>
<br />
<input type="submit" value="login" />
</form>
</>
);
};

export const action: ActionFunction = async ({ request }) => {
const form = await request.formData();
const username = form.get("username") as string;
const password = form.get("password") as string;
const redirectTo = form.get("redirectTo") || "/";
const user = await login({ username, password });
if (!user) {
return null;
}

return createUserSession(user as any, redirectTo as string);
};

export default LoginPage;

Yeeyy! Now our users can login! 🎉

Remember, actions and loaders run on the server, so console.log calls you put in those you can't see in the browser console. Those will show up in the terminal window you're running your server in.

We can call the requireUserId function on our routes where we want the authentication check done.

import { json, LoaderFunction } from "@remix-run/node";
import { requireUserId } from "~/session.server";

export const loader: LoaderFunction = async ({ params, request, context }) => {
await requireUserId(request);

return json({});
};

Finally, let's make sure our users can log out. For this, we create a routes for /logout.

/app/routes/logout.tsx
import type { LoaderFunction } from "@remix-run/node";

import { logout } from "~/session.server";

export const loader: LoaderFunction = async ({ request }) => {
return await logout(request);
};

First, let's install the js-cookie and cookie packages in our project.

npm i js-cookie cookie
npm i -D @types/js-cookie

We will set/destroy cookies in the login, logout and check functions of our AuthProvider.

app/authProvider.ts
import { AuthProvider } from "@refinedev/core";
import Cookies from "js-cookie";
import * as cookie from "cookie";

const mockUsers = [
{
username: "admin",
roles: ["admin"],
},
{
username: "editor",
roles: ["editor"],
},
];

const COOKIE_NAME = "user";

export const authProvider: AuthProvider = {
login: ({ username, password, remember }) => {
// Suppose we actually send a request to the back end here.
const user = mockUsers.find((item) => item.username === username);

if (user) {
Cookies.set(COOKIE_NAME, JSON.stringify(user));
return {
success: true,
};
}

return {
success: false,
};
},
logout: () => {
Cookies.remove(COOKIE_NAME);

return {
success: true,
redirectTo: "/login",
};
},
onError: (error) => {
if (error && error.statusCode === 401) {
return {
error: new Error("Unauthorized"),
logout: true,
redirectTo: "/login",
};
}

return {};
},
check: async (context) => {
let user = undefined;
if (context) {
// for SSR
const { request } = context;
const parsedCookie = cookie.parse(request.headers.get("Cookie"));
user = parsedCookie[COOKIE_NAME];
} else {
// for CSR
const parsedCookie = Cookies.get(COOKIE_NAME);
user = parsedCookie ? JSON.parse(parsedCookie) : undefined;
}

if (!user) {
return {
authenticated: false,
error: {
message: "Check failed",
name: "Unauthorized",
},
logout: true,
redirectTo: "/login",
};
}

return {
authenticated: true,
};
},
getPermissions: async () => {
return null;
},
getIdentity: async () => {
return null;
},
};

Tadaa! that's all! 🎉

Now, we can check the authentication in loaders of our routes.

app/routes/_index.tsx
import { json, LoaderFunction } from "@remix-run/node";
import { authProvider } from "~/authProvider";

export const loader: LoaderFunction = async ({ params, request, context }) => {
// We've handled the SSR case in our `check` function by sending the `request` as parameter.
const { authenticated } = await authProvider.check(request);

if (!authenticated) {
return json({}, { status: 401 });
}

return null;
};

This needs to be done for all the routes that we want to protect.

Access Control

There are two ways to do Server Side Authentication with Remix. You can choose one of the two methods according to your use case.

Server Side

On the server-side accessControlProvider's can function inside server side functions (loader) to redirect unauthorized users to other pages using redirect from @remix-run/node.

First, let's build our AccessControlProvider

app/acccessControlProvider.ts
export const accessControlProvider = {
can: async ({ resource, action, params }) => {
if (resource === "posts" && action === "edit") {
return {
can: false,
reason: "Unauthorized",
};
}

return { can: true };
},
};

You can also access resource object directly.

const resourceName = params?.resource?.name;
const anyUsefulMeta = params?.resource?.meta?.yourUsefulMeta;

export const accessControlProvider = {
can: async ({ resource, action, params }) => {
if (
resourceName === "posts" &&
anyUsefulMeta === true &&
action === "edit"
) {
return {
can: false,
reason: "Unauthorized",
};
}
},
};

Then, let's create our posts route.

app/routes/_protected.posts._index.tsx
import { useLoaderData } from "@remix-run/react";
import { json, LoaderFunctionArgs } from "@remix-run/node";
import dataProvider from "@refinedev/simple-rest";

import { IPost } from "../interfaces";
import { API_URL } from "~/constants";
import { accessControlProvider } from "../accessControlProvider";

const PostList: React.FC = () => {
const { initialData } = useLoaderData<typeof loader>();

return <>{/* ... */}</>;
};

export default PostList;

export async function loader({ request }: LoaderFunctionArgs) {
const can = accessControlProvider.can({
resource: "posts",
action: "list",
});

if (!can) {
return json({}, { status: 403 });
}

const data = await dataProvider(API_URL).getList<IPost>({
resource: "posts",
});

return json({ initialData: data });
}

Tadaa! that's all! 🎉

Client Side

For client-side, you can wrap your pages with CanAccess component from @refinedev/core to protect your pages from unauthorized access.

import { CanAccess } from "@refinedev/core";

export const MyPage = () => (
<CanAccess>
<div>{/* ... */}</div>
</CanAccess>
);

FAQ

Can I use nested routes?

Yes, you can use nested routes in your app. Refine will match the routes depending on how you define the action paths in your resources. Additional parameters and nesting is supported. Refine will not limit you and your router in route configuration, all you need to do is to pass the appropriate path to the related resource and the action in the resources array (This is also optional but recommended due to the features it provides).

You can use :param syntax to define parameters in your routes.

How to make SSR work?

You can always use the methods provided from the dataProvider to fetch data in your pages. To do this, you can use loader function and pass the data to your page as a prop.

All you need to do is to pass the data as the initialData to your data hooks using the queryOptions prop.

import { useList } from "@refinedev/core";
import { useLoaderData } from "@remix-run/react";

import { dataProvider } from "src/providers";

type IPost = {
id: number;
title: string;
description: string;
};

export async function loader() {
const { data } = await dataProvider.getList<IPost>("posts", {
pagination: {
page: 1,
perPage: 10,
},
});

return json(data);
}

export default function Posts() {
const initialPosts = useLoaderData<typeof loader>();

const {
tableQuery: { data },
} = useTable<IPost>({
queryOptions: {
initialData: initialPosts,
},
});

return <>{/* ... */}</>;
}

How to persist syncWithLocation in SSR?

If syncWithLocation is enabled, query parameters must be handled while doing SSR.

import { json, LoaderFunction } from "@remix-run/node";
import { parseTableParams } from "@refinedev/remix-router";
import dataProvider from "@refinedev/simple-rest";

const API_URL = "https://api.fake-rest.refine.dev";

export const loader: LoaderFunction = async ({ params, request }) => {
const { resource } = params;
const url = new URL(request.url);

const tableParams = parseTableParams(url.search);

try {
const data = await dataProvider(API_URL).getList({
resource: resource as string,
...tableParams, // this includes `filters`, `sorters` and `pagination`
});

return json({ initialData: data });
} catch (error) {
return json({});
}
};

export default function MyListRoute() {
return <>{/* ... */}</>;
}

parseTableParams parses the query string and returns query parameters(refer here for their interfaces). They can be directly used for dataProvider methods that accept them.

Handling 404s

In the earlier versions of Refine, if authProvider was defined, we've redirected the users to the /login route even with the 404s and 404 pages were only available to the authenticated users. Now, the routes are handled by the users, so you can handle the 404s however you like.

In remix, you can use a splat ($) route to handle the 404s. Check out the remix docs for more information.

Using loader

app/routes/$.tsx
import { json, LoaderFunction } from "@remix-run/node";

export const loader: LoaderFunction = async ({ params, request, context }) => {
return json({}, { status: 404 });
};

Using Authenticated

app/routes/$.tsx
import { Authenticated } from "@refinedev/core";

export default function NotFound() {
return (
<Authenticated>
<div>I'm the 404 page for the authenticated users.</div>
</Authenticated>
);
}

RefineRoutes

While this may work for the simple cases, it is not recommended to use this component. Defining your routes separately will give you more control over your routes and will allow you to use the full potential of your router.

This component can be used to render the matching route in your resources by using the resources prop. It will only take effect if the action properties in the resource definitions are assigned to components or objects with component property.

It will render the component for the matching route and pass it as a JSX.Element to the children function. You can use this to render your components in a single catch-all route. If there's no matching route undefined will be passed to the children function. In this case, you can render an error page or redirect the user to another page.

We'll define our resources in the <Refine> component:

app/root.tsx
return (
<Refine
resources={[
{
name: "posts",
list: "/posts",
show: "/posts/show/:id",
},
{
name: "categories",
list: "/categories",
},
]}
>
{/* ... */}
</Refine>
);

Then, we'll create a catch-all route to render the matching route in our resources:

app/routes/$.tsx
import { RefineRoutes } from "@refinedev/remix-router";

import { ErrorPage } from "components/error";

export default function CatchAll() {
return (
<RefineRoutes>
{(matchingRoute) => {
if (matchingRoute) {
return { matchingRoute };
}

return <ErrorPage />;
}}
</RefineRoutes>
);
}

When components are used to define the resource actions, default paths will be used. You can override the default paths by assigning an object with component and path properties to the action properties.

Default paths are:

  • list: /resources
  • create: /resources/create
  • edit: /resources/edit/:id
  • show: /resources/show/:id

Example

Run on your local
npm create refine-app@latest -- --example with-remix-antd
Run on your local
npm create refine-app@latest -- --example with-remix-headless