Skip to content
Open
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 .changeset/mean-oranges-rest.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@tailor-platform/app-shell": patch
---

Upgrade AppShell to React Router v8 and raise the minimum supported `react` / `react-dom` version to `19.2.7`.
4 changes: 2 additions & 2 deletions docs/concepts/routing-navigation.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ description: Learn how to navigate between pages using React Router hooks and co

# Routing and Navigation

Because AppShell manages routing internally, it has its own instance of the RouterProvider from `react-router`.
Because AppShell manages routing internally, it owns the application's `RouterProvider` instance.

To use client-side navigation from AppShell pages, instead of installing `react-router` to package.json and importing components and hooks from your application's dependencies, we must use the exports from `@tailor-platform/app-shell`, such as `Link` component, or `useParams`, `useNavigate` hooks. These components and hooks internally use the correct context for the AppShell `RouterProvider` to properly recognize the intent.
To use client-side navigation from AppShell pages, use the exports from `@tailor-platform/app-shell`, such as the `Link` component or `useParams` / `useNavigate` hooks, instead of importing those primitives from your app's `react-router` dependency. This keeps every hook and component on the same router context; mixing AppShell with a separate `react-router` import can fail at runtime, especially across mismatched major versions.

## Exported React Router Hooks

Expand Down
6 changes: 3 additions & 3 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@
"lucide-react": "^1.8.0",
"openai": "6.45.0",
"papaparse": "^5.5.3",
"react-router": "^7.18.2",
"react-router": "^8.3.0",
"sonner": "^2.0.7",
"tailwind-merge": "^3.6.0"
},
Expand Down Expand Up @@ -90,7 +90,7 @@
"vitest": "catalog:"
},
"peerDependencies": {
"react": ">= 19",
"react-dom": ">= 19"
"react": ">=19.2.7",
"react-dom": ">=19.2.7"
}
}
10 changes: 3 additions & 7 deletions packages/core/src/routing/router.test.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { act, cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react";
import { RouterContainer } from "./router";
import { RouterContainer, routerFactories } from "./router";
import { AppShellConfigContext, AppShellDataContext } from "@/contexts/appshell-context";
import * as ReactRouter from "react-router";
import { Link, Outlet, useNavigate } from "react-router";
import {
defineModule,
Expand Down Expand Up @@ -518,7 +517,7 @@ describe("RouterContainer with AuthProvider", () => {
isReady: false,
};
const listeners: Array<(event: { type: string }) => void> = [];
const createMemoryRouterSpy = vi.spyOn(ReactRouter, "createMemoryRouter");
const createMemoryRouterSpy = vi.spyOn(routerFactories, "createMemoryRouter");

const authClient = createMockAuthClient(snapshot, {
getState: vi.fn(() => snapshot),
Expand All @@ -541,8 +540,6 @@ describe("RouterContainer with AuthProvider", () => {
});

expect(await screen.findByText("Loading...")).toBeDefined();
// With guardComponent now applied in AuthProvider (not the router),
// RouterContainer is not mounted until auth is ready.
expect(createMemoryRouterSpy).toHaveBeenCalledTimes(0);

act(() => {
Expand All @@ -557,15 +554,14 @@ describe("RouterContainer with AuthProvider", () => {
});

expect(await screen.findByText("Home")).toBeDefined();
// Router is created once when children first render after auth is ready.
expect(createMemoryRouterSpy).toHaveBeenCalledTimes(1);
} finally {
createMemoryRouterSpy.mockRestore();
}
});

it("recreates the router when route-defining config changes", async () => {
const createMemoryRouterSpy = vi.spyOn(ReactRouter, "createMemoryRouter");
const createMemoryRouterSpy = vi.spyOn(routerFactories, "createMemoryRouter");
const module = defineModule({
path: "dashboard",
component: () => <div>Dashboard</div>,
Expand Down
14 changes: 11 additions & 3 deletions packages/core/src/routing/router.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { type PropsWithChildren, type ReactNode, useMemo } from "react";
import { Outlet, createMemoryRouter, createBrowserRouter, RouterProvider } from "react-router";
import { Outlet, createMemoryRouter, createBrowserRouter } from "react-router";
import { RouterProvider } from "react-router/dom";
import type { RouteObject } from "react-router";
import { createContentRoutes, wrapErrorBoundary } from "./routes";
import { useAppShellConfig, type RootConfiguration } from "@/contexts/appshell-context";
Expand Down Expand Up @@ -68,6 +69,13 @@ export type RouterContainerProps =
initialEntries: Array<string>;
};

// Small indirection so tests can observe router creation under react-router v8's
// ESM-only exports, which Vitest cannot spy on directly.
export const routerFactories = {
createBrowserRouter,
createMemoryRouter,
};

export const RouterContainer = (props: PropsWithChildren<RouterContainerProps>) => {
const { children } = props;
const { configurations } = useAppShellConfig();
Expand Down Expand Up @@ -101,11 +109,11 @@ export const RouterContainer = (props: PropsWithChildren<RouterContainerProps>)
const router = useMemo(
() =>
props.memory
? createMemoryRouter(routes, {
? routerFactories.createMemoryRouter(routes, {
basename,
...(initialEntries ? { initialEntries } : {}),
})
: createBrowserRouter(routes, {
: routerFactories.createBrowserRouter(routes, {
basename,
}),
[basename, initialEntries, props.memory, routes],
Expand Down
Loading
Loading