Skip to content

Commit

Permalink
feat(react-server): router transition state (#224)
Browse files Browse the repository at this point in the history
  • Loading branch information
hi-ogawa authored Mar 25, 2024
1 parent 9941a33 commit 4dadb80
Show file tree
Hide file tree
Showing 23 changed files with 367 additions and 77 deletions.
44 changes: 44 additions & 0 deletions packages/react-server/examples/basic/e2e/basic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,50 @@ test("navigation", async ({ page }) => {
await checkClientState();
});

test("ServerTransitionContext.isPending", async ({ page }) => {
checkNoError(page);

await page.goto("/test/transition");
await page.getByText("hydrated: true").click();

await expect(page.getByRole("link", { name: "About" })).toHaveAttribute(
"aria-selected",
"true",
);
await expect(
page.getByRole("link", { name: "Posts (2.0 sec)" }),
).toHaveAttribute("aria-selected", "false");

await page.getByText("Took 0 sec to load.").click();
await page.getByRole("link", { name: "Posts (2.0 sec)" }).click();
await expect(page.getByRole("link", { name: "About" })).toHaveAttribute(
"aria-selected",
"false",
);
await expect(
page.getByRole("link", { name: "Posts (2.0 sec)" }),
).toHaveAttribute("aria-selected", "true");
await expect(page.getByRole("link", { name: "Posts (2.0 sec)" })).toHaveClass(
/opacity-50/,
);
await expect(
page.getByRole("link", { name: "Posts (2.0 sec)" }),
).not.toHaveClass(/opacity-50/);
await page.getByText("Took 2 sec to load.").click();
});

test("ServerTransitionContext.isActionPending", async ({ page }) => {
checkNoError(page);

await page.goto("/test/transition");
await page.getByText("hydrated: true").click();

await expect(page.getByText("Count: 0")).not.toHaveClass(/opacity-50/);
await page.getByRole("button", { name: "-1 (2.0 sec)" }).click();
await expect(page.getByText("Count: 0")).toHaveClass(/opacity-50/);
await expect(page.getByText("Count: -1")).not.toHaveClass(/opacity-50/);
});

test("Link modifier", async ({ page, context }) => {
checkNoError(page);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ npx esbuild ../../dist/server/index.js \
--outfile=dist/server/index.js \
--metafile=dist/esbuild-metafile.json \
--define:process.env.NODE_ENV='"production"' \
--log-override:ignored-bare-import=silent \
--bundle \
--minify \
--format=esm \
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,17 @@ rm -rf .vercel/output/static/index.html
mkdir -p .vercel/output/functions/index.func
cp .vc-config.json .vercel/output/functions/index.func/.vc-config.json

# NOTE: silence `ignored-bare-import` for
# https://rollupjs.org/configuration-options/#output-hoisttransitiveimports
# https://rollupjs.org/faqs/#why-do-additional-imports-turn-up-in-my-entry-chunks-when-code-splitting
# https://github.com/evanw/esbuild/issues/2334

npx esbuild ../../dist/server/index.js \
--outfile=.vercel/output/functions/index.func/index.mjs \
--metafile=dist/esbuild-metafile.json \
--define:process.env.NODE_ENV='"production"' \
--banner:js="import { createRequire } from 'module'; const require = createRequire(import.meta.url);" \
--log-override:ignored-bare-import=silent \
--bundle \
--minify \
--format=esm \
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
"use client";

import { useServerTransitionState } from "@hiogawa/react-server/client";
import { cls } from "./utils";

export function GlobalProgress() {
const ctx = useServerTransitionState();

return (
<>
<div
className={cls(
"antd-spin w-5 h-5 text-colorInfo transition duration-500",
ctx.isPending ? "opacity-100" : "opacity-0",
)}
></div>
<div
className={cls(
"antd-spin w-5 h-5 text-colorWarning transition duration-500",
ctx.isActionPending ? "opacity-100" : "opacity-0",
)}
></div>
</>
);
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import { GlobalProgress } from "./global-progress";

export function Header() {
return (
<div className="flex items-center gap-2">
<div className="flex items-center gap-3">
<h1 className="text-lg font-bold">RSC Experiment</h1>
<a
className="antd-link i-ri-github-line w-6 h-6"
href="https://github.com/hi-ogawa/vite-plugins/tree/main/packages/react-server"
target="_blank"
/>
<GlobalProgress />
</div>
);
}
16 changes: 10 additions & 6 deletions packages/react-server/examples/basic/src/components/nav-menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,18 @@ import { Link } from "@hiogawa/react-server/client";

export function NavMenu(props: { links: string[]; className?: string }) {
return (
<ul className={props.className ?? "flex flex-col items-start gap-1"}>
<ul className={props.className}>
{props.links.map((e) => (
<li key={e} className="antd-link flex items-stretch">
<Link href={e}>
<span className="text-lg pr-2"></span>
<Link
key={e}
href={e}
className="antd-link self-start justify-self-start"
>
<li className="flex items-center">
<span className="text-lg pr-2 select-none"></span>
{e}
</Link>
</li>
</li>
</Link>
))}
</ul>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const cls = (...args: unknown[]) => args.filter(Boolean).join(" ");
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
"use client";

import { __global } from "@hiogawa/react-server";
import { useRouter } from "@hiogawa/react-server/client";

// TODO: server action + redirect
export function SearchInput() {
const router = useRouter();

return (
<form
onSubmit={(e) => {
e.preventDefault();
const q = e.currentTarget["q"].value;
if (typeof q === "string") {
__global.history.push(`/demo/waku_02/${q.toLowerCase()}`);
router.history.push(`/demo/waku_02/${q.toLowerCase()}`);
}
}}
>
Expand Down
5 changes: 4 additions & 1 deletion packages/react-server/examples/basic/src/routes/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@ export default function Layout(props: LayoutProps) {
<body>
<div className="p-4 flex flex-col gap-2">
<Header />
<NavMenu links={["/", "/test", "/demo/waku_02"]} />
<NavMenu
className="flex flex-col items-start gap-1"
links={["/", "/test", "/demo/waku_02"]}
/>
{props.children}
</div>
</body>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export default async function Layout(props: LayoutProps) {
<div className="flex flex-col gap-2">
<h2 className="text-lg">Test</h2>
<NavMenu
className="grid grid-cols-2 w-xs gap-1"
className="grid grid-cols-3 w-lg gap-1"
links={[
"/test",
"/test/other",
Expand All @@ -17,6 +17,7 @@ export default async function Layout(props: LayoutProps) {
"/test/css",
"/test/error",
"/test/not-found",
"/test/transition",
]}
/>
<div className="flex items-center gap-2 w-sm text-sm">
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
"use server";

import { sleep } from "@hiogawa/utils";

let counter = 0;

export function getCounter() {
return counter;
}

export async function changeCounter(formData: FormData) {
const delta = Number(formData.get("delta"));
if (delta === -1) {
await sleep(2000);
} else {
await sleep(200);
}
counter += delta;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
"use client";

import {
Link,
useRouter,
useServerTransitionState,
} from "@hiogawa/react-server/client";
import { cls } from "../../../components/utils";
import { changeCounter } from "./_action";

const TABS = [
["/test/transition", "About"],
["/test/transition?sleep=2000", "Posts (2.0 sec)"],
["/test/transition?sleep=200", "Contact (0.2 sec)"],
] as const;

export function Tablist() {
const { isPending } = useServerTransitionState();
const router = useRouter();

return (
<ul className="antd-tablist flex gap-5 px-2">
{TABS.map(([href, name]) => (
<Link
key={href}
href={href}
className={cls(
"antd-tab py-1.5",
href === router.history.location.href && isPending && "opacity-50",
)}
aria-selected={href === router.history.location.href}
>
<li key={href}>{name}</li>
</Link>
))}
</ul>
);
}

export function Counter(props: { value: number }) {
const { isActionPending } = useServerTransitionState();

return (
<form action={changeCounter} className="flex flex-col items-start gap-2">
<div className="flex items-center gap-2">
<button
className="antd-btn antd-btn-default px-2"
name="delta"
value={-1}
>
-1 (2.0 sec)
</button>
<button
className="antd-btn antd-btn-default px-2"
name="delta"
value={+1}
>
+1 (0.2 sec)
</button>
<div className={cls(isActionPending && "opacity-50")}>
Count: {props.value}
</div>
</div>
</form>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import type { LayoutProps } from "@hiogawa/react-server/server";
import { getCounter } from "./_action";
import { Counter, Tablist } from "./_client";

// similar demo
// https://react.dev/reference/react/useTransition#marking-a-state-update-as-a-non-blocking-transition

export default async function Layout(props: LayoutProps) {
return (
<div className="w-lg flex flex-col gap-4 p-4">
<div className="border p-3 flex flex-col gap-2">
<h4 className="font-bold">Navigation State</h4>
<Tablist />
<div className="p-2">{props.children}</div>
</div>
<div className="border p-3 flex flex-col gap-2">
<h4 className="font-bold">Action state</h4>
<Counter value={getCounter()} />
</div>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import type { PageProps } from "@hiogawa/react-server/server";
import { sleep } from "@hiogawa/utils";

export default async function Page(props: PageProps) {
const url = new URL(props.request.url);
const ms = Number(url.searchParams.get("sleep"));
await sleep(ms);
return <p>Took {ms / 1000} sec to load.</p>;
}
2 changes: 1 addition & 1 deletion packages/react-server/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@hiogawa/react-server",
"version": "0.1.2",
"version": "0.1.3",
"license": "MIT",
"type": "module",
"exports": {
Expand Down
1 change: 1 addition & 0 deletions packages/react-server/src/client.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
"use client";

export { Link } from "./lib/components/link";
export { useServerTransitionState, useRouter } from "./lib/client/router";
Loading

0 comments on commit 4dadb80

Please sign in to comment.