diff --git a/packages/react-router-devtools/package.json b/packages/react-router-devtools/package.json
index 0b9ab50..7fe3b0d 100644
--- a/packages/react-router-devtools/package.json
+++ b/packages/react-router-devtools/package.json
@@ -2,7 +2,7 @@
"name": "react-router-devtools",
"description": "Devtools for React Router - debug, trace, find hydration errors, catch bugs and inspect server/client data with react-router-devtools",
"author": "Alem Tuzlak",
- "version": "6.1.0",
+ "version": "6.2.0",
"license": "MIT",
"keywords": [
"react-router",
@@ -131,6 +131,7 @@
"@babel/traverse": "^7.28.5",
"@babel/types": "^7.28.5",
"@radix-ui/react-accordion": "^1.2.12",
+ "@tanstack/devtools-client": "^0.0.5",
"@tanstack/devtools-event-client": "^0.4.0",
"@tanstack/devtools-vite": "^0.4.1",
"@tanstack/react-devtools": "^0.9.1",
diff --git a/packages/react-router-devtools/src/client/components/TabContent.tsx b/packages/react-router-devtools/src/client/components/TabContent.tsx
index accd18f..e3c38da 100644
--- a/packages/react-router-devtools/src/client/components/TabContent.tsx
+++ b/packages/react-router-devtools/src/client/components/TabContent.tsx
@@ -1,10 +1,11 @@
+import { memo } from "react"
import { useStyles } from "../styles/use-styles.js"
interface TabContentProps {
children: React.ReactNode
}
-export const TabContent = ({ children }: TabContentProps) => {
+export const TabContent = memo(({ children }: TabContentProps) => {
const { styles } = useStyles()
return
{children}
-}
+})
diff --git a/packages/react-router-devtools/src/client/components/Tag.tsx b/packages/react-router-devtools/src/client/components/Tag.tsx
index 44f4267..94afe06 100644
--- a/packages/react-router-devtools/src/client/components/Tag.tsx
+++ b/packages/react-router-devtools/src/client/components/Tag.tsx
@@ -1,4 +1,4 @@
-import type { ReactNode } from "react"
+import { type ReactNode, memo } from "react"
import { cx, useStyles } from "../styles/use-styles.js"
export const TAG_COLORS = {
@@ -16,7 +16,7 @@ interface TagProps {
size?: "small" | "default"
}
-const Tag = ({ color, children, className, size = "default" }: TagProps) => {
+const Tag = memo(({ color, children, className, size = "default" }: TagProps) => {
const { styles } = useStyles()
return (
{
{children}
)
-}
+})
export { Tag }
diff --git a/packages/react-router-devtools/src/client/components/icon/Icon.tsx b/packages/react-router-devtools/src/client/components/icon/Icon.tsx
index 2002fb8..e83aaa3 100644
--- a/packages/react-router-devtools/src/client/components/icon/Icon.tsx
+++ b/packages/react-router-devtools/src/client/components/icon/Icon.tsx
@@ -1,4 +1,4 @@
-import type { SVGProps } from "react"
+import { type SVGProps, memo } from "react"
import { cx } from "../../styles/use-styles.js"
import { useStyles } from "../../styles/use-styles.js"
import type { IconName } from "./icons/types.js"
@@ -30,7 +30,7 @@ const strokeIcon: Partial[] = []
* Icon component wrapper for SVG icons.
* @returns SVG icon as a react component
*/
-export const Icon = ({ name, title, testId, className, size = "sm", ...props }: IconProps) => {
+export const Icon = memo(({ name, title, testId, className, size = "sm", ...props }: IconProps) => {
const { styles } = useStyles()
const iconSize = IconSize[size]
const isEmptyFill = emptyFill.includes(name)
@@ -316,4 +316,4 @@ export const Icon = ({ name, title, testId, className, size = "sm", ...props }:
)
-}
+})
diff --git a/packages/react-router-devtools/src/client/components/jsonRenderer.tsx b/packages/react-router-devtools/src/client/components/jsonRenderer.tsx
index d741327..661aae2 100644
--- a/packages/react-router-devtools/src/client/components/jsonRenderer.tsx
+++ b/packages/react-router-devtools/src/client/components/jsonRenderer.tsx
@@ -14,7 +14,7 @@ const isPromise = (value: any): value is Promise => {
return value && typeof value.then === "function"
}
-const JsonRendererComponent = ({ data, expansionLevel }: JsonRendererProps) => {
+const JsonRendererComponent = memo(({ data, expansionLevel }: JsonRendererProps) => {
const { styles } = useStyles()
const { settings } = useSettingsContext()
const ref = useRef(true)
@@ -71,7 +71,7 @@ const JsonRendererComponent = ({ data, expansionLevel }: JsonRendererProps) => {
return (
)
-}
+})
const JsonRenderer = memo(JsonRendererComponent)
diff --git a/packages/react-router-devtools/src/client/embedded-dev-tools.tsx b/packages/react-router-devtools/src/client/embedded-dev-tools.tsx
index 6b83043..fe89ce4 100644
--- a/packages/react-router-devtools/src/client/embedded-dev-tools.tsx
+++ b/packages/react-router-devtools/src/client/embedded-dev-tools.tsx
@@ -1,5 +1,5 @@
import clsx from "clsx"
-import { useEffect, useState } from "react"
+import { memo, useEffect, useState } from "react"
import { RDTContextProvider } from "./context/RDTContext.js"
import { useFindRouteOutlets } from "./hooks/useReactTreeListeners.js"
import { useSetRouteBoundaries } from "./hooks/useSetRouteBoundaries.js"
@@ -10,18 +10,25 @@ import { Tabs } from "./layout/Tabs.js"
import type { ReactRouterDevtoolsProps } from "./react-router-dev-tools.js"
// Import to ensure global reset styles are injected
import "./styles/use-styles.js"
+import { devtoolsEventClient } from "@tanstack/devtools-client"
import { RequestProvider } from "./context/requests/request-context.js"
import { REACT_ROUTER_DEV_TOOLS } from "./utils/storage.js"
-
export interface EmbeddedDevToolsProps extends ReactRouterDevtoolsProps {
mainPanelClassName?: string
className?: string
}
-const Embedded = ({ mainPanelClassName, className }: EmbeddedDevToolsProps) => {
+const Embedded = memo(({ mainPanelClassName, className }: EmbeddedDevToolsProps) => {
useTimelineHandler()
useFindRouteOutlets()
useSetRouteBoundaries()
+ const [isOpen, setIsOpen] = useState(true)
+ useEffect(() => {
+ const cleanup = devtoolsEventClient.on("trigger-toggled", (e) => {
+ setIsOpen(e.payload.isOpen)
+ })
+ return cleanup
+ }, [])
return (
{
}}
className={clsx("react-router-dev-tools", "h-full flex-row w-full", className)}
>
-
-
-
-
+ {isOpen ? (
+
+
+
+
+ ) : null}
)
-}
+})
let hydrating = true
diff --git a/packages/react-router-devtools/src/client/hooks/useReactTreeListeners.ts b/packages/react-router-devtools/src/client/hooks/useReactTreeListeners.ts
index df2e922..b4127c3 100644
--- a/packages/react-router-devtools/src/client/hooks/useReactTreeListeners.ts
+++ b/packages/react-router-devtools/src/client/hooks/useReactTreeListeners.ts
@@ -21,7 +21,7 @@ export function useFindRouteOutlets() {
const styleNearestElement = useCallback((fiberNode: any) => {
if (!fiberNode) return
- if (fiberNode.stateNode) {
+ if (typeof fiberNode?.stateNode?.classList?.add === "function") {
return fiberNode.stateNode.classList.add(ROUTE_CLASS)
}
styleNearestElement(fiberNode.child)
diff --git a/packages/react-router-devtools/src/client/layout/ContentPanel.tsx b/packages/react-router-devtools/src/client/layout/ContentPanel.tsx
index 332ecfb..522b2d5 100644
--- a/packages/react-router-devtools/src/client/layout/ContentPanel.tsx
+++ b/packages/react-router-devtools/src/client/layout/ContentPanel.tsx
@@ -1,10 +1,10 @@
-import { Fragment } from "react"
+import { Fragment, memo } from "react"
import { useTabs } from "../hooks/useTabs.js"
import { cx } from "../styles/use-styles.js"
import { useStyles } from "../styles/use-styles.js"
import { TimelineTab } from "../tabs/TimelineTab.js"
-const ContentPanel = () => {
+const ContentPanel = memo(() => {
const { Component, hideTimeline, activeTab } = useTabs()
const { styles } = useStyles()
@@ -29,6 +29,6 @@ const ContentPanel = () => {
)}
)
-}
+})
export { ContentPanel }
diff --git a/packages/react-router-devtools/src/client/layout/MainPanel.tsx b/packages/react-router-devtools/src/client/layout/MainPanel.tsx
index b06cc53..b21f0bc 100644
--- a/packages/react-router-devtools/src/client/layout/MainPanel.tsx
+++ b/packages/react-router-devtools/src/client/layout/MainPanel.tsx
@@ -1,3 +1,4 @@
+import { memo } from "react"
import { cx } from "../styles/use-styles.js"
import { useStyles } from "../styles/use-styles.js"
@@ -8,7 +9,7 @@ interface MainPanelProps {
className?: string
}
-const MainPanel = ({ children, isOpen, className }: MainPanelProps) => {
+const MainPanel = memo(({ children, isOpen, className }: MainPanelProps) => {
const { styles } = useStyles()
return (
@@ -26,6 +27,6 @@ const MainPanel = ({ children, isOpen, className }: MainPanelProps) => {
{children}
)
-}
+})
export { MainPanel }
diff --git a/packages/react-router-devtools/src/client/layout/Tabs.tsx b/packages/react-router-devtools/src/client/layout/Tabs.tsx
index 7246978..4e18c23 100644
--- a/packages/react-router-devtools/src/client/layout/Tabs.tsx
+++ b/packages/react-router-devtools/src/client/layout/Tabs.tsx
@@ -1,3 +1,4 @@
+import { memo } from "react"
import { useSettingsContext } from "../context/useRDTContext.js"
import { useHorizontalScroll } from "../hooks/useHorizontalScroll.js"
import { useTabs } from "../hooks/useTabs.js"
@@ -11,39 +12,41 @@ declare global {
}
}
-const Tab = ({
- tab,
- activeTab,
- className,
- onClick,
-}: {
- tab: TabType
- activeTab?: string
- className?: string
- onClick?: () => void
-}) => {
- const { setSettings } = useSettingsContext()
- const { styles } = useStyles()
+const Tab = memo(
+ ({
+ tab,
+ activeTab,
+ className,
+ onClick,
+ }: {
+ tab: TabType
+ activeTab?: string
+ className?: string
+ onClick?: () => void
+ }) => {
+ const { setSettings } = useSettingsContext()
+ const { styles } = useStyles()
- return (
-
- )
-}
+ return (
+
+ )
+ }
+)
-const Tabs = () => {
+const Tabs = memo(() => {
const { settings } = useSettingsContext()
const { styles } = useStyles()
const { activeTab } = settings
@@ -59,6 +62,6 @@ const Tabs = () => {
)
-}
+})
export { Tabs }
diff --git a/packages/react-router-devtools/src/client/tabs/NetworkTab.tsx b/packages/react-router-devtools/src/client/tabs/NetworkTab.tsx
index 073b8ad..1475dc4 100644
--- a/packages/react-router-devtools/src/client/tabs/NetworkTab.tsx
+++ b/packages/react-router-devtools/src/client/tabs/NetworkTab.tsx
@@ -7,7 +7,6 @@ import { useStyles } from "../styles/use-styles.js"
export const NetworkTab = () => {
const { styles } = useStyles()
const { requests, removeAllRequests, isLimitReached } = useRequestContext()
-
return (
{
const { revalidate, state } = useRevalidator()
// Memoize reversed routes to avoid creating new array on every render
- const reversedRoutes = useMemo(() => routes.toReversed(), [routes])
+ const reversedRoutes = useMemo(() => [...routes.toReversed()], [routes])
return (
<>
diff --git a/packages/react-router-devtools/src/client/tabs/RoutesTab.tsx b/packages/react-router-devtools/src/client/tabs/RoutesTab.tsx
index 20cd21e..de55b57 100644
--- a/packages/react-router-devtools/src/client/tabs/RoutesTab.tsx
+++ b/packages/react-router-devtools/src/client/tabs/RoutesTab.tsx
@@ -50,7 +50,7 @@ const RoutesTab = () => {
}
// Add root from manifest
- routeObject.root = window.__reactRouterManifest?.routes?.root
+ routeObject.root = { ...window.__reactRouterManifest?.routes?.root }
// Update tree view routes with merged data
setTreeRoutes(createRouteTree(routeObject))
@@ -63,9 +63,7 @@ const RoutesTab = () => {
// Request routes info from the server AFTER listener is set up
eventClient.emit("routes-tab-mounted", {})
- return () => {
- unsubscribe()
- }
+ return unsubscribe
}, [])
return (
diff --git a/packages/react-router-devtools/src/client/tabs/TimelineTab.tsx b/packages/react-router-devtools/src/client/tabs/TimelineTab.tsx
index 5a93728..68d0595 100644
--- a/packages/react-router-devtools/src/client/tabs/TimelineTab.tsx
+++ b/packages/react-router-devtools/src/client/tabs/TimelineTab.tsx
@@ -1,3 +1,4 @@
+import { memo } from "react"
import { TabContent } from "../components/TabContent.js"
import { TabHeader } from "../components/TabHeader.js"
import { type TAG_COLORS, Tag } from "../components/Tag.js"
@@ -17,7 +18,7 @@ const Translations: Record
= {
FETCHER_RESPONSE: "Fetcher action response",
}
-const RedirectEventComponent = (event: RedirectEvent) => {
+const RedirectEventComponent = memo((event: RedirectEvent) => {
const { styles } = useStyles()
return (
@@ -31,9 +32,9 @@ const RedirectEventComponent = (event: RedirectEvent) => {
)}
)
-}
+})
-const FormEventComponent = (event: FormEvent) => {
+const FormEventComponent = memo((event: FormEvent) => {
const { styles } = useStyles()
const isRedirect = event.type === "ACTION_REDIRECT"
const responseData = event.responseData
@@ -73,7 +74,7 @@ const FormEventComponent = (event: FormEvent) => {
)
-}
+})
export const METHOD_COLORS: Record = {
GET: "GREEN",
diff --git a/packages/react-router-devtools/src/client/tabs/index.tsx b/packages/react-router-devtools/src/client/tabs/index.tsx
index 2cdf0b1..47a2c35 100644
--- a/packages/react-router-devtools/src/client/tabs/index.tsx
+++ b/packages/react-router-devtools/src/client/tabs/index.tsx
@@ -19,21 +19,21 @@ export const tabs = [
name: "Active page",
icon: ,
id: "page",
- component: PageTab,
+ component: () => ,
hideTimeline: false,
},
{
name: "Routes",
icon: ,
id: "routes",
- component: RoutesTab,
+ component: () => ,
hideTimeline: false,
},
{
name: "Network",
icon: ,
id: "network",
- component: NetworkTab,
+ component: () => ,
hideTimeline: true,
},
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 29d4a26..a16af5b 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -244,6 +244,9 @@ importers:
'@radix-ui/react-accordion':
specifier: ^1.2.12
version: 1.2.12(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+ '@tanstack/devtools-client':
+ specifier: ^0.0.5
+ version: 0.0.5
'@tanstack/devtools-event-client':
specifier: ^0.4.0
version: 0.4.0
diff --git a/test-apps/react-router-vite/app/root.tsx b/test-apps/react-router-vite/app/root.tsx
index b4dc2bb..cf17051 100644
--- a/test-apps/react-router-vite/app/root.tsx
+++ b/test-apps/react-router-vite/app/root.tsx
@@ -1,98 +1,103 @@
-import {
- type ActionFunctionArgs,
- data,
- Links,
- type LoaderFunctionArgs,
- Meta,
- Outlet,
- Scripts,
- ScrollRestoration,
-} from "react-router";
-import { userSomething } from "./modules/user.server";
-
-// Server middleware
-const authMiddleware = async (args: any, next: () => Promise) => {
- console.log("Auth middleware - checking authentication");
- return next();
-}
-
-const loggingMiddleware = async (args: any, next: () => Promise) => {
- console.log("Logging middleware - request:", args.request.url);
- const response = await next();
- console.log("Logging middleware - response status:", response.status);
- return response;
-}
-
-export const middleware = [authMiddleware, loggingMiddleware];
-
-// Client middleware
-const clientAuthMiddleware = async (args: any, next: () => Promise) => {
- console.log("Client auth middleware - checking client-side authentication");
- return next();
-}
-
-const clientLoggingMiddleware = async (args: any, next: () => Promise) => {
- console.log("Client logging middleware - request:", args.request.url);
- const response = await next();
- console.log("Client logging middleware - response status:", response.status);
- return response;
-}
-
-export const clientMiddleware = [
- clientAuthMiddleware,
- clientLoggingMiddleware,
-];
-
-export const links = () => [];
-
-export const loader = ({context, devTools }: LoaderFunctionArgs) => {
- userSomething();
- const mainPromise = new Promise((resolve, reject) => {
- setTimeout(() => {
- const subPromise = new Promise((resolve, reject) => {
- setTimeout(() => {
- resolve("test");
- }, 2000);
- });
- resolve({ test: "test", subPromise});
- }, 2000);
- });
- console.log("loader called");
- const end =devTools?.tracing.start("test")!;
- end();
- return data({ message: "Hello World", mainPromise, bigInt: BigInt(10) }, );
-}
-
-export const action =async ({devTools}: ActionFunctionArgs) => {
- const end = devTools?.tracing.start("action submission")
- await new Promise((resolve, reject) => {
- setTimeout(() => {
- resolve("test");
- }, 2000);
- });
- end?.();
- console.log("action called");
- return ({ message: "Hello World", bigInt: BigInt(10) });
-}
-
-function App() {
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
-}
-
-export { App as default }
\ No newline at end of file
+import type { Route } from "./+types/root";
+import {
+ type ActionFunctionArgs,
+ data,
+ Links,
+ type LoaderFunctionArgs,
+ Meta,
+ Outlet,
+ Scripts,
+ ScrollRestoration } from
+"react-router";
+import { userSomething } from "./modules/user.server";
+
+// Server middleware
+const authMiddleware = async (args: any, next: () => Promise) => {
+ console.log("Auth middleware - checking authentication");
+ return next();
+};
+
+const loggingMiddleware = async (args: any, next: () => Promise) => {
+ console.log("Logging middleware - request:", args.request.url);
+ const response = await next();
+ console.log("Logging middleware - response status:", response.status);
+ return response;
+};
+
+export const middleware: Route.MiddlewareFunction[] = [authMiddleware, loggingMiddleware];
+
+// Client middleware
+const clientAuthMiddleware = async (args: any, next: () => Promise) => {
+ console.log("Client auth middleware - checking client-side authentication");
+ return next();
+};
+
+const clientLoggingMiddleware = async (args: any, next: () => Promise) => {
+ console.log("Client logging middleware - request:", args.request.url);
+ const response = await next();
+ console.log("Client logging middleware - response status:", response.status);
+ return response;
+};
+
+export const clientMiddleware: Route.ClientMiddlewareFunction[] = [
+clientAuthMiddleware,
+clientLoggingMiddleware];
+
+
+export const links = () => [];
+
+export const loader = ({ context, devTools }: LoaderFunctionArgs) => {
+ userSomething();
+ const mainPromise = new Promise((resolve, reject) => {
+ setTimeout(() => {
+ const subPromise = new Promise((resolve, reject) => {
+ setTimeout(() => {
+ resolve("test");
+ }, 2000);
+ });
+ resolve({ test: "test", subPromise });
+ }, 2000);
+ });
+ console.log("loader called");
+ const end = devTools?.tracing.start("test")!;
+ end();
+ return data({ message: "Hello World", mainPromise, bigInt: BigInt(10) });
+};
+
+export const action = async ({ devTools }: ActionFunctionArgs) => {
+ const end = devTools?.tracing.start("action submission");
+ await new Promise((resolve, reject) => {
+ setTimeout(() => {
+ resolve("test");
+ }, 2000);
+ });
+ end?.();
+ console.log("action called");
+ return { message: "Hello World", bigInt: BigInt(10) };
+};
+
+function App() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+
+}
+
+export { App as default };
\ No newline at end of file