Skip to content

Commit 7f23644

Browse files
authored
feat: add react router v7 (#978)
fixed #964
1 parent 474fd95 commit 7f23644

File tree

67 files changed

+2329
-1874
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

67 files changed

+2329
-1874
lines changed

.changeset/dry-hornets-perform.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
"@logto/react-router-sample": major
3+
"@logto/react-router": major
4+
---
5+
6+
add react-router SDK
7+
8+
Migrate from Remix to React Router, please refer to the [official React Router migration documentation](https://reactrouter.com/upgrading/remix).
9+
10+
And you can check the new sample project to see how to use the SDK.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
.react-router

packages/remix-sample/README.md renamed to packages/react-router-sample/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
# Remix Sample
1+
# React Router Sample
22

3-
This is a sample project for Logto's Remix SDK.
3+
This is a sample project for Logto's React Router SDK.
44

55
## Configuration
66

packages/remix-sample/app/root.tsx renamed to packages/react-router-sample/app/root.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/* eslint-disable consistent-default-export-name/default-export-match-filename */
2-
import { Links, Meta, Outlet, Scripts, ScrollRestoration } from '@remix-run/react';
2+
import { Links, Meta, Outlet, Scripts, ScrollRestoration } from 'react-router';
33

44
const App = () => {
55
return (
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import { type RouteConfig } from '@react-router/dev/routes';
2+
import { flatRoutes } from '@react-router/fs-routes';
3+
4+
export default flatRoutes() satisfies RouteConfig;

packages/remix-sample/app/routes/_index.tsx renamed to packages/react-router-sample/app/routes/_index.tsx

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
11
/* eslint-disable consistent-default-export-name/default-export-match-filename */
2-
import { type LogtoContext } from '@logto/remix';
3-
import { type LoaderFunction } from '@remix-run/node';
4-
import { json, Link, useLoaderData } from '@remix-run/react';
2+
import { type LogtoContext } from '@logto/react-router';
3+
import { Link, type LoaderFunctionArgs } from 'react-router';
54

65
import { logto } from '../services/auth.server';
76

87
type LoaderResponse = {
98
readonly context: LogtoContext;
109
};
1110

12-
export const loader: LoaderFunction = async ({ request }) => {
11+
export const loader = async ({ request }: LoaderFunctionArgs) => {
1312
const context = await logto.getContext({ getAccessToken: false })(request);
1413

1514
// You can uncomment this to protect the route and
@@ -19,11 +18,11 @@ export const loader: LoaderFunction = async ({ request }) => {
1918
// return redirect('/api/logto/sign-in');
2019
// }
2120

22-
return json<LoaderResponse>({ context });
21+
return { context };
2322
};
2423

25-
const Home = () => {
26-
const { context } = useLoaderData<LoaderResponse>();
24+
const Home = ({ loaderData }: { readonly loaderData: LoaderResponse }) => {
25+
const { context } = loaderData;
2726
const { isAuthenticated, claims } = context;
2827

2928
return (

packages/remix-sample/app/routes/access-token.tsx renamed to packages/react-router-sample/app/routes/access-token.tsx

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
11
// eslint-disable-next-line consistent-default-export-name/default-export-match-filename
2-
import type { LogtoContext } from '@logto/remix';
3-
import { type LoaderFunction, json, redirect } from '@remix-run/node';
4-
import { useLoaderData } from '@remix-run/react';
2+
import type { LogtoContext } from '@logto/react-router';
3+
import { redirect, type LoaderFunctionArgs } from 'react-router';
54

65
import { logto } from '../services/auth.server';
76

87
type LoaderResponse = {
98
readonly context: LogtoContext;
109
};
1110

12-
export const loader: LoaderFunction = async ({ request }) => {
11+
export const loader = async ({ request }: LoaderFunctionArgs) => {
1312
const context = await logto.getContext({ getAccessToken: true })(request);
1413

1514
if (!context.isAuthenticated) {
@@ -19,13 +18,13 @@ export const loader: LoaderFunction = async ({ request }) => {
1918
// You can now use the access token here,
2019
// for demonstration purposes, we'll just pass it back to the client
2120

22-
return json<LoaderResponse>({ context });
21+
return { context };
2322
};
2423

25-
const AccessToken = () => {
24+
const AccessToken = ({ loaderData }: { readonly loaderData: LoaderResponse }) => {
2625
const {
2726
context: { accessToken },
28-
} = useLoaderData<LoaderResponse>();
27+
} = loaderData;
2928

3029
return (
3130
<div>

packages/remix-sample/app/routes/user-info.tsx renamed to packages/react-router-sample/app/routes/user-info.tsx

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,28 @@
11
// eslint-disable-next-line consistent-default-export-name/default-export-match-filename
22
import type { LogtoContext } from '@logto/remix';
3-
import { type LoaderFunction, json, redirect } from '@remix-run/node';
4-
import { useLoaderData } from '@remix-run/react';
3+
import { type LoaderFunctionArgs, redirect } from 'react-router';
54

65
import { logto } from '../services/auth.server';
76

87
type LoaderResponse = {
98
readonly context: LogtoContext;
109
};
1110

12-
export const loader: LoaderFunction = async ({ request }) => {
11+
export const loader = async ({ request }: LoaderFunctionArgs) => {
1312
// This will fetch the user info from Logto every time the route is loaded
1413
const context = await logto.getContext({ fetchUserInfo: true })(request);
1514

1615
if (!context.isAuthenticated) {
1716
return redirect('/api/logto/sign-in');
1817
}
1918

20-
return json<LoaderResponse>({ context });
19+
return { context };
2120
};
2221

23-
const UserInfo = () => {
22+
const UserInfo = ({ loaderData }: { readonly loaderData: LoaderResponse }) => {
2423
const {
2524
context: { userInfo },
26-
} = useLoaderData<LoaderResponse>();
25+
} = loaderData;
2726

2827
return (
2928
<div>

packages/remix-sample/app/services/auth.server.ts renamed to packages/react-router-sample/app/services/auth.server.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { makeLogtoRemix } from '@logto/remix';
2-
import { createCookieSessionStorage } from '@remix-run/node';
1+
import { makeLogtoReactRouter } from '@logto/react-router';
2+
import { createCookieSessionStorage } from 'react-router';
33

44
const sessionStorage = createCookieSessionStorage({
55
cookie: {
@@ -14,7 +14,7 @@ if (!process.env.LOGTO_ENDPOINT || !process.env.LOGTO_APP_ID || !process.env.LOG
1414
throw new Error('Missing Logto environment variables');
1515
}
1616

17-
export const logto = makeLogtoRemix(
17+
export const logto = makeLogtoReactRouter(
1818
{
1919
endpoint: process.env.LOGTO_ENDPOINT,
2020
appId: process.env.LOGTO_APP_ID,

packages/remix-sample/package.json renamed to packages/react-router-sample/package.json

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,26 @@
11
{
2-
"name": "@logto/remix-sample",
3-
"version": "0.0.1",
2+
"name": "@logto/react-router-sample",
3+
"version": "0.0.0",
44
"license": "MIT",
55
"private": true,
66
"scripts": {
7-
"build": "remix vite:build",
8-
"dev": "remix vite:dev",
9-
"start": "remix-serve build/server/index.js",
10-
"typecheck": "tsc"
7+
"build": "react-router build",
8+
"dev": "react-router dev",
9+
"start": "react-router-serve ./build/server/index.js",
10+
"typecheck": "react-router typegen && tsc"
1111
},
1212
"dependencies": {
13-
"@logto/remix": "workspace:*",
14-
"@remix-run/node": "^2.16.5",
15-
"@remix-run/react": "^2.16.5",
16-
"@remix-run/serve": "^2.16.5",
13+
"@logto/react-router": "workspace:*",
14+
"@react-router/fs-routes": "^7.6.2",
15+
"@react-router/node": "^7.5.3",
16+
"@react-router/serve": "^7.5.3",
1717
"isbot": "^4",
1818
"react": "^18.2.0",
19-
"react-dom": "^18.2.0"
19+
"react-dom": "^18.2.0",
20+
"react-router": "^7.5.3"
2021
},
2122
"devDependencies": {
22-
"@remix-run/dev": "^2.16.5",
23+
"@react-router/dev": "^7.5.3",
2324
"@silverhand/eslint-config": "^6.0.1",
2425
"@silverhand/eslint-config-react": "^6.0.2",
2526
"@silverhand/ts-config": "^6.0.0",
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import type { Config } from "@react-router/dev/config";
2+
3+
export default {
4+
// Config options...
5+
// Server-side render by default, to enable SPA mode set this to `false`
6+
ssr: true,
7+
} satisfies Config;
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { reactRouter } from '@react-router/dev/vite';
2+
import { defineConfig } from 'vite';
3+
import tsconfigPaths from 'vite-tsconfig-paths';
4+
5+
export default defineConfig({
6+
plugins: [reactRouter(), tsconfigPaths()],
7+
});

packages/react-router/README.md

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
# Logto React Router SDK
2+
3+
[![Version](https://img.shields.io/npm/v/@logto/react-router)](https://www.npmjs.com/package/@logto/react-router)
4+
[![Build Status](https://github.com/logto-io/js/actions/workflows/main.yml/badge.svg)](https://github.com/logto-io/js/actions/workflows/main.yml)
5+
[![Codecov](https://img.shields.io/codecov/c/github/logto-io/js)](https://app.codecov.io/gh/logto-io/js?branch=master)
6+
7+
The Logto React Router SDK written in TypeScript.
8+
9+
> **Note:** This SDK has been migrated from Remix to React Router. For detailed migration guide, please refer to the [official React Router migration documentation](https://reactrouter.com/upgrading/remix).
10+
11+
## Installation
12+
13+
**Note:** This package requires Node.js version 20 or higher.
14+
15+
### Using npm
16+
17+
```bash
18+
npm install @logto/react-router
19+
```
20+
21+
### Using yarn
22+
23+
```bash
24+
yarn add @logto/react-router
25+
```
26+
27+
### Using pnpm
28+
29+
```bash
30+
pnpm add @logto/react-router
31+
```
32+
33+
## Usage
34+
35+
Before initializing the SDK, we have to create a `SessionStorage` instance which takes care of the session persistence. In our case, we want to use a cookie-based session:
36+
37+
```ts
38+
// services/auth.server.ts
39+
import { makeLogtoReactRouter } from '@logto/react-router';
40+
import { createCookieSessionStorage } from 'react-router';
41+
42+
const sessionStorage = createCookieSessionStorage({
43+
cookie: {
44+
name: 'logto-session',
45+
maxAge: 14 * 24 * 60 * 60,
46+
secrets: ['secr3tSession'],
47+
},
48+
});
49+
50+
export const logto = makeLogtoReactRouter(
51+
{
52+
endpoint: process.env.LOGTO_ENDPOINT!,
53+
appId: process.env.LOGTO_APP_ID!,
54+
appSecret: process.env.LOGTO_APP_SECRET!,
55+
baseUrl: process.env.LOGTO_BASE_URL!,
56+
},
57+
{ sessionStorage }
58+
);
59+
```
60+
61+
Whereas the environment variables reflect the respective configuration of the application in Logto.
62+
63+
### Setup file-based routing
64+
65+
```ts
66+
// app/routes.ts
67+
import { type RouteConfig } from '@react-router/dev/routes';
68+
import { flatRoutes } from '@react-router/fs-routes';
69+
70+
export default flatRoutes() satisfies RouteConfig;
71+
```
72+
73+
This will generate the routes for you based on the files in the `app/routes` directory.
74+
75+
### Mounting the authentication route handlers
76+
77+
The SDK ships with a convenient function that mounts the authentication routes: sign-in, sign-in callback and the sign-out route. Create a file `routes/api.logto.$action.ts`
78+
79+
```ts
80+
// routes/api.logto.$action.ts
81+
82+
import { logto } from '../../services/auth.server';
83+
84+
export const loader = logto.handleAuthRoutes({
85+
'sign-in': {
86+
path: '/api/logto/sign-in',
87+
redirectBackTo: '/api/logto/callback',
88+
},
89+
'sign-in-callback': {
90+
path: '/api/logto/callback',
91+
redirectBackTo: '/',
92+
},
93+
'sign-out': {
94+
path: '/api/logto/sign-out',
95+
redirectBackTo: '/',
96+
},
97+
'sign-up': {
98+
path: '/api/logto/sign-up',
99+
redirectBackTo: '/api/logto/callback',
100+
},
101+
});
102+
```
103+
104+
As you can see, the mount process is configurable and you can adjust it for your particular route structure. The whole URL path structure can be customized via the passed configuration object.
105+
106+
When mounting the routes as described above, you can navigate your browser to `/api/logto/sign-in` and you should be redirected to your Logto instance where you have to authenticate then.
107+
108+
### Get the authentication context
109+
110+
A typical use case is to fetch the _authentication context_ which contains information about the respective user. With that information, it is possible to decide if the user is authenticated or not. The SDK exposes a function that can be used in a React Router `loader` function:
111+
112+
```ts
113+
// app/routes/_index.tsx
114+
import { type LogtoContext } from '@logto/react-router';
115+
import { Link, type LoaderFunctionArgs } from 'react-router';
116+
117+
import { logto } from '../../services/auth.server';
118+
119+
type LoaderResponse = {
120+
readonly context: LogtoContext;
121+
};
122+
123+
export const loader = async ({ request }: LoaderFunctionArgs) => {
124+
const context = await logto.getContext({ getAccessToken: false })(request);
125+
126+
if (!context.isAuthenticated) {
127+
return redirect('/api/logto/sign-in');
128+
}
129+
130+
return { context };
131+
};
132+
133+
const Home = ({ loaderData }: { readonly loaderData: LoaderResponse }) => {
134+
const { context } = loaderData;
135+
const { isAuthenticated, claims } = context;
136+
137+
return <div>Protected Route.</div>;
138+
};
139+
```
140+
141+
## Resources
142+
143+
[![Website](https://img.shields.io/badge/website-logto.io-8262F8.svg)](https://logto.io/)
144+
[![Discord](https://img.shields.io/discord/965845662535147551?logo=discord&logoColor=ffffff&color=7389D8&cacheSeconds=600)](https://discord.gg/UEPaF3j5e6)

packages/react-router/jest.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { default } from '../../jest.config.js';

0 commit comments

Comments
 (0)