Core & UI: Drop framework-side createContext

This commit is contained in:
Fuma Nama
2025-11-12 20:23:48 +08:00
parent ff68f69f80
commit 00058c8f6e
9 changed files with 111 additions and 60 deletions
+6
View File
@@ -0,0 +1,6 @@
---
'fumadocs-core': patch
'fumadocs-ui': patch
---
Drop framework-side `createContext`
@@ -0,0 +1,8 @@
---
title: 中文
description: 您的第一個文檔
---
## Hi 中文
Fumadocs 對 i18n 有良好的支持
+17
View File
@@ -0,0 +1,17 @@
---
title: Hello World
description: Your first document
---
Hey there!
## Heading
<Cards>
<Card title="Learn more about Next.js" href="https://nextjs.org/docs" />
<Card title="Learn more about Fumadocs" href="https://fumadocs.dev" />
</Cards>
### Heading
#### Heading
+6
View File
@@ -0,0 +1,6 @@
{
"title": "Git",
"root": true,
"pages": ["index", "test"],
"icon": "git"
}
+21
View File
@@ -0,0 +1,21 @@
---
title: Hello World
description: Your first document
---
Hey there!
## Heading
<Cards>
<Card title="Learn more about Next.js" href="https://nextjs.org/docs" />
<Card title="Learn more about Fumadocs" href="https://fumadocs.dev" />
</Cards>
### Heading
```js
console.log('Hello World');
```
#### Heading
+19 -5
View File
@@ -2,7 +2,11 @@
"compilerOptions": {
"baseUrl": ".",
"target": "ESNext",
"lib": ["dom", "dom.iterable", "esnext"],
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
@@ -13,10 +17,12 @@
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"jsx": "react-jsx",
"incremental": true,
"paths": {
"@/*": ["./*"]
"@/*": [
"./*"
]
},
"plugins": [
{
@@ -24,6 +30,14 @@
}
]
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
".next/dev/types/**/*.ts"
],
"exclude": [
"node_modules"
]
}
+9 -36
View File
@@ -1,6 +1,6 @@
'use client';
import type { ComponentProps, FC, ReactNode } from 'react';
import React from 'react';
import { createContext, use, useMemo } from 'react';
import type { StaticImport } from 'next/dist/shared/lib/get-img-props';
export interface ImageProps extends Omit<ComponentProps<'img'>, 'src'> {
@@ -46,7 +46,7 @@ const notImplemented = () => {
);
};
const FrameworkContext = createContext<Framework>('FrameworkContext', {
const FrameworkContext = createContext<Framework>({
useParams: notImplemented,
useRouter: notImplemented,
usePathname: notImplemented,
@@ -60,7 +60,7 @@ export function FrameworkProvider({
Image,
children,
}: Framework & { children: ReactNode }) {
const framework = React.useMemo(
const framework = useMemo(
() => ({
usePathname,
useRouter,
@@ -71,27 +71,23 @@ export function FrameworkProvider({
[Link, usePathname, useRouter, useParams, Image],
);
return (
<FrameworkContext.Provider value={framework}>
{children}
</FrameworkContext.Provider>
);
return <FrameworkContext value={framework}>{children}</FrameworkContext>;
}
export function usePathname() {
return FrameworkContext.use().usePathname();
return use(FrameworkContext).usePathname();
}
export function useRouter() {
return FrameworkContext.use().useRouter();
return use(FrameworkContext).useRouter();
}
export function useParams() {
return FrameworkContext.use().useParams();
return use(FrameworkContext).useParams();
}
export function Image(props: ImageProps) {
const { Image } = FrameworkContext.use();
const { Image } = use(FrameworkContext);
if (!Image) {
const { src, alt, priority, ...rest } = props;
@@ -109,7 +105,7 @@ export function Image(props: ImageProps) {
}
export function Link(props: LinkProps) {
const { Link } = FrameworkContext.use();
const { Link } = use(FrameworkContext);
if (!Link) {
const { href, prefetch: _, ...rest } = props;
return <a href={href} {...rest} />;
@@ -117,26 +113,3 @@ export function Link(props: LinkProps) {
return <Link {...props} />;
}
export function createContext<T>(name: string, v?: T) {
const Context = React.createContext(v);
return {
Provider: (props: { value: T; children: ReactNode }) => {
return (
<Context.Provider value={props.value}>
{props.children}
</Context.Provider>
);
},
use: (errorMessage?: string): Exclude<T, undefined | null> => {
const value = React.useContext(Context);
if (!value)
throw new Error(
errorMessage ?? `Provider of ${name} is required but missing.`,
);
return value as Exclude<T, undefined | null>;
},
};
}
+18 -14
View File
@@ -1,7 +1,7 @@
'use client';
import type * as PageTree from 'fumadocs-core/page-tree';
import { createContext, usePathname } from 'fumadocs-core/framework';
import { type ReactNode, useMemo, useRef } from 'react';
import { usePathname } from 'fumadocs-core/framework';
import { type ReactNode, useMemo, useRef, createContext, use } from 'react';
import { searchPath } from 'fumadocs-core/breadcrumb';
type MakeRequired<O, K extends keyof O> = Omit<O, K> & Pick<Required<O>, K>;
@@ -11,8 +11,8 @@ interface TreeContextType {
full: PageTree.Root;
}
const TreeContext = createContext<TreeContextType>('TreeContext');
const PathContext = createContext<PageTree.Node[]>('PathContext', []);
const TreeContext = createContext<TreeContextType | null>(null);
const PathContext = createContext<PageTree.Node[]>([]);
export function TreeContextProvider(props: {
tree: PageTree.Root;
@@ -26,11 +26,11 @@ export function TreeContextProvider(props: {
// eslint-disable-next-line react-hooks/exhaustive-deps
const tree = useMemo(() => props.tree, [props.tree.$id ?? props.tree]);
const path = useMemo(() => {
let result = searchPath(tree.children, pathname);
if (result) return result;
if (tree.fallback) result = searchPath(tree.fallback.children, pathname);
return result ?? [];
return (
searchPath(tree.children, pathname) ??
(tree.fallback ? searchPath(tree.fallback.children, pathname) : null) ??
[]
);
}, [tree, pathname]);
const root =
@@ -38,21 +38,25 @@ export function TreeContextProvider(props: {
root.$id ??= String(nextIdRef.current++);
return (
<TreeContext.Provider
<TreeContext
value={useMemo(
() => ({ root, full: tree }) as TreeContextType,
[root, tree],
)}
>
<PathContext.Provider value={path}>{props.children}</PathContext.Provider>
</TreeContext.Provider>
<PathContext value={path}>{props.children}</PathContext>
</TreeContext>
);
}
export function useTreePath(): PageTree.Node[] {
return PathContext.use();
return use(PathContext);
}
export function useTreeContext(): TreeContextType {
return TreeContext.use('You must wrap this component under <DocsLayout />');
const ctx = use(TreeContext);
if (!ctx)
throw new Error('You must wrap this component under <DocsLayout />');
return ctx;
}
+7 -5
View File
@@ -5,9 +5,11 @@ import {
Fragment,
useEffect,
useEffectEvent,
createContext,
useMemo,
useRef,
useState,
use,
} from 'react';
import { ChevronDown, ChevronLeft, ChevronRight } from '@/icons';
import Link from 'fumadocs-core/link';
@@ -15,7 +17,7 @@ import { cn } from '@/utils/cn';
import { useI18n } from '@/contexts/i18n';
import { useTreeContext, useTreePath } from '@/contexts/tree';
import type * as PageTree from 'fumadocs-core/page-tree';
import { createContext, usePathname } from 'fumadocs-core/framework';
import { usePathname } from 'fumadocs-core/framework';
import {
type BreadcrumbOptions,
getBreadcrumbItemsFromPath,
@@ -34,11 +36,11 @@ import { useActiveAnchor } from 'fumadocs-core/toc';
const TocPopoverContext = createContext<{
open: boolean;
setOpen: (open: boolean) => void;
}>('TocPopoverContext');
} | null>(null);
export function PageTOCPopoverTrigger(props: ComponentProps<'button'>) {
const { text } = useI18n();
const { open } = TocPopoverContext.use();
const { open } = use(TocPopoverContext)!;
const items = useTOCItems();
const active = useActiveAnchor();
const selected = useMemo(
@@ -182,7 +184,7 @@ export function PageTOCPopover(props: ComponentProps<'div'>) {
}, []);
return (
<TocPopoverContext.Provider
<TocPopoverContext
value={useMemo(
() => ({
open,
@@ -214,7 +216,7 @@ export function PageTOCPopover(props: ComponentProps<'div'>) {
{props.children}
</header>
</Collapsible>
</TocPopoverContext.Provider>
</TocPopoverContext>
);
}