react-admin 的<RecordField>组件完全指南:标签 + 字段值的一体化渲染方案
【免费下载链接】react-adminA frontend Framework for single-page applications on top of REST/GraphQL APIs, using TypeScript, React and Material Design项目地址: https://gitcode.com/gh_mirrors/re/react-admin
<RecordField>是 react-admin 中一个轻量但实用的展示型组件,它把"字段标签"与"字段值"打包成一个组件:默认基于source自动生成人性化标签,并从当前RecordContext中取出记录渲染字段值。本文将从用法、全部 Props、与field/children/render三种渲染方式的关系、国际化标签、内联布局与主题定制等角度展开,并深入packages/ra-ui-materialui/src/field/RecordField.tsx源码与单元测试,帮助你在<Show>、<Edit>、<ReferenceField>等场景中灵活、正确地使用它。
核心用法
<RecordField>必须在提供RecordContext的组件内部使用,例如记录详情类组件<Show>、<Edit>、<ReferenceField>、<ReferenceOneField>。例如在 Show 视图中渲染一本书的标题:
import { Show, RecordField } from 'react-admin'; import { Stack } from '@mui/material'; export const BookShow = () => ( <Show> <Stack> <RecordField source="title" /> </Stack> </Show> );其渲染流程是:<RecordField>根据source(或labelprop)渲染一个标签,同时从当前RecordContext中取出record,提取record[source]的值,默认交给<TextField>显示。
从源码看(RecordField.tsx),组件内部通过useRecordContext<RecordType>(props)获取记录,该 Hook 的实现是(props && props.record) || context,即显式传入的recordprop 优先级高于上下文(见 useRecordContext.ts)。此外,若既没有source也没有label,组件会直接返回null,不会渲染任何内容。
你可以通过labelprop 覆盖自动生成的标签:
<RecordField source="title" label="Book title" />source支持深层字段路径(deep source),例如渲染嵌套对象author.name:
<RecordField label="Author name" source="author.name" />如果你想自定义值的显示方式,可以传入一个 Field 组件作为fieldprop。例如用浏览器 locale 格式化数字,使用NumberField:
import { RecordField, NumberField } from 'react-admin'; <RecordField source="price" field={NumberField} />如果需要给字段组件传特定 props(例如格式化货币),优先把字段组件作为children传入。此时传给<RecordField>的source仅用于生成标签:
import { RecordField, NumberField } from 'react-admin'; <RecordField source="price"> <NumberField source="price" options={{ style: 'currency', currency: 'USD' }} /> </RecordField>如果需要聚合多个字段,可以用renderprop 传入一个接收当前 record、返回 React 元素的函数:
import { RecordField } from 'react-admin'; <RecordField label="Name" render={record => `${record.firstName} ${record.lastName}`} />注意:field、children、render三个 prop 互斥。从源码的渲染优先级看,它们是children > render > field > source(默认 TextField)的递进关系,后设置的会被前面的覆盖。
Props 一览
| Prop | Required | Type | Default | Description |
|---|---|---|---|---|
children | Optional | ReactNode | '' | Elements rendering the actual field. |
className | Optional | string | '' | CSS class name to apply to the field. |
empty | Optional | ReactNode | '' | Text to display when the field is empty. |
field | Optional | ReactElement | TextField | Field component used to render the field. Ignored ifchildrenorrenderare set. |
label | Optional | string | '' | Label to render. Can be a translation key. |
record | Optional | object | {} | Record to use. If not set, the record is taken from the context. |
render | Optional | record => JSX | Function to render the field value. Ignored ifchildrenis set. | |
source | Optional | string | '' | Name of the record field to render. |
sx | Optional | object | {} | Styles to apply to the field. |
TypographyProps | Optional | object | {} | Props to pass to label wrapper |
variant | Optional | 'default' || 'inline' | 'default' | Wheninline, the label is displayed inline with the field value. |
源码提示:
RecordFieldProps实际继承自 MUI 的StackProps(见 RecordField.tsx),因此任何Stack支持的布局类 prop 都可以直接透传给<RecordField>。另外组件内部通过useThemeProps({ props: inProps, name: 'RaRecordField' })启用主题体系,这也是后文"通过主题定制默认variant"能够生效的底层原因。
children:自定义渲染的字段组件
children用于传入一个字段组件,替换默认的渲染方式。此时source仅用于生成标签:
import { RecordField, NumberField } from 'react-admin'; <RecordField source="price"> <NumberField source="price" options={{ style: 'currency', currency: 'USD' }} /> </RecordField>这一能力经常被用来渲染来自引用(reference)记录的字段,配合<ReferenceField>:
import { RecordField, ReferenceField } from 'react-admin'; <RecordField label="Author"> <ReferenceField source="author_id" reference="users" /> </RecordField>如果只是需要一个无特殊 props 的字段组件,优先使用fieldprop,代码更简洁:
import { RecordField, NumberField } from 'react-admin'; <RecordField source="price" field={NumberField} /> // instead of <RecordField source="price"> <NumberField source="price" /> </RecordField>源码中children分支会把子元素包在一个span.RaRecordField-value中(见 RecordField.tsx),字段值区域默认flex: 1,便于与标签对齐排版。Story 示例Children还展示了如何在子元素里拼接普通Typography文本(RecordField.stories.tsx)。
empty:空值占位
当record[source]为空时,<RecordField>默认渲染空字符串。如果希望显示自定义内容,使用emptyprop:
<RecordField source="title" empty="Missing title" />empty也接受翻译 key,从而在字段为空时展示本地化文案:
<RecordField source="title" empty="resources.books.fields.title.missing" />如果使用renderprop,你甚至可以把 React 元素作为empty值:
<RecordField source="title" empty={<span style={{ color: 'red' }}>Missing title</span>} render={record => record.title} />注意:当你以子组件方式传入自定义字段组件时,empty会被忽略,此时空值处理由子组件自己负责:
<RecordField label="title"> <TextField source="title" emptyText="Missing title" /> </RecordField>从源码可以看清empty的三种落地方式(RecordField.tsx):
render分支:当render(record)的返回值为空时,若empty是字符串则调用translate(empty, { _: empty })翻译,否则原样渲染empty元素;field分支:empty被转换成emptyTextprop 传给字段组件;source分支:empty同样作为emptyText传给默认的<TextField>,而TextField内部会再次执行translate(emptyText, { _: emptyText })(见 TextField.tsx)。
单元测试对这三种路径均有覆盖(RecordField.spec.tsx):record 为undefined时渲染翻译后的 "No title";render模式下渲染 "Unknown author";field模式下渲染数字字段的 "0"。
field:替换默认的 TextField
默认情况下,<RecordField>使用<TextField>渲染字段值:
<RecordField source="title" /> // equivalent to <RecordField source="title" field={TextField} />使用fieldprop 传入自定义字段组件:
import { RecordField, NumberField } from 'react-admin'; <RecordField source="price" field={NumberField} />如需给字段组件传 props(例如格式化货币),优先将字段组件作为children,此时source只用于标签:
import { RecordField, NumberField } from 'react-admin'; <RecordField source="price"> <NumberField source="price" options={{ style: 'currency', currency: 'USD' }} /> </RecordField>源码中field分支通过React.createElement(field, { source, emptyText: empty as string, className: RecordFieldClasses.value })实例化该组件(RecordField.tsx),自动把source、empty(作为emptyText)和值区域的 class 传入,无需手工重复声明。
label:标签的自动生成与覆盖
当使用sourceprop 时,标签会自动根据 source 名经 "humanize" 处理生成。例如author.name会显示为 "Author name"。
你还可以通过为resources.${resourceName}.fields.${source}key 配置翻译文案来定制标签。例如资源posts下,要为<RecordField source="title" />定制标签,添加如下翻译:
{ "resources": { "posts": { "fields": { "title": "Post title" } } } }若未使用source,或不希望借助 i18N 定制标签,可以用labelprop 覆盖默认标签:
<RecordField source="title" label="Post title" />label也可以传翻译 key,react-admin 会通过i18nProvider翻译:
<RecordField source="title" label="resources.posts.fields.title_custom" />最后,传false可以隐藏标签:
<RecordField source="title" label={false} />注意,label={false}等价于直接渲染一个<TextField>(此时不再有标签包装层)。
结合源码可以更深入地理解标签解析链路:标签最终由FieldTitle渲染(FieldTitle.tsx),它调用useTranslateLabel,后者通过getFieldLabelTranslationArgs生成resources.${resource}.fields.${source}这样的翻译 key(useTranslateLabel.ts)。在<RecordField>内部,label为空字符串或false时,整个<Typography>标签包装层都会被跳过(RecordField.tsx)。测试用例也验证了:默认渲染 humanized 的 "Title"、labelprop 覆盖为 "Identifier"、label={false}时页面中不再出现 "Summary"(RecordField.spec.tsx)。
record:覆盖上下文记录
默认情况下<RecordField>使用当前RecordContext中的记录。你也可以通过recordprop 覆盖:
<RecordField record={record} source="title" />这与useRecordContext的实现一致:return (props && props.record) || context;(useRecordContext.ts),即 prop 优先、context 兜底。
render:聚合多字段或使用任意组件
renderprop 接收当前 record 并返回 React 元素,适合聚合多个字段,或使用不接受sourceprop 的组件:
import { RecordField } from 'react-admin'; <RecordField label="Name" render={record => `${record.firstName} ${record.lastName}`} />如果同时传source和render,source仅用于标签。
源码中render分支在record存在时才渲染,并把结果包进Typography component="span" variant="body2"(RecordField.tsx);当返回值为空时按上文所述回退到empty。测试覆盖了字符串返回值(如大写标题)、React 元素返回值,以及 record 为undefined时不抛错(RecordField.spec.tsx)。
sx:样式定制
使用sxprop 给整个字段传自定义样式:
<RecordField source="id" sx={{ opacity: 0.5 }} />若要单独给标签加样式,使用TypographyProps:
<RecordField source="id" TypographyProps={{ sx: { color: 'red' } }} />若只想给值加样式,优先把自定义组件作为 children:
<RecordField source="id"> <TextField source="id" sx={{ color: 'red' }} /> </RecordField>底层上,sx作用于包裹根节点的styled(Stack),而标签与值分别使用.RaRecordField-label与.RaRecordField-value两个内部 class(RecordField.tsx),因此你也可以像 StorySX那样用后代选择器单独命中标签:
<RecordField source="year" field={NumberField} sx={{ '& .RaRecordField-label': { color: 'red' } }} />source:要渲染的字段名
使用sourceprop 指定要渲染的记录字段名。例如当前 record 为:
{ "id": 123, "title": "My post", "author": { "name": "John Doe" } }显示title字段:
<RecordField source="title" />source可以是深层路径,例如author.name:
<RecordField source="author.name" />如果同时使用render或childrenprop,source仅用于生成标签。
深层路径提示:
<RecordField source="author.name" />依赖底层<TextField>的useFieldValue能力去解析点分路径;而当 record 中恰好存在一个 key 就叫"author.name"时,StorySource中的行为也验证了这一点(RecordField.stories.tsx)。实践中请留意数据结构的实际形态。
TypographyProps:标签包装层属性
TypographyProps用于给标签包装层传 props,便于让标签的样式与字段值区分开:
<RecordField source="id" TypographyProps={{ sx: { color: 'red' } }} />从源码看,该 prop 会被透传给承载FieldTitle的<Typography>元素(RecordField.tsx),所以TypographyProps里除了sx还可以传 MUI Typography 支持的任何属性(如variant、color等)。
variant:默认布局与内联布局
默认情况下,<RecordField>将标签渲染在字段值上方。使用variant="inline"可将标签与字段值同行显示:
<RecordField source="title" variant="inline" />如果需要定制标签宽度,使用TypographyProps:
<RecordField source="title" variant="inline" TypographyProps={{ sx: { width: 200 } }} />但由于通常需要对多个字段统一设置,推荐在父组件中统一处理:
<Stack sx={{ '& .RaRecordField-label': { width: 200 } }}> <RecordField variant="inline" source="id" /> <RecordField variant="inline" source="title" /> <RecordField variant="inline" source="author" /> <RecordField variant="inline" source="summary" /> <RecordField variant="inline" source="year" field={NumberField} /> </Stack>提示:如果希望所有字段都默认内联显示,可以在自定义应用主题中为RaRecordField定义默认variant(详见主题化单个组件):
import { defaultTheme } from 'react-admin'; import { deepmerge } from '@mui/utils'; const theme = deepmerge(defaultTheme, { components: { RaRecordField: { defaultProps: { variant: 'inline', }, }, }, }); const App = () => ( <Admin theme={theme}> // ... </Admin> );从源码可以看到内联布局的底层实现:variant === 'inline'时根节点追加.RaRecordField-inlineclass,样式将根Stack的flexDirection切换为row,同时内联状态下标签字号调整为0.875rem、display: block、默认minWidth: 150(RecordField.tsx)。此外组件通过declare module '@mui/material/styles'注册了RaRecordField的defaultProps与styleOverrides类型声明(RecordField.tsx),这正是主题化定制能获得类型提示的原因。
TypeScript:泛型类型安全
<RecordField>是一个泛型组件。你可以传入类型参数,从而获得sourceprop 的补全提示,以及render函数中record参数的类型安全:
import { Show, RecordField } from 'react-admin'; import { Stack } from '@mui/material'; import { Book } from './types'; const BookShow = () => { const BookField = RecordField<Book>; return ( <Show> <Stack> <BookField source="title" /> <BookField source="author.name" /> <BookField source="price" render={record => `${record.price} USD`} /> </Stack> </Show> ); };从类型定义看(RecordField.tsx),RecordType默认是Record<string, any>;source的类型被限定为NoInfer<HintedString<ExtractRecordPaths<RecordType>>>,即根据记录类型提取出的字段路径的补全字符串,render的函数签名则为(record: RecordType) => React.ReactNode。StoryGeneric中给出了完整可运行的泛型示例(RecordField.stories.tsx)。
小结与使用建议
<RecordField>把"标签 + 值"这一最常见的信息展示模式封装为一个组件,适合在<Show>、<Edit>、<ReferenceField>等记录上下文中快速搭建只读展示界面。使用时建议遵循以下取舍:
- 仅需展示单字段 → 用
source(默认TextField渲染); - 需换字段组件且无额外 props → 用
field(如field={NumberField}); - 需给字段组件传格式化等 props,或渲染引用字段 → 用
children; - 需聚合多字段或接入不接受
source的组件 → 用render; - 空值展示 → 用
empty(支持翻译 key 与 React 元素); - 批量内联布局 → 在父组件统一设置
sx,或在应用主题中为RaRecordField设置默认variant。
相关阅读:<TextField>、<ReferenceField>、RecordContext与useRecordContext、字段组件总览。
【免费下载链接】react-adminA frontend Framework for single-page applications on top of REST/GraphQL APIs, using TypeScript, React and Material Design项目地址: https://gitcode.com/gh_mirrors/re/react-admin
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考