diff --git a/packages/docusaurus-theme-openapi-docs/src/theme/ApiExplorer/MethodEndpoint/index.tsx b/packages/docusaurus-theme-openapi-docs/src/theme/ApiExplorer/MethodEndpoint/index.tsx index 95fc62dc8..0c2b3bc0d 100644 --- a/packages/docusaurus-theme-openapi-docs/src/theme/ApiExplorer/MethodEndpoint/index.tsx +++ b/packages/docusaurus-theme-openapi-docs/src/theme/ApiExplorer/MethodEndpoint/index.tsx @@ -8,6 +8,7 @@ import React from "react"; import BrowserOnly from "@docusaurus/BrowserOnly"; +import ExecutionEnvironment from "@docusaurus/ExecutionEnvironment"; import { useTypedSelector } from "@theme/ApiItem/hooks"; function colorForMethod(method: string) { @@ -38,6 +39,27 @@ export interface Props { } function MethodEndpoint({ method, path, context }: Props) { + // SSR-safe: During server-side rendering, render without Redux store access + // This fixes React 19 compatibility where useSelector fails during SSR + // because the Redux context is not properly propagated with react-redux v7.x + if (!ExecutionEnvironment.canUseDOM) { + return ( + <> +
+          
+            {method === "event" ? "Webhook" : method.toUpperCase()}
+          {" "}
+          {method !== "event" && (
+            

+ {`${path.replace(/{([a-z0-9-_]+)}/gi, ":$1")}`} +

+ )} +
+
+ + ); + } + let serverValue = useTypedSelector((state: any) => state.server.value); let serverUrlWithVariables = ""; diff --git a/packages/docusaurus-theme-openapi-docs/src/theme/ApiItem/hooks.ts b/packages/docusaurus-theme-openapi-docs/src/theme/ApiItem/hooks.ts index 3c015b978..eaf95e0b2 100644 --- a/packages/docusaurus-theme-openapi-docs/src/theme/ApiItem/hooks.ts +++ b/packages/docusaurus-theme-openapi-docs/src/theme/ApiItem/hooks.ts @@ -5,9 +5,46 @@ * LICENSE file in the root directory of this source tree. * ========================================================================== */ +import { useState, useEffect } from "react"; import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux"; import type { RootState, AppDispatch } from "./store"; -export const useTypedDispatch = () => useDispatch(); -export const useTypedSelector: TypedUseSelectorHook = useSelector; +// Safe wrapper for useDispatch that returns a no-op when Redux context is unavailable +// This fixes React 19 compatibility where the Redux context may not be available +// during SSR or initial hydration with react-redux v7.x +export const useTypedDispatch = (): AppDispatch => { + try { + return useDispatch(); + } catch (e) { + // Return a no-op dispatch function when Redux context is not available + return (() => {}) as unknown as AppDispatch; + } +}; + +// Safe wrapper for useSelector that returns undefined when Redux context is unavailable +// This fixes React 19 compatibility where useSelector fails during SSR +// because the Redux context is not properly propagated with react-redux v7.x +export const useTypedSelector: TypedUseSelectorHook = (( + selector: (state: RootState) => unknown +) => { + const [state, setState] = useState(undefined); + const [isReady, setIsReady] = useState(false); + + // Try to get the Redux context + let reduxState: unknown = undefined; + try { + reduxState = useSelector(selector); + } catch (e) { + // Redux context not available - will return undefined + } + + useEffect(() => { + if (reduxState !== undefined) { + setState(reduxState); + setIsReady(true); + } + }, [reduxState]); + + return isReady ? state : reduxState; +}) as TypedUseSelectorHook; diff --git a/packages/docusaurus-theme-openapi-docs/src/theme/Schema/index.tsx b/packages/docusaurus-theme-openapi-docs/src/theme/Schema/index.tsx index be9f9a289..f6ad639c0 100644 --- a/packages/docusaurus-theme-openapi-docs/src/theme/Schema/index.tsx +++ b/packages/docusaurus-theme-openapi-docs/src/theme/Schema/index.tsx @@ -128,6 +128,13 @@ interface SchemaProps { const AnyOneOf: React.FC = ({ schema, schemaType }) => { const key = schema.oneOf ? "oneOf" : "anyOf"; + const schemaArray = schema[key]; + + // Handle empty oneOf/anyOf arrays - return null to avoid empty Tabs error + if (!schemaArray || !Array.isArray(schemaArray) || schemaArray.length === 0) { + return null; + } + const type = schema.oneOf ? translate({ id: OPENAPI_SCHEMA_ITEM.ONE_OF, message: "oneOf" }) : translate({ id: OPENAPI_SCHEMA_ITEM.ANY_OF, message: "anyOf" }); diff --git a/packages/docusaurus-theme-openapi-docs/src/theme/SchemaTabs/index.tsx b/packages/docusaurus-theme-openapi-docs/src/theme/SchemaTabs/index.tsx index 12cc1167c..91f62c8fa 100644 --- a/packages/docusaurus-theme-openapi-docs/src/theme/SchemaTabs/index.tsx +++ b/packages/docusaurus-theme-openapi-docs/src/theme/SchemaTabs/index.tsx @@ -212,8 +212,37 @@ function TabsComponent(props: TabProps): React.JSX.Element {
); } -export default function SchemaTabs(props: TabProps): React.JSX.Element { +export default function SchemaTabs(props: TabProps): React.JSX.Element | null { const isBrowser = useIsBrowser(); + + // Filter out null/undefined children before sanitizing + const children = Array.isArray(props.children) + ? props.children.filter(Boolean) + : props.children + ? [props.children] + : []; + + // Return null if no valid children to avoid "Tabs requires at least one TabItem" error + if (children.length === 0) { + return null; + } + + let sanitizedChildren; + try { + sanitizedChildren = sanitizeTabsChildren(children); + } catch (e) { + // If sanitization fails (no valid TabItem children), return null + return null; + } + + // Additional check - if sanitization returns empty/invalid, return null + if ( + !sanitizedChildren || + (Array.isArray(sanitizedChildren) && sanitizedChildren.length === 0) + ) { + return null; + } + return ( - {sanitizeTabsChildren(props.children)} + {sanitizedChildren} ); }