Compare commits

...

5 Commits

Author SHA1 Message Date
c336615c68 简单测试记录上下文 2025-06-19 22:42:13 +08:00
02b54e1cda 数据请求设计 2025-06-19 22:40:39 +08:00
da8e866a2b 重新设计记录上下文 2025-06-19 22:40:14 +08:00
889e759656 数据源上下文 2025-06-19 22:39:47 +08:00
43b445c10b 通用工具类型 2025-06-19 22:39:16 +08:00
19 changed files with 339 additions and 53 deletions

View File

@ -1,8 +1,8 @@
import "./App.css"; import "./App.css";
import { RecordProvider, useNamedRecord, useRecord } from "./core/record"; import { RecordContextProvider, useRecordContext } from "./core/record";
function Consumer({ name }: { name: string }) { function Consumer({ name }: { name: string }) {
const record = useNamedRecord<{ text: string }>(name); const record = useRecordContext<{ text: string }>(name);
return ( return (
<div <div
style={{ style={{
@ -20,7 +20,7 @@ function Consumer({ name }: { name: string }) {
} }
function RecordConsumer() { function RecordConsumer() {
const record = useRecord<{ text: string }>(); const record = useRecordContext<{ text: string }>();
return ( return (
<div <div
style={{ style={{
@ -42,16 +42,16 @@ const App = () => {
<div className="content"> <div className="content">
<h1>Rsbuild with React</h1> <h1>Rsbuild with React</h1>
<p>Start building amazing things with Rsbuild.</p> <p>Start building amazing things with Rsbuild.</p>
<RecordProvider name="a" value={{ text: "aaa Rsbuild message" }}> <RecordContextProvider name="a" value={{ text: "aaa Rsbuild message" }}>
<RecordProvider name="av" value={{ text: "vvv with Rsbuild" }}> <RecordContextProvider name="av" value={{ text: "vvv with Rsbuild" }}>
<Consumer name="av" /> <Consumer name="av" />
<Consumer name="a" /> <Consumer name="a" />
<RecordConsumer /> <RecordConsumer />
</RecordProvider> </RecordContextProvider>
<Consumer name="a" /> <Consumer name="a" />
<Consumer name="x" /> <Consumer name="x" />
<RecordConsumer /> <RecordConsumer />
</RecordProvider> </RecordContextProvider>
</div> </div>
); );
}; };

View File

@ -0,0 +1,2 @@
// https://tkdodo.eu/blog/react-query-meets-react-router
// https://remix.run/blog/remixing-react-router

View File

View File

View File

View File

View File

View File

View File

View File

View File

@ -1,4 +1,5 @@
import { createContext, useContext, useMemo } from "react"; import { createContext, type ReactNode, useContext, useMemo } from "react";
import { type Validator } from "./types";
/** /**
* *
@ -12,19 +13,163 @@ export type RecordContextValue<T extends object = object> = {
/** /**
* Component hook : * Component hook :
* *
* - RecordProvider * - RecordContextProvider
* - useRecord * - useRecordContext
* - useNamedRecord *
* @internal
*/ */
const RecordContext = createContext<RecordContextValue | null>(null); const RecordContext = createContext<RecordContextValue | null>(null);
type RecordProviderProps<T extends object> = { /**
children: React.ReactNode; *
*
* @example
*
*
*
* ```jsx
* <RecordContextProvider name="..." value={{ name: 'John', age: 30 }}>
* {useRecordContext().name}
* </RecordContextProvider>
* ```
*
* @example
*
*
*
* ```jsx
* <RecordContextProvider name="..." value={{ name: 'John', age: 30 }}>
* <RecordContextProvider name="..." value={{ email: 'Jane@example.com', phone: '1234567890' }}>
* {useRecordContext((value) => 'email' in value).phone}
* </RecordContextProvider>
* </RecordContextProvider>
* ```
*
* @example
*
*
*
* ```jsx
* <RecordContextProvider name="user.2" value={{ id: 2, name: 'John', age: 30 }}>
* {useRecordContext('user.2').name}
* </RecordContextProvider>
* ```
*/
export function useRecordContext<T extends object = object>(): T | null;
export function useRecordContext<T extends object = object>(name: string): T | null;
export function useRecordContext<T extends object = object>(validator: Validator<object>): T | null;
export function useRecordContext<T extends object = object>(name: string, validator: Validator<object>): T | null;
export function useRecordContext<T extends object = object>(...args: unknown[]): T | null {
let name: string | undefined;
let validator: Validator<object> | undefined;
if (typeof args[0] === 'string') {
name = args[0];
if (typeof args[1] === 'function') {
validator = args[1] as Validator<object>;
}
} else if (typeof args[0] === 'function') {
validator = args[0] as Validator<object>;
}
let context = useContext(RecordContext);
if (!context) {
throw new Error("useRecordContext must be used within RecordContextProvider");
}
if (validator == null && name == null) {
return context.value as T;
}
return resolveRecord(context, c => {
return (name == null || c.name === name)
&& (validator == null || validator(c.value));
})
}
function resolveRecord<T extends object = object>(
context: RecordContextValue,
validate: (ctx: RecordContextValue) => boolean,
): T | null {
while (true) {
if (validate(context)) {
return context.value as T;
}
if (!context.parent) {
return null;
}
context = context.parent;
}
}
/**
*
*/
export interface RecordProviderProps<T extends object> {
/**
*
*
*
*/
children: ReactNode;
/**
*
*
* useRecordContext
*
* @example
*
* ```jsx
* function UserName({ id }: { id: number }) {
* const user = useRecordContext<User>(`user.${id}`);
* return <div>{user.name}</div>
* }
*
* <RecordContextProvider name="user.1" value={{ id: 1, name: "John" }}>
* <RecordContextProvider name="user.2" value={{ id: 2, name: "Jane" }}>
* <p>
* user name for id equal 2 is
* <UserName id={2} />
* </p>
* <p>
* user name for id equal 1 is
* <UserName id={1} />
* </p>
* </RecordContextProvider>
* </RecordContextProvider>
* ```
*/
name: string; name: string;
/**
*
*/
value: T; value: T;
}; };
export function RecordProvider<T extends object>({ /**
*
*
* @example
*
* ```jsx
* <RecordContextProvider name="user.1" value={{ id: 1, name: "John" }}>
* <RecordContextProvider name="user.2" value={{ id: 2, name: "Jane" }}>
* <p>
* user name for id equal 2 is
* <UserName id={2} />
* </p>
* <p>
* user name for id equal 1 is
* <UserName id={1} />
* </p>
* </RecordContextProvider>
* </RecordContextProvider>
* ```
*/
export function RecordContextProvider<T extends object>({
children, children,
...context ...context
}: RecordProviderProps<T>) { }: RecordProviderProps<T>) {
@ -36,48 +181,109 @@ export function RecordProvider<T extends object>({
Object.values(context), Object.values(context),
) as RecordContextValue; ) as RecordContextValue;
return <RecordContext value={value}>{children}</RecordContext>; return (
<RecordContext value={value}>
{children}
</RecordContext>
);
} }
export function useRecord<T extends object = object>( /**
validate?: (value: unknown) => boolean, *
): T | null { *
let context = useContext(RecordContext); * RecordContextProvider
if (!context) { *
throw new Error("useRecord must be used within RecordProvider"); *
} * @example
if (validate == null) { *
return context.value as T; * ```jsx
} * const RecordTitle = ({ record }) => (
while (true) { * <OptionalRecordProvider name="post.1" value={record}>
if (validate(context.value)) { * <TextField source="title" />
return context.value as T; * </OptionalRecordProvider>
} * );
if (!context.parent) { * ```
return null; */
} export function OptionalRecordProvider<T extends object>({
context = context.parent; children,
name,
value,
}: Omit<RecordProviderProps<T>, 'value'> & {
value?: T | null | undefined;
}) {
if (value != null) {
return (
<RecordContextProvider
name={name}
value={value}
>
{children}
</RecordContextProvider>
);
} }
return children;
} }
export function useNamedRecord<T extends object = object>( export interface WithRecordProps<T extends object = object> {
name: string, /**
validate?: (value: unknown) => boolean, *
): T | null { *
let context = useContext(RecordContext); * @type {string}
if (!context) { */
throw new Error("useNamedRecord must be used within RecordProvider"); name?: string;
}
while (true) { /**
if (context!.name === name) { *
if (validate && !validate(context.value)) { *
throw new Error("Invalid record value"); * @param {T} record
} * @returns {ReactNode}
return context!.value as T; */
} render: (record: T) => ReactNode;
if (!context.parent) {
return null; /**
} *
context = context!.parent; *
} * @param {boolean} hasContext
* @returns {ReactNode}
*/
fallback?: ReactNode | ((hasContext: boolean) => ReactNode);
}
/**
*
*
* 使 useRecordContext
*
*
* @example
*
* ```jsx
* const BookShow = () => (
* <Show>
* <SimpleShowLayout>
* <WithRecord render={record => <span>{record.title}</span>} />
* </SimpleShowLayout>
* </Show>
* );
* ```
*/
export function WithRecord<T extends object = object>({
name,
render,
fallback,
}: WithRecordProps<T>) {
const context = useContext(RecordContext);
const record = context != null
? resolveRecord<T>(context, c => name == null || c.name == name)
: null;
if (record == null) {
if (typeof fallback === 'function') {
return fallback(context != null);
}
return fallback ?? null;
}
return <>{render(record)}</>
} }

64
src/core/source.tsx Normal file
View File

@ -0,0 +1,64 @@
import { createContext, ProviderProps, useContext } from 'react';
/**
*
*/
export type SourceContextValue = {
/*
*
*/
getSource: (source: string) => string;
/*
*
* i18n/i10n
*/
getLabel: (source: string) => string;
};
const SourceContext = createContext<SourceContextValue | undefined>(undefined);
/**
*
*
*
*
* @example
*
* ```jsx
* const sourceContext = {
* getSource: source => `coordinates.${source}`,
* getLabel: source => `resources.posts.fields.${source}`,
* }
*
* const CoordinatesInput = () => {
* return (
* <SourceContextProvider value={sourceContext}>
* <TextInput source="lat" />
* <TextInput source="lng" />
* </SourceContextProvider>
* );
* };
* ```
*/
export function SourceContextProvider({ children, value }: ProviderProps<SourceContextValue>) {
return (
<SourceContext value={value}>
{children}
</SourceContext>
);
}
const defaultContextValue: SourceContextValue = {
getSource: (source: string) => source,
getLabel: (source: string) => source,
};
export function useSourceContext(): SourceContextValue {
const context = useContext(SourceContext);
return context ?? defaultContextValue;
}
export function useOptionalSourceContext(): SourceContextValue | undefined {
return useContext(SourceContext);
}

14
src/core/types.ts Normal file
View File

@ -0,0 +1,14 @@
import { Dispatch, SetStateAction } from "react";
export type ValueGetter<T> = () => T;
export type ValueSetter<T> = (value: T) => void;
export type AsyncGetter<T> = () => Promise<T>;
export type AsyncSetter<T> = (value: T) => Promise<void>;
export type Validator<T> = (value: T) => boolean;
export type AsyncValidator<T> = (value: T) => Promise<boolean>;
export type Transformer<T, R> = (value: T) => R;
export type SetState<T> = Dispatch<SetStateAction<T>>;