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
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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 (
<>
<pre className="openapi__method-endpoint">
<span className={"badge badge--" + colorForMethod(method)}>
{method === "event" ? "Webhook" : method.toUpperCase()}
</span>{" "}
{method !== "event" && (
<h2 className="openapi__method-endpoint-path">
{`${path.replace(/{([a-z0-9-_]+)}/gi, ":$1")}`}
</h2>
)}
</pre>
<div className="openapi__divider" />
</>
);
}

let serverValue = useTypedSelector((state: any) => state.server.value);
let serverUrlWithVariables = "";

Expand Down
41 changes: 39 additions & 2 deletions packages/docusaurus-theme-openapi-docs/src/theme/ApiItem/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<AppDispatch>();
export const useTypedSelector: TypedUseSelectorHook<RootState> = 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<AppDispatch>();
} 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<RootState> = ((
selector: (state: RootState) => unknown
) => {
const [state, setState] = useState<unknown>(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<RootState>;
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,13 @@ interface SchemaProps {

const AnyOneOf: React.FC<SchemaProps> = ({ 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" });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -212,16 +212,45 @@ function TabsComponent(props: TabProps): React.JSX.Element {
</div>
);
}
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 (
<TabsComponent
// Remount tabs after hydration
// Temporary fix for https://github.com/facebook/docusaurus/issues/5653
key={String(isBrowser)}
{...props}
>
{sanitizeTabsChildren(props.children)}
{sanitizedChildren}
</TabsComponent>
);
}