react-admin `<Title>` 组件完全指南:从页面标题注入到可配置化持久化

  • 前端
  • UI组件

【免费下载链接】react-admin

A 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
点击查看 免费下载

<Title> 是 react-admin 中用于设置页面标题(App Bar 中央显示文本)的核心组件,它借助 React Portal 将标题渲染到页面组件树之外,再通过 App Bar 中的 <TitlePortal> 占位符统一展示。本指南将带你掌握 <Title> 的全部用法——自定义页面标题、加载时的 defaultTitle 防闪烁策略,以及在 Configurable 模式下通过 preferenceKey 让终端用户自主定制并持久化标题,并深入源码剖析其底层实现原理。

react-admin App Bar 中显示自定义标题 "Custom title"

一、<Title> 是什么:为任意页面设置 App Bar 标题

在 react-admin 中,页面顶部的 App Bar 中央区域会显示当前页面的标题。<Title> 组件的职责就是"从页面内部的任意位置设置这个标题"。它的定位可以从 Title.tsx 的组件职责看出:你可以在任何页面组件中渲染 <Title>,标题文本会立即出现在应用栏中央,无需手动管理全局状态。

最基本的使用方式是在自定义页面中直接渲染它:

import { Title } from 'react-admin';

const CustomPage = () => (
    <>
        <Title title="My Custom Page" />
        <div>Content</div>
    </>
);

这段代码没有任何额外的配置即可工作,因为它不依赖当前页面的数据、上下文或资源定义——只需一个 title 值,标题就会出现在 App Bar 中央。

二、工作原理:React Portal 与 <TitlePortal> 占位符

<Title> 能够"隔空"写入 App Bar 的核心机制是 React Portal(组件注:React 官方机制,将子树渲染到指定 DOM 节点)。其实现位于 Title.tsx:

export const Title = (props: TitleProps) => {
    const { defaultTitle, title, preferenceKey, ...rest } = props;
    const [container, setContainer] = useState<HTMLElement | null>(() =>
        typeof document !== 'undefined'
            ? document.getElementById('react-admin-title')
            : null
    );

    // on first mount, we don't have the container yet, so we wait for it
    useEffect(() => {
        setContainer(container => {
            const isInTheDom =
                typeof document !== 'undefined' &&
                document.body.contains(container);
            if (container && isInTheDom) return container;
            return typeof document !== 'undefined'
                ? document.getElementById('react-admin-title')
                : null;
        });
    }, []);

    if (!container) return null;
    // ... 选择 PageTitle 或 PageTitleConfigurable 渲染
    return <>{createPortal(pageTitle, container)}</>;
};

关键实现细节:

  • 容器查找:组件通过 document.getElementById('react-admin-title') 查找挂载目标。由于首次渲染时容器可能尚未挂载,源码专门使用 useEffect 在挂载后二次确认容器存在(document.body.contains(container)),保证标题不会因时序问题丢失。
  • Portal 渲染:找到容器后,createPortal(pageTitle, container) 将标题节点渲染到 App Bar 所在的 DOM 节点之下,从而实现"组件写在页面里,渲染在 App Bar 里"。

而 react-admin-title 这个 DOM 节点的来源,是默认 App Bar 内置的 <TitlePortal> 占位符。它的实现位于 TitlePortal.tsx,本质是一个 MUI Typography:

export const TitlePortal = (props: TypographyProps) => (
    <Typography
        variant="h6"
        color="inherit"
        id="react-admin-title"
        {...props}
        sx={[
            {
                flex: '1',
                textOverflow: 'ellipsis',
                whiteSpace: 'nowrap',
                overflow: 'hidden',
            },
            ...(Array.isArray(props.sx) ? props.sx : [props.sx]),
        ]}
    />
);

默认情况下,AppBar.tsx 会渲染 <TitlePortal className={AppBarClasses.title} />,因此开箱即用。<TitlePortal> 自带三组关键样式:flex: '1' 让标题区域撑满剩余空间,textOverflow: 'ellipsis'、whiteSpace: 'nowrap'、overflow: 'hidden' 保证标题过长时以省略号截断而非换行撑破布局。

如果你编写自定义 App Bar,务必保留 <TitlePortal /> 占位符,否则 <Title> 找不到容器会直接返回 null。参见 AppBar.stories.tsx 中"AppBar 内放置 TitlePortal + 搜索框"的组合示例。自定义 App Bar 的完整流程见 AppBar.md。

三、Props 一览

Prop是否必填类型默认值说明
title可选string | ReactElement-App Bar 中央区域显示的内容
defaultTitle可选string(实现上支持 TitleComponent,含 React 元素)''当 title 未设置时显示的内容
preferenceKey可选string | false`${pathname}.title`在 Configurable 模式下用于存储自定义标题的用户偏好键

TitleProps 接口定义在 Title.tsx,除上述三个核心 prop 外还支持 className 与 record(传入当前记录,供标题中的记录字段插值使用)。

