React API & 组件
API
react 包还导出了一些其他的 API,这些 API 对于创建组件非常有用
createContext
可以创建一个 context,你可以将其提供给子组件,通常会与 useContext 一起配合使用。
forwardRef
允许组件将 DOM 节点作为 ref 暴露给父组件。
lazy
允许你延迟加载组件,直到该组件需要第一次被渲染。
lazy(load)
参数
load
: 一个返回 Promise 或另一个 thenable(具有 then 方法的类 Promise 对象)的函数。React 不会在你尝试首次渲染返回的组件之前调用 load 函数。在 React 首次调用 load 后,它将等待其解析,然后将解析值的 .default 渲染为 React 组件。返回的 Promise 和 Promise 的解析值都将被缓存,因此 React 不会多次调用 load 函数。如果 Promise 被拒绝,则 React 将抛出拒绝原因给最近的错误边界处理。
返回值
- lazy 返回一个 React 组件,你可以在 fiber 树中渲染。当懒加载组件的代码仍在加载时,尝试渲染它将会处于 暂停 状态。使用
可以在其加载时显示一个正在加载的提示。
import { lazy } from 'react';
const MarkdownPreview = lazy(() => import('./MarkdownPreview.js'));
// 此代码依赖于 动态 import(),因此可能需要你的打包工具或框架提供支持。使用这种模式要求导入的懒加载组件必须作为 default 导出。
<Suspense fallback={<Loading />}>
<h2>Preview</h2>
<MarkdownPreview />
</Suspense>
不要在其他组件 内部 声明 lazy 组件
import { lazy } from 'react';
// ✅ Good: 将 lazy 组件声明在组件外部
// const MarkdownPreview = lazy(() => import('./MarkdownPreview.js'));
function Editor() {
// 🔴 Bad: 这将导致在重新渲染时重置所有状态
const MarkdownPreview = lazy(() => import('./MarkdownPreview.js'));
// ...
}
memo
React 通常在其父组件重新渲染时重新渲染一个组件。你可以使用 memo 创建一个组件,当它的父组件重新渲染时,只要它的新 props 与旧 props 相同时,React 就不会重新渲染它。这样的组件被称为 记忆化的(memoized)组件。通常 useMemo 与 useCallback 一起配合使用。
参考
memo(Component, arePropsEqual?)
参数
Component
:要进行记忆化的组件。memo 不会修改该组件,而是返回一个新的、记忆化的组件。它接受任何有效的 React 组件,包括函数组件和 forwardRef 组件。arePropsEqual
:一个函数,接受两个参数:组件的前一个 props 和新的 props。如果旧的和新的 props 相等,即组件使用新的 props 渲染的输出和表现与旧的 props 完全相同,则它应该返回 true。否则返回 false。通常情况下,你不需要指定此函数。默认情况下,React 将使用Object.is
比较每个 prop。
返回值 memo 返回一个新的 React 组件。它的行为与提供给 memo 的组件相同,只是当它的父组件重新渲染时 React 不会总是重新渲染它,除非它的 props 发生了变化。
当 props 没有改变时跳过重新渲染
通过使用 memo
,只要其 props 没有改变,React 就不需要重新渲染。即使使用 memo,如果它自己的 state 或正在使用的 context 发生更改,组件也会重新渲染。
export const Greeting = memo(function Greeting({ name }) {
return <h1>Hello, {name}!</h1>;
});
最小化 props 的变化
useMemo 避免父组件每次都重新创建该对象
function Page() {
const [name, setName] = useState('Taylor');
const [age, setAge] = useState(42);
const person = useMemo(
() => ({ name, age }),
[name, age]
);
return <Profile person={person} />;
}
const Profile = memo(function Profile({ person }) {
// ...
});
接受单独的值而不是整个对象
function Page() {
const [name, setName] = useState('Taylor');
const [age, setAge] = useState(42);
return <Profile name={name} age={age} />;
}
const Profile = memo(function Profile({ name, age }) {
// ...
});
接受一个布尔值,表示是否存在某个值,而不是值本身
function GroupsLanding({ person }) {
const hasGroups = person.groups !== null;
return <CallToAction hasGroups={hasGroups} />;
}
const CallToAction = memo(function CallToAction({ hasGroups }) {
// ...
});
自定义比较函数
你可以提供一个自定义比较函数,React 将使用它来比较旧的和新的 props,而不是使用浅比较。
const Chart = memo(function Chart({ dataPoints }) {
// ...
}, arePropsEqual);
function arePropsEqual(oldProps, newProps) {
// 在新的 props 与旧的 props 具有相同的输出时返回 true;否则应该返回 false
return Object.is(oldProps, newProps);
}
startTransition
startTransition 可以让你在不阻塞 UI 的情况下更新 state, startTransition 与 useTransition 非常相似,但它不提供 isPending 标志来跟踪一个 transition 是否正在进行。你可以在 useTransition 不可用时调用 startTransition。例如,在组件外部(如从数据库中)使用 startTransition
startTransition(scope)
参数
scope
:调用一个或多个 set 函数 来更新 state 的函数。React 会立即调用没有参数的 scope,并将在 scope 函数调用期间,调度所有的 state,并将同步更新标记为 transition。
返回值
- 无
注意事项
- startTransition 没有提供一种跟踪 transition 是否处于待定状态的方法。为了在 transition 进行时显示一个待定状态的指示器,你需要使用 useTransition
- 传递给 startTransition 的函数必须是同步的。React 会立即执行此函数,将其执行期间发生的所有 state 更新标记为 transition。
- 一个被标记为 transition 的 state 更新时将会被其他 state 更新打断。例如,如果你在 transition 内部更新图表组件,但在图表重新渲染时在输入框中打字,则 React 将先处理输入 state 更新,之后才会重新启动对图表组件的渲染工作。
- 如果有多个正在进行的 transition,当前 React 会将它们集中在一起处理。这是一个限制,在未来的版本中可能会被移除。
将 state 更新标记为非阻塞 transition
transition 可以让用户界面在慢速设备上保持更新响应。例如,如果用户单击一个选项卡后又改变主意并单击另一个选项卡,则可以在第一次重新渲染完成之前执行此操作而无需等待。
import { startTransition } from 'react';
function TabContainer() {
const [tab, setTab] = useState('about');
function selectTab(nextTab) {
startTransition(() => {
setTab(nextTab);
});
}
// ...
}
组件
React 提供了一些内置的组件,你可以在 JSX 中使用它们
Fragment
通常使用 <>…</> 代替,它们都允许你在不添加额外节点的情况下将子元素组合。
<>
<OneChild />
<AnotherChild />
</>
Profiler
允许你编程式测量 React 树的渲染性能
参数
- id:字符串,用于标识正在测量的 UI 部分。
- onRender:onRender 回调函数,当包裹的组件树更新时,React 都会调用它。它接收有关渲染内容和所花费时间的信息。
<Profiler id="App" onRender={onRender}>
<App />
</Profiler>
function onRender(id, phase, actualDuration, baseDuration, startTime, commitTime) {
// 对渲染时间进行汇总或记录...
}
StrictMode
帮助你在开发过程中尽早地发现组件中的常见错误
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
const root = createRoot(document.getElementById('root'));
root.render(
<StrictMode>
<App />
</StrictMode>
);
严格模式启用了以下仅在开发环境下有效的行为:
- 组件将 重新渲染一次,以查找由于非纯渲染而引起的错误。
- 组件将 重新运行 Effect 一次,以查找由于缺少 Effect 清理而引起的错误。
- 组件将被 检查是否使用了已弃用的 API。
Suspense
允许在子组件完成加载前展示后备方案
<Suspense fallback={<Loading />}>
<SomeComponent />
</Suspense>
参数
children
:真正的 UI 渲染内容。如果 children 在渲染中被挂起,Suspense 边界将会渲染 fallback。fallback
:真正的 UI 未渲染完成时代替其渲染的备用 UI,它可以是任何有效的 React 节点。后备方案通常是一个轻量的占位符,例如表示加载中的图标或者骨架屏。当 children 被挂起时,Suspense 将自动切换至渲染 fallback;当数据准备好时,又会自动切换至渲染 children。如果 fallback 在渲染中被挂起,那么将自动激活最近的 Suspense 边界。
注意事项
- 如果 Suspense 正在展示 React 组件树中的内容,那么当再次被挂起时,除非导致此处更新是由 startTransition 或 useDeferredValue 引起,否则 Suspense 将展示 fallback
- 只有启用了 Suspense 的数据源才会激活 Suspense 组件, 1.支持 Suspense 的框架如 Relay 和 Next.js, 2.使用 lazy 懒加载组件代码,3.使用 use 读取 Promise 的值。(Suspense 无法 检测在 Effect 或事件处理程序中获取数据的情况)
逐步加载内容
// 调整之后,Biography 不需要“等待” Albums 加载完成就可以展示。
<Suspense fallback={<BigSpinner />}>
<Biography />
<Suspense fallback={<AlbumsGlimmer />}>
<Panel>
<Albums />
</Panel>
</Suspense>
</Suspense>
// 1.如果 Biography 没有加载完成,BigSpinner 会显示在整个内容区域的位置。
// 2.一旦 Biography 加载完成,BigSpinner 会被内容替换。
// 3.如果 Albums 没有加载完成,AlbumsGlimmer 会显示在 Albums 和它的父级 Panel 的位置。
// 4.最后,一旦 Albums 加载完成,它会替换 AlbumsGlimmer。
在新内容加载时展示过时内容
延迟值和 transition 都可以让你避免显示 Suspense 后备方案,而是使用内联指示器。transition 将整个更新标记为非紧急的,因此它们通常由框架和路由库用于导航。另一方面,延迟值在你希望将 UI 的一部分标记为非紧急,并让它“落后于” UI 的其余部分时非常有用
import { Suspense, useState, useDeferredValue } from 'react';
import SearchResults from './SearchResults.js';
export default function App() {
const [query, setQuery] = useState('');
const deferredQuery = useDeferredValue(query);
const isStale = query !== deferredQuery;
return (
<>
<label>
Search albums:
<input value={query} onChange={e => setQuery(e.target.value)} />
</label>
<Suspense fallback={<h2>Loading...</h2>}>
<div style={{ opacity: isStale ? 0.5 : 1 }}>
<SearchResults query={deferredQuery} />
</div>
</Suspense>
</>
);
}