Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add in useOptimisticRoute #31

Merged
merged 1 commit into from
Oct 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions examples/kitchen-sink/src/pages/nav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,11 @@ export default function Nav() {
description="The hook that controls the router."
href="/routing/use-router"
/>
<ExampleLink
title="useOptimisticRoute"
description="The hook that sees the router's future."
href="/routing/use-optimistic-route"
/>
<ExampleLink
title="Navigation"
description="Navigation between different pages"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import Link from "@twofold/framework/link";
import { RouteInfo } from "./route-info";

export default function RouterPage() {
return (
<div>
<h1 className="text-4xl font-black tracking-tight">Optimistic Route</h1>
<div className="mt-3">
Visit the{" "}
<Link
href="/routing/use-optimistic-route/slow?a=query"
className="text-blue-500 underline"
>
slow route
</Link>
.
</div>
<div className="mt-3">
<RouteInfo />
</div>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
"use client";

import { useOptimisticRoute } from "@twofold/framework/use-optimistic-route";
import { useRouter } from "@twofold/framework/use-router";

export function RouteInfo() {
let router = useRouter();
let optimisticRoute = useOptimisticRoute();

return (
<div className="">
<table cellPadding={8} className="min-w-[400px]">
<thead>
<tr>
<th className="text-left">State</th>
<th className="text-left">Value</th>
</tr>
</thead>
<tbody>
<tr>
<td>Current path:</td>
<td>
<span className="font-mono text-sm text-gray-900">
{router.path}
</span>
</td>
</tr>
<tr>
<td>Current search params: </td>
<td>
<span className="font-mono text-sm text-gray-900">
{router.searchParams.toString()}
</span>
</td>
</tr>
<tr>
<td>Optimistic path: </td>
<td>
<span className="font-mono text-sm text-gray-900">
{optimisticRoute.path}
</span>
</td>
</tr>
<tr>
<td>Current search params: </td>
<td>
<span className="font-mono text-sm text-gray-900">
{optimisticRoute.searchParams.toString()}
</span>
</td>
</tr>
<tr>
<td>Router is transitioning: </td>
<td>{optimisticRoute.isTransitioning ? "YES" : "NO"}</td>
</tr>
</tbody>
</table>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import Link from "@twofold/framework/link";
import { RouteInfo } from "./route-info";

export default async function SlowPage() {
await new Promise((resolve) => setTimeout(resolve, 2000));

return (
<div>
<h1 className="text-4xl font-black tracking-tight">Slow route</h1>
<div className="mt-3">
This page took 2 seconds to load.{" "}
<Link
href="/routing/use-optimistic-route"
className="text-blue-500 underline"
>
Go back
</Link>{" "}
to the start page.
</div>
<div className="mt-3">
<RouteInfo />
</div>
</div>
);
}
1 change: 1 addition & 0 deletions packages/framework/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"./twofold-framework": "./src/components/twofold-framework.tsx",
"./link": "./src/components/link.tsx",
"./use-router": "./src/hooks/use-router.ts",
"./use-optimistic-route": "./src/hooks/use-optimistic-route.ts",
"./cookies": "./src/http/cookies.ts",
"./not-found": "./src/http/not-found.ts",
"./redirect": "./src/http/redirect.ts",
Expand Down
40 changes: 28 additions & 12 deletions packages/framework/src/apps/client/browser/browser-app.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import {
startTransition,
useCallback,
useEffect,
useLayoutEffect,
useMemo,
useOptimistic,
useTransition,
} from "react";
import { RoutingContext } from "../contexts/routing-context";
import { useRouterReducer } from "./router-hooks";
Expand All @@ -22,6 +23,8 @@ let origin = window.location.origin;

function Router() {
let [routerState, dispatch] = useRouterReducer();
let [optimisticPath, setOptimisticPath] = useOptimistic(routerState.path);
let [isTransitioning, startTransition] = useTransition();

let navigate = useCallback(
(
Expand All @@ -38,6 +41,7 @@ function Router() {
let newPath = `${pathname}${url.search}${url.hash}`;

startTransition(() => {
setOptimisticPath(newPath);
dispatch({
type: "NAVIGATE",
path: newPath,
Expand All @@ -46,7 +50,7 @@ function Router() {
});
});
},
[dispatch],
[dispatch, setOptimisticPath],
);

let replace = useCallback(
Expand All @@ -72,6 +76,7 @@ function Router() {
function onPopState(_event: PopStateEvent) {
let path = `${location.pathname}${location.search}${location.hash}`;
startTransition(() => {
setOptimisticPath(path);
dispatch({ type: "POP", path });
});
}
Expand All @@ -80,7 +85,7 @@ function Router() {
return () => {
window.removeEventListener("popstate", onPopState);
};
}, [dispatch]);
}, [dispatch, setOptimisticPath]);

useLayoutEffect(() => {
if (routerState.history === "push") {
Expand Down Expand Up @@ -115,6 +120,7 @@ function Router() {
window.__twofold = {
updateTree(path: string, tree: any) {
startTransition(() => {
setOptimisticPath(path);
dispatch({
type: "UPDATE",
path,
Expand All @@ -125,6 +131,7 @@ function Router() {
},
navigate(path: string) {
startTransition(() => {
setOptimisticPath(path);
dispatch({
type: "NAVIGATE",
path,
Expand All @@ -138,20 +145,19 @@ function Router() {
return () => {
window.__twofold = undefined;
};
}, [dispatch]);
}, [dispatch, setOptimisticPath]);

let url = useMemo(
() => new URL(routerState.path, origin),
[routerState.path],
);
let path = url.pathname;
let searchParams = url.searchParams;
let url = useURLFromPath(routerState.path);
let optimisticURL = useURLFromPath(optimisticPath);

return (
<ErrorBoundary>
<RoutingContext
path={path}
searchParams={searchParams}
path={url.pathname}
searchParams={url.searchParams}
optimisticPath={optimisticURL.pathname}
optimisticSearchParams={optimisticURL.searchParams}
isTransitioning={isTransitioning}
navigate={navigate}
replace={replace}
refresh={refresh}
Expand All @@ -162,3 +168,13 @@ function Router() {
</ErrorBoundary>
);
}

/**
* Turns a router path into a URL object.
*
* @param path Path is a Twofold router path: `/path?query#hash`.
* @returns
*/
function useURLFromPath(path: string) {
return useMemo(() => new URL(path, origin), [path]);
}
12 changes: 12 additions & 0 deletions packages/framework/src/apps/client/contexts/routing-context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ import { ReactNode, createContext } from "react";
export const Context = createContext({
path: "/",
searchParams: new URLSearchParams(),
optimisticPath: "/",
optimisticSearchParams: new URLSearchParams(),
isTransitioning: false,
navigate: (path: string) => {},
replace: (path: string) => {},
refresh: () => {},
Expand All @@ -12,6 +15,9 @@ export const Context = createContext({
export function RoutingContext({
path,
searchParams,
optimisticPath,
optimisticSearchParams,
isTransitioning,
navigate,
replace,
refresh,
Expand All @@ -20,6 +26,9 @@ export function RoutingContext({
}: {
path: string;
searchParams: URLSearchParams;
optimisticPath: string;
optimisticSearchParams: URLSearchParams;
isTransitioning: boolean;
navigate: (path: string) => void;
replace: (path: string) => void;
refresh: () => void;
Expand All @@ -31,6 +40,9 @@ export function RoutingContext({
value={{
path,
searchParams,
optimisticPath,
optimisticSearchParams,
isTransitioning,
notFound,
navigate,
replace,
Expand Down
7 changes: 5 additions & 2 deletions packages/framework/src/apps/client/ssr/ssr-app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ export function SSRApp({
<RoutingContext
path={url.pathname}
searchParams={url.searchParams}
optimisticPath={url.pathname}
optimisticSearchParams={url.searchParams}
isTransitioning={false}
navigate={navigate}
replace={replace}
refresh={refresh}
Expand Down Expand Up @@ -103,11 +106,11 @@ export async function render({
console.error(err);
} else {
console.error(
`An unknown error occurred while SSR rendering: ${url.pathname}`
`An unknown error occurred while SSR rendering: ${url.pathname}`,
);
}
},
}
},
);

return htmlStream;
Expand Down
12 changes: 12 additions & 0 deletions packages/framework/src/hooks/use-optimistic-route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { useContext } from "react";
import { Context } from "../apps/client/contexts/routing-context";

export function useOptimisticRoute() {
let context = useContext(Context);

return {
path: context.optimisticPath,
searchParams: context.optimisticSearchParams,
isTransitioning: context.isTransitioning,
};
}
8 changes: 5 additions & 3 deletions todo.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@

framework

- live reload should reconnect when disconnected
- rename src to app
- i think typescript should be in devDeps
- use es6 tailwind

- live reload should reconnect when disconnected
- add --log=debug flag to show debug logs
- eslint upgrade
- rename src to app
- tailwind v4/lightening css

- useOptimisticRoute -> path and params

Expand Down