从源码还可以看到一个防御性设计:Title.tsx 中当 defaultTitle 与 title 同时为空时会输出 'Missing title prop in <Title> element' 的警告,提醒开发者 <Title> 至少要提供二者之一。

四、title:字符串走翻译,React 元素原样渲染

title 接受两种类型的值,行为不同:

4.1 字符串:自动经过 translate

当 title 是字符串时,它会被传给 react-admin 的 translate 函数(参见 useTranslate.md)。因此你既可以传一段普通文案,也可以传一个翻译消息 id,由 i18n 消息文件按语言解析:

import { Title } from 'react-admin';

const CustomPage = () => (
    <>
        <Title title="my.custom.page.title" />
        <div>Content</div>
    </>
);

这种模式下,在 i18nProvider 的消息定义中加入 my.custom.page.title 键即可实现多语言标题。底层翻译逻辑位于 PageTitle.tsx,对字符串调用 translate(title, { _: title })——_: title 兜底参数保证了即使找不到对应翻译键,也会原样显示字符串本身。

4.2 React 元素:原样渲染,翻译由你负责

当 title 是 React 元素时,它会被原样渲染。此时组件不做任何翻译处理——如果元素中包含文本,翻译的责任在调用方。这一设计让你可以自由组合图标、富文本等内容:

import { Title } from 'react-admin';
import ArticleIcon from '@mui/icons-material/Article';

const ArticlePage = () => (
    <>
        <Title title={
            <>
                <ArticleIcon />
                My Custom Page
            </>
        } />
        <div>My Custom Content</div>
    </>
);

4.3 场景对照

场景title 值渲染结果
纯静态文案"All posts"原样显示
国际化"my.custom.page.title"经 translate 解析后显示
图标 + 文案<> <ArticleIcon /> My Custom Page </>原样渲染,需自行处理文案翻译
动态记录字段data && <> <ArticleIcon /> {data.title} </>随数据更新显示

五、defaultTitle:数据加载期间的防闪烁方案

实际业务中常见的问题是:页面标题依赖异步获取的数据(如编辑页的"编辑文章:某某标题"),数据未返回时标题为空,加载完成后又突然出现,产生明显的"闪烁"(flicker)。

defaultTitle 就是为这个场景设计的:它作为 title 为空时的回退内容。下面的示例来自原文档,展示了结合 useGetOne 的完整写法——数据未到时只显示图标,数据到位后显示"图标 + 标题":

import { Title, useGetOne } from 'react-admin';
import ArticleIcon from '@mui/icons-material/Article';

const ArticlePage = ({ id }) => {
    const { data, loading } = useGetOne('articles', { id });
    return (
        <>
            <Title
                title={data && <><ArticleIcon />{data.title}</>}
                defaultTitle={<ArticleIcon />}
            />
            {!loading && <div>{data.body}</div>}
        </>
    );
};

从 PageTitle.tsx 的源码可以看到 title 与 defaultTitle 的优先级逻辑:只有 title 为假值(未提供或仍为 null/undefined)时才渲染 defaultTitle;一旦 title 可用,立即切换。这保证了整个加载周期内 App Bar 标题区域始终有内容占位,体验平滑。

六、preferenceKey:Configurable 模式下标题的定制与持久化

6.1 默认行为:按路径自动生成偏好键

在 Configurable 模式下(用户可通过 App Bar 的配置按钮进入编辑状态),用户可以自定义页面标题,并通过 react-admin 的偏好存储(usePreference)持久化。为避免多个页面互相覆盖标题,<Title> 默认使用基于当前 pathname 的偏好键:

例如 posts 列表页的 <Title> 会使用 posts.title 作为偏好键。

这一默认逻辑实现在 PageTitleConfigurable.tsx:

const { pathname } = useLocation();
return (
    <Configurable
        editor={<PageTitleEditor />}
        preferenceKey={preferenceKey || `${pathname}.title`}
        ...
    >
        ...
    </Configurable>
);

其核心是 ra-core 的 Configurable 容器(偏好键为 preferenceKey || \${pathname}.title`),配合PageTitleEditor(一个绑定usePreferenceInput的 MUITextField)作为配置编辑器。用户保存后,自定义标题写入偏好存储;渲染时PageTitleConfigurableInner通过usePreference()读取偏好值,若存在则以用户定制内容优先(同样经过translate并支持记录字段插值{...record}),否则回退到title/defaultTitle` 的常规渲染逻辑(见 PageTitleConfigurable.tsx)。

偏好相关 hooks 的实现位于 packages/ra-core/src/preferences(usePreference.ts、usePreferenceInput.ts),存储于 react-admin 的 Store 中,刷新页面后依然保留。

6.2 自定义偏好键

如果你希望标题与某个语义化的键绑定(例如跨页面共享同一份定制标题),可以显式传入:

import { Title } from 'react-admin';

const CustomPage = () => (
    <>
        <Title title="My Custom Page" preferenceKey="my.custom.page.title" />
        <div>Content</div>
    </>
);

6.3 禁用配置:preferenceKey={false}

如果不希望用户即使在 Configurable 模式下也能修改某页标题,传入 preferenceKey={false} 即可。此时组件放弃 Configurable 包装,直接使用 PageTitle.tsx 的普通渲染路径(见 Title.tsx 中的分支判断):

import { Title } from 'react-admin';

const CustomPageWithNonConfigurableTitle = () => (
    <>
        <Title title="My Custom Page" preferenceKey={false} />
        <div>Content</div>
    </>
);

七、CRUD 页面:<List> / <Edit> / <Create> / <Show> 的标题内置机制

<List>、<Edit>、<Create>、<Show> 四个 CRUD 页面组件内部已经内置了 <Title>,所以你不需要(也不应该)在这些组件内部再手动渲染一个 <Title>,直接通过 title prop 设置即可:

import { List } from 'react-admin';

const PostList = () => (
    <List title="All posts">
        ...
    </List>
);

各 View 组件渲染 <Title> 的源码实现:

  • 列表页:ListView.tsx 使用 `${resource}.list.title` 作为偏好键;
  • 编辑页:EditView.tsx 使用 `${resource}.edit.title`;
  • 创建页:CreateView.tsx 使用 `${resource}.create.title`;
  • 详情页:ShowView.tsx 使用 `${resource}.show.title`。

同时四个 View 都遵循同一个约定:title !== false 时才渲染 <Title>,即传入 title={false} 可以完全禁用标题。

7.1 默认标题与翻译键

以列表页为例,<List> 的默认标题对应翻译键 ra.page.list,默认解析为资源的复数名称(如 "Posts")。react-admin 内置的英文与法文语言包(ra-language-english、ra-language-french)中均定义了该键。

你可以通过资源级翻译键 resources.RESOURCE.page.list 覆盖默认值,例如为 posts 资源定制:

// in src/i18n/en.js
import englishMessages from 'ra-language-english';

export const en = {
    ...englishMessages,
    resources: {
        posts: {
            name: 'Post |||| Posts',
            page: {
                list: 'Post list'
            }
        },
    },
    ...
};

优先级总结:title prop(显式指定)> 偏好存储中的用户定制标题 > 资源级翻译键 resources.RESOURCE.page.* > 全局键 ra.page.* > 默认值。

7.2 动态标题的推荐写法

编辑页、详情页的标题通常要展示记录字段(如"编辑:文章标题")。推荐在自定义视图组件中结合 useRecordContext 或 useGetOne 与 defaultTitle 组合使用,数据未到时不出现空白闪烁:

import { Title } from 'react-admin';
import { useRecordContext } from 'react-admin';

const PostTitle = () => {
    const record = useRecordContext();
    if (!record) return null;
    return <span>Post {record.title}</span>;
};

// 在 Edit 的 title 中传入组件
// <Edit title={<PostTitle />}>

八、综合示例:自定义页面 + 可配置标题

把上述知识点串起来,一个支持多语言、可配置、带防闪烁的完整自定义页面如下:

import { Title } from 'react-admin';
import ArticleIcon from '@mui/icons-material/Article';

const ArticlePage = () => (
    <>
        <Title
            title="resources.articles.title"        // 走 translate 的翻译键
            defaultTitle={<ArticleIcon />}           // 加载期间的占位内容
            preferenceKey="articles.custom.title"    // 自定义偏好键(持久化定制标题)
        />
        <div>My Custom Content</div>
    </>
);

如需禁用配置能力,将 preferenceKey 改为 {false} 即可。所有 <Title> 的导出入口见 packages/ra-ui-materialui/src/layout/index.ts(导出 Title、TitlePortal、PageTitleConfigurable 等),并从顶层 react-admin 包对外暴露。

九、小结与最佳实践

  • 自定义页面:手动渲染 <Title title="..." />,配合 translate 实现多语言;
  • CRUD 页面:直接使用 <List>/<Edit>/<Create>/<Show> 的 title prop,不要重复渲染 <Title>;需要禁用时传 title={false};
  • 异步标题:始终搭配 defaultTitle,避免数据加载期的空白闪烁;
  • 可配置化:默认偏好键按 pathname 自动生成、互不冲突;需要跨页共享或禁用定制时显式设置 preferenceKey;
  • 自定义 App Bar:务必保留 <TitlePortal /> 占位符,它是 <Title> 通过 Portal 渲染的目标容器,也是标题与搜索框等其他 App Bar 内容混排的挂载点。

相关文档延伸阅读:AppBar.md(App Bar 与 TitlePortal 布局)、List.md(列表页标题定制)、useTranslate.md(字符串标题的翻译机制)、Features.md(Configurable 模式总览)。

  • 前端
  • UI组件

【免费下载链接】react-admin

A 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),仅供参考

实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值