Skip to content

Commit

Permalink
fix(react-server): consistent history pathname encoding + support `Li…
Browse files Browse the repository at this point in the history
…nk.activeProps` (#276)
  • Loading branch information
hi-ogawa committed Apr 8, 2024
1 parent 6172989 commit 039e814
Show file tree
Hide file tree
Showing 9 changed files with 100 additions and 16 deletions.
24 changes: 18 additions & 6 deletions packages/react-server/examples/basic/e2e/basic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -739,13 +739,25 @@ test("dynamic routes", async ({ page }) => {
await page.getByText("pathname: /test/dynamic/abc/def").click();
await page.getByText('params: {"id":"abc","nested":"def"}').click();

await page.getByRole("link", { name: "/test/dynamic/🎸 + 🎷 = 🎶" }).click();
await page.getByText('params: {"id":"🎸 + 🎷 = 🎶"}').click();
await page.waitForURL("/test/dynamic/🎸 + 🎷 = 🎶");
await page.getByRole("link", { name: "/test/dynamic/✅" }).click();
await page.getByText('params: {"id":"✅"}').click();
await page.waitForURL("/test/dynamic/✅");
await expect(
page.getByRole("link", { name: "/test/dynamic/✅" }),
).toHaveAttribute("aria-current", "page");
await expect(
page.getByRole("link", { name: "/test/dynamic/%E2%9C%85" }),
).toHaveAttribute("aria-current", "page");

await page.getByRole("link", { name: "/test/dynamic/%F0%9F%8E%B8%" }).click();
await page.getByText('params: {"id":"🎸 + 🎷 = 🎶"}').click();
await page.waitForURL("/test/dynamic/🎸 + 🎷 = 🎶");
await page.getByRole("link", { name: "/test/dynamic/%E2%9C%85" }).click();
await page.getByText('params: {"id":"✅"}').click();
await page.waitForURL("/test/dynamic/✅");
await expect(
page.getByRole("link", { name: "/test/dynamic/✅" }),
).toHaveAttribute("aria-current", "page");
await expect(
page.getByRole("link", { name: "/test/dynamic/%E2%9C%85" }),
).toHaveAttribute("aria-current", "page");
});

test("full client route", async ({ page }) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
import { Link } from "@hiogawa/react-server/client";

export function NavMenu(props: { links: string[]; className?: string }) {
export function NavMenu(props: {
links: string[];
className?: string;
activeProps?: JSX.IntrinsicElements["a"];
}) {
return (
<ul className={props.className}>
{props.links.map((e) => (
<Link
key={e}
href={e}
className="antd-link self-start justify-self-start"
activeProps={props.activeProps}
>
<li className="flex items-center">
<span className="text-lg pr-2 select-none"></span>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
"use client";

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

export function ClientLocation() {
const location = useRouter((s) => s.location);
return <>{location.pathname}</>;
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { PageProps } from "@hiogawa/react-server/server";
import { ClientLocation } from "./_cilent";

export function TestDynamic({
file: file,
Expand All @@ -10,7 +11,13 @@ export function TestDynamic({
return (
<div className="flex flex-col gap-1">
<div>file: {file}</div>
<div>pathname: {new URL(props.request.url).pathname}</div>
<div>
pathname:{" "}
{props.request.url.slice(new URL(props.request.url).origin.length)}
</div>
<div>
pathname (client): <ClientLocation />
</div>
<div>params: {JSON.stringify(props.params)}</div>
</div>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,14 @@ export default function Layout(props: LayoutProps) {
"/test/dynamic",
"/test/dynamic/static",
"/test/dynamic/abc",
"/test/dynamic/🎸 + 🎷 = 🎶",
"/test/dynamic/" + encodeURI("🎸 + 🎷 = 🎶"),
// these two should work same inside the application?
"/test/dynamic/✅",
"/test/dynamic/" + encodeURI("✅"),
"/test/dynamic/abc/def",
]}
activeProps={{
"aria-current": "page",
}}
/>
<div>{props.children}</div>
</div>
Expand Down
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.16",
"version": "0.1.17-pre.0",
"license": "MIT",
"type": "module",
"exports": {
Expand Down
10 changes: 7 additions & 3 deletions packages/react-server/src/entry/browser.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { createDebug, memoize, tinyassert } from "@hiogawa/utils";
import { createBrowserHistory } from "@tanstack/history";
import React from "react";
import reactDomClient from "react-dom/client";
import {
Expand All @@ -13,7 +12,12 @@ import { injectActionId } from "../features/server-action/utils";
import { wrapStreamRequestUrl } from "../features/server-component/utils";
import { initializeWebpackBrowser } from "../features/use-client/browser";
import { RootErrorBoundary } from "../lib/client/error-boundary";
import { Router, RouterContext, useRouter } from "../lib/client/router";
import {
Router,
RouterContext,
createEncodedBrowserHistory,
useRouter,
} from "../lib/client/router";
import { __global } from "../lib/global";
import type { CallServerCallback } from "../lib/types";
import { readStreamScript } from "../utils/stream-script";
Expand All @@ -31,7 +35,7 @@ export async function start() {
"react-server-dom-webpack/client.browser"
);

const history = createBrowserHistory();
const history = createEncodedBrowserHistory();
const router = new Router(history);

let __setLayout: (v: Promise<ServerRouterData>) => void;
Expand Down
19 changes: 18 additions & 1 deletion packages/react-server/src/lib/client/link.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,33 @@ import { useRouter } from "./router";

interface LinkProps {
revalidate?: boolean;
activeProps?: JSX.IntrinsicElements["a"];
}

function encodeHref(href: string) {
const url = new URL(href, "https://test.local");
return url.href.slice(url.origin.length);
}

function matchHref(href: string, pathname: string) {
pathname = pathname.replaceAll(/\/*$/g, "/");
href = href.replaceAll(/\/*$/g, "/");
return pathname.startsWith(href);
}

export function Link({
revalidate,
activeProps,
...props
}: JSX.IntrinsicElements["a"] & { href: string } & LinkProps) {
const history = useRouter((s) => s.history);
const pathname = useRouter((s) => s.location.pathname);
const href = encodeHref(props.href);

return (
<a
{...props}
{...(matchHref(href, pathname) ? activeProps : {})}
onClick={(e) => {
const target = e.currentTarget.target;
if (
Expand All @@ -26,7 +42,7 @@ export function Link({
(!target || target === "_self")
) {
e.preventDefault();
history.push(props.href!, revalidate ? routerRevalidate() : {});
history.push(href, revalidate ? routerRevalidate() : {});
}
}}
/>
Expand All @@ -35,6 +51,7 @@ export function Link({

export function LinkForm({
revalidate,
activeProps,
...props
}: JSX.IntrinsicElements["form"] & { action: string } & LinkProps) {
const history = useRouter((s) => s.history);
Expand Down
29 changes: 28 additions & 1 deletion packages/react-server/src/lib/client/router.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import type { HistoryLocation, RouterHistory } from "@tanstack/history";
import {
type HistoryLocation,
type RouterHistory,
createBrowserHistory,
} from "@tanstack/history";
import React from "react";
import { TinyStore, useStore } from "./store-utils";

Expand Down Expand Up @@ -37,3 +41,26 @@ export function useRouter<U = RouterState>(select?: (v: RouterState) => U) {
const router = React.useContext(RouterContext);
return useStore(router.store, select);
}

export function createEncodedBrowserHistory() {
// patch push/replace so that location object consistently includes encoded url
// (i.e. history.push("/✅") should set { pathname: "/%E2%9C%85" } as state)
// cf.
// https://github.com/remix-run/react-router/pull/9477
// https://github.com/TanStack/router/issues/1441

const history = createBrowserHistory();

function encode(href: string) {
const url = new URL(href, window.location.origin);
return url.href.slice(url.origin.length);
}

function wrapEncode(f: typeof history.push): typeof history.push {
return (path, state) => f(encode(path), state);
}

history.push = wrapEncode(history.push);
history.replace = wrapEncode(history.replace);
return history;
}

0 comments on commit 039e814

Please sign in to comment.