Skip to content

Commit 5e24d8f

Browse files
authored
feat: redirect kind (#315)
Adds a `kind` parameter to the `redirect()` function, giving developers control over how redirects behave on the client side. ### Motivation Resolves #289. The default redirect behavior (RSC navigation with `replaceState`) isn't always what's needed. For example, OAuth flows require a full browser navigation, some redirects should preserve history for back-button support, and server actions sometimes need to signal a redirect to the client without automatically navigating. ### API ```ts redirect(url: string, status?: number, kind?: RedirectKind): never ``` **`RedirectKind`** is a union of four values: | Kind | Behavior | |---|---| | `"navigate"` | **(default)** RSC navigation with `replaceState` — same as current behavior | | `"push"` | RSC navigation with `pushState` — adds a history entry so the user can go back | | `"location"` | Full browser navigation via `location.href` — useful for external URLs or OAuth flows | | `"error"` | Throws the redirect error on the client — allows custom handling via `try`/`catch` in server actions |
1 parent c39c3ae commit 5e24d8f

16 files changed

Lines changed: 515 additions & 32 deletions

File tree

docs/src/pages/en/(pages)/framework/http.mdx

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,51 @@ export default function MyComponent() {
280280
};
281281
```
282282

283+
The `redirect()` function accepts an optional third argument `kind` that controls how the redirect is performed on the client. The available kinds are:
284+
285+
| Kind | Description |
286+
| --- | --- |
287+
| `"navigate"` | **(default)** Performs an RSC navigation using `replaceState`. The browser URL changes without adding a history entry. |
288+
| `"push"` | Performs an RSC navigation using `pushState`. The browser URL changes and a new history entry is added, so the user can navigate back. |
289+
| `"location"` | Forces a full browser navigation via `location.href`. Useful for redirecting to external URLs or when a full page reload is needed. |
290+
| `"error"` | Throws the redirect error on the client instead of navigating. This allows custom handling via `try`/`catch` in server action calls. |
291+
292+
```jsx
293+
import { redirect } from "@lazarv/react-server";
294+
295+
// RSC navigation with pushState (adds history entry)
296+
redirect("/dashboard", 302, "push");
297+
298+
// Full browser navigation
299+
redirect("/oauth/authorize", 302, "location");
300+
301+
// Throw on client for custom handling
302+
redirect("/login", 302, "error");
303+
```
304+
305+
When using the `"error"` kind in a server action, the client can catch the redirect error and handle it:
306+
307+
```jsx
308+
"use client";
309+
310+
import { myServerAction } from "./actions";
311+
312+
export function MyComponent() {
313+
const handleClick = async () => {
314+
try {
315+
await myServerAction();
316+
} catch (e) {
317+
if (e?.digest?.startsWith("Location=")) {
318+
const url = e.digest.split("Location=")[1]?.split(";")[0];
319+
console.log(`Redirect to: ${url}`);
320+
}
321+
}
322+
};
323+
324+
return <button onClick={handleClick}>Submit</button>;
325+
}
326+
```
327+
283328
<Link name="rewrite">
284329
## Rewrite
285330
</Link>

docs/src/pages/en/(pages)/router/server-routing.mdx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,19 @@ export default function App() {
192192
}
193193
```
194194

195+
You can also specify a `kind` parameter to control how the redirect behaves on the client:
196+
197+
```tsx
198+
import { redirect } from "@lazarv/react-server";
199+
200+
export default function ProtectedPage() {
201+
// Redirect with pushState so the user can navigate back
202+
redirect("/login", 302, "push");
203+
}
204+
```
205+
206+
Available redirect kinds: `"navigate"` (default, replaceState), `"push"` (pushState), `"location"` (full browser navigation), and `"error"` (throw on client for custom handling). See the [HTTP redirect documentation](/framework/http#redirect) for details.
207+
195208
<Link name="rewrites">
196209
## Rewrites
197210
</Link>

docs/src/pages/ja/(pages)/framework/http.mdx

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,51 @@ export default function MyComponent() {
279279
};
280280
```
281281

282+
`redirect()` 関数は、クライアントでのリダイレクトの動作を制御するオプションの第3引数 `kind` を受け付けます。利用可能な種類は以下の通りです:
283+
284+
| 種類 | 説明 |
285+
| --- | --- |
286+
| `"navigate"` | **(デフォルト)** `replaceState` を使用したRSCナビゲーションを実行します。ブラウザのURLは変更されますが、履歴エントリは追加されません。 |
287+
| `"push"` | `pushState` を使用したRSCナビゲーションを実行します。ブラウザのURLが変更され、新しい履歴エントリが追加されるため、ユーザーは戻るボタンで戻ることができます。 |
288+
| `"location"` | `location.href` を使用した完全なブラウザナビゲーションを強制します。外部URLへのリダイレクトやページの完全なリロードが必要な場合に便利です。 |
289+
| `"error"` | ナビゲーションの代わりにクライアントでリダイレクトエラーをスローします。サーバーアクション呼び出しで `try`/`catch` によるカスタム処理が可能になります。 |
290+
291+
```jsx
292+
import { redirect } from "@lazarv/react-server";
293+
294+
// pushStateを使用したRSCナビゲーション(履歴エントリを追加)
295+
redirect("/dashboard", 302, "push");
296+
297+
// 完全なブラウザナビゲーション
298+
redirect("/oauth/authorize", 302, "location");
299+
300+
// カスタム処理のためにクライアントでスロー
301+
redirect("/login", 302, "error");
302+
```
303+
304+
サーバーアクションで `"error"` 種類を使用する場合、クライアントでリダイレクトエラーをキャッチして処理できます:
305+
306+
```jsx
307+
"use client";
308+
309+
import { myServerAction } from "./actions";
310+
311+
export function MyComponent() {
312+
const handleClick = async () => {
313+
try {
314+
await myServerAction();
315+
} catch (e) {
316+
if (e?.digest?.startsWith("Location=")) {
317+
const url = e.digest.split("Location=")[1]?.split(";")[0];
318+
console.log(`リダイレクト先: ${url}`);
319+
}
320+
}
321+
};
322+
323+
return <button onClick={handleClick}>送信</button>;
324+
}
325+
```
326+
282327
<Link name="rewrite">
283328
## リライト
284329
</Link>

docs/src/pages/ja/(pages)/router/server-routing.mdx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,19 @@ export default function App() {
192192
}
193193
```
194194

195+
`kind` パラメータを指定して、クライアントでのリダイレクトの動作を制御することもできます:
196+
197+
```tsx
198+
import { redirect } from "@lazarv/react-server";
199+
200+
export default function ProtectedPage() {
201+
// pushStateを使用してリダイレクト(ユーザーが戻れるように)
202+
redirect("/login", 302, "push");
203+
}
204+
```
205+
206+
利用可能なリダイレクト種類:`"navigate"`(デフォルト、replaceState)、`"push"`(pushState)、`"location"`(完全なブラウザナビゲーション)、`"error"`(カスタム処理のためにクライアントでスロー)。詳細は[HTTPリダイレクトのドキュメント](/framework/http#redirect)を参照してください。
207+
195208
<Link name="rewrites">
196209
## リライト
197210
</Link>
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
"use client";
2+
3+
import { useState } from "react";
4+
5+
import {
6+
redirectNavigate,
7+
redirectPush,
8+
redirectLocation,
9+
redirectLocationExternal,
10+
redirectError,
11+
} from "../redirect-actions";
12+
13+
export default function RedirectKindButtons() {
14+
const [result, setResult] = useState<string | null>(null);
15+
16+
return (
17+
<div>
18+
<button
19+
data-testid="redirect-navigate"
20+
onClick={async () => {
21+
await redirectNavigate();
22+
}}
23+
>
24+
Navigate (default)
25+
</button>
26+
<br />
27+
<button
28+
data-testid="redirect-push"
29+
onClick={async () => {
30+
await redirectPush();
31+
}}
32+
>
33+
Push (pushState)
34+
</button>
35+
<br />
36+
<button
37+
data-testid="redirect-location"
38+
onClick={async () => {
39+
await redirectLocation();
40+
}}
41+
>
42+
Location (full browser navigation)
43+
</button>
44+
<br />
45+
<button
46+
data-testid="redirect-location-external"
47+
onClick={async () => {
48+
await redirectLocationExternal();
49+
}}
50+
>
51+
Location External
52+
</button>
53+
<br />
54+
<button
55+
data-testid="redirect-error"
56+
onClick={async () => {
57+
try {
58+
await redirectError();
59+
} catch (e: any) {
60+
if (e?.digest?.startsWith("Location=")) {
61+
const url = e.digest.split("Location=")[1]?.split(";")[0];
62+
setResult(`Caught redirect to: ${url}`);
63+
} else {
64+
setResult(`Unexpected error: ${e.message}`);
65+
}
66+
}
67+
}}
68+
>
69+
Error (try/catch)
70+
</button>
71+
{result && <p data-testid="redirect-error-result">{result}</p>}
72+
</div>
73+
);
74+
}

examples/file-router/pages/(redirect_extern).middleware.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,17 @@ export default function RedirectMiddleware() {
1616
if (pathname === "/redirect-about") {
1717
redirect("/about");
1818
}
19+
// Redirect kind examples
20+
if (pathname === "/redirect-push") {
21+
redirect("/about", 302, "push");
22+
}
23+
if (pathname === "/redirect-location") {
24+
redirect("/about", 302, "location");
25+
}
26+
if (pathname === "/redirect-location-external") {
27+
redirect("https://react-server.dev", 302, "location");
28+
}
29+
if (pathname === "/redirect-error") {
30+
redirect("/about", 302, "error");
31+
}
1932
}

examples/file-router/pages/index.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,18 @@ export default function IndexPage() {
2222
<Link to="/redirect-api-external">External with API</Link>
2323
<br />
2424
<Link to="/redirect-about">Internal redirect to existing about page</Link>
25+
<h2>Redirect Kind:</h2>
26+
<Link to="/redirect-kind">Redirect Kind (server actions)</Link>
27+
<br />
28+
<Link to="/redirect-push">Push (middleware)</Link>
29+
<br />
30+
<Link to="/redirect-location">Location (middleware)</Link>
31+
<br />
32+
<Link to="/redirect-location-external">
33+
Location External (middleware)
34+
</Link>
35+
<br />
36+
<Link to="/redirect-error">Error (middleware)</Link>
2537
<h2>Error:</h2>
2638
<Link to="/middleware-error">Throw error in middleware</Link>
2739
</div>
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import RedirectKindButtons from "../../components/redirect-kind-buttons";
2+
3+
export default function RedirectKindPage() {
4+
return (
5+
<div>
6+
<h1>Redirect Kind</h1>
7+
<p>Test different redirect kinds via server actions:</p>
8+
<RedirectKindButtons />
9+
</div>
10+
);
11+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
"use server";
2+
3+
import { redirect } from "@lazarv/react-server";
4+
5+
export async function redirectNavigate() {
6+
redirect("/about", 302, "navigate");
7+
}
8+
9+
export async function redirectPush() {
10+
redirect("/about", 302, "push");
11+
}
12+
13+
export async function redirectLocation() {
14+
redirect("/about", 302, "location");
15+
}
16+
17+
export async function redirectLocationExternal() {
18+
redirect("https://react-server.dev", 302, "location");
19+
}
20+
21+
export async function redirectError() {
22+
redirect("/about", 302, "error");
23+
}

examples/file-router/tsconfig.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"compilerOptions": {
3+
"jsx": "react-jsx",
4+
"strict": true,
5+
"target": "ESNext",
6+
"lib": ["ESNext", "DOM", "DOM.Iterable"],
7+
"module": "ESNext",
8+
"moduleResolution": "Bundler"
9+
},
10+
"include": ["**/*.ts", "**/*.tsx"],
11+
"exclude": ["**/*.js", "**/*.mjs", "node_modules"]
12+
}

0 commit comments

Comments
 (0)