React 18 Alpha版本发布
React 18 Beta版本发布
React 18 RC版本发布
React 18正式版发布
# 1. 安装React 18和React DOM 18
npm install react@18 react-dom@18
# 2. 更新入口文件
// 从React 17及更早版本
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>
document.getElementById('root')
);
// 升级为React 18
import React from 'react';
import { createRoot } from 'react-dom/client'; // 注意新的导入方式
import App from './App';
// 获取根DOM元素
const container = document.getElementById('root');
// 创建根
const root = createRoot(container);
// 渲染应用
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
ReactDOM.render)并发渲染是React 18最重要的特性,它允许React同时准备多个版本的UI。
import { startTransition } from 'react';
// 将非紧急的更新标记为"transition"
function SearchBox() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
function handleSearch(value) {
setQuery(value); // 紧急更新:立即执行
// 非紧急更新:使用startTransition包裹
startTransition(() => {
// 获取搜索结果(可能是耗时的操作)
fetchResults(value).then(data => {
setResults(data);
});
});
}
return (
<div>
<input
value={query}
onChange={(e) => handleSearch(e.target.value)}
/>
<ResultsList results={results} />
</div>
);
}
React 18扩展了批处理的范围,现在包括事件处理器、Promise、setTimeout和原生事件处理程序中的状态更新。
// React 17:只在React事件处理程序中批处理
function handleClick() {
setCount(c => c + 1); // 重新渲染
setFlag(f => !f); // 重新渲染
// 结果:只重新渲染一次(批处理)
}
// React 17:异步代码中不会批处理
setTimeout(() => {
setCount(c => c + 1); // 重新渲染
setFlag(f => !f); // 重新渲染
// 结果:重新渲染两次(无批处理)
}, 1000);
// React 17:Promise中不会批处理
fetch('/api/data').then(() => {
setCount(c => c + 1); // 重新渲染
setFlag(f => !f); // 重新渲染
// 结果:重新渲染两次(无批处理)
});
// React 18:所有场景都自动批处理
function handleClick() {
setCount(c => c + 1);
setFlag(f => !f);
// 结果:只重新渲染一次
}
setTimeout(() => {
setCount(c => c + 1);
setFlag(f => !f);
// 结果:只重新渲染一次(React 18新行为)
}, 1000);
fetch('/api/data').then(() => {
setCount(c => c + 1);
setFlag(f => !f);
// 结果:只重新渲染一次(React 18新行为)
});
// 如果需要立即重新渲染(不推荐),可以使用flushSync
import { flushSync } from 'react-dom';
flushSync(() => {
setCount(c => c + 1); // 立即重新渲染
});
setFlag(f => !f); // 另一次重新渲染
标记非紧急的状态更新,允许用户界面在更新进行时保持响应。
import { useTransition } from 'react';
function App() {
const [isPending, startTransition] = useTransition();
const [filter, setFilter] = useState('');
const [results, setResults] = useState(initialResults);
function handleFilterChange(value) {
// 紧急:立即更新输入框
setFilter(value);
// 非紧急:搜索结果可以稍后更新
startTransition(() => {
setResults(computeExpensiveResults(value));
});
}
return (
<div>
<input
value={filter}
onChange={(e) => handleFilterChange(e.target.value)}
/>
{isPending && <Spinner />}
<ResultsList results={results} />
</div>
);
}
延迟更新某个值,类似于防抖,但更适合React的渲染模型。
import { useDeferredValue } from 'react';
function SearchResults({ query }) {
// query是紧急更新,deferredQuery是非紧急更新
const deferredQuery = useDeferredValue(query);
// 根据延迟的值进行昂贵的计算
const suggestions = useMemo(() => {
return computeSuggestions(deferredQuery);
}, [deferredQuery]);
return (
<>
<SearchInput query={query} />
<SuggestionList suggestions={suggestions} />
<>
);
}
生成在服务端和客户端之间稳定的唯一ID,用于无障碍属性。
import { useId } from 'react';
function EmailForm() {
const id = useId();
return (
<>
<label htmlFor={`${id}-email`}>邮箱</label>
<input id={`${id}-email`} type="email" />
<label htmlFor={`${id}-name`}>姓名</label>
<input id={`${id}-name`} type="text" />
</>
);
}
// 在SSR和CSR中生成的ID保持一致
// SSR: :r1:-email, :r1:-name
// CSR: :r1:-email, :r1:-name
用于在并发渲染中安全地从外部数据源读取数据。
import { useSyncExternalStore } from 'react';
function useOnlineStatus() {
const isOnline = useSyncExternalStore(
subscribe, // 订阅函数
getSnapshot, // 获取当前快照
getServerSnapshot // 服务端快照(SSR时使用)
);
return isOnline;
}
// 具体实现
function subscribe(callback) {
window.addEventListener('online', callback);
window.addEventListener('offline', callback);
return () => {
window.removeEventListener('online', callback);
window.removeEventListener('offline', callback);
};
}
function getSnapshot() {
return navigator.onLine;
}
function getServerSnapshot() {
return true; // 服务端渲染时的默认值
}
在DOM变更前同步执行,主要用于CSS-in-JS库。
import { useInsertionEffect } from 'react';
// 供CSS-in-JS库使用,普通应用很少需要
function useCSS(rule) {
useInsertionEffect(() => {
// 在DOM变更前注入样式
const style = document.createElement('style');
style.textContent = rule;
document.head.appendChild(style);
return () => {
document.head.removeChild(style);
};
});
return rule;
}
// React 18之前的SSR
import { renderToString } from 'react-dom/server';
import App from './App';
// 阻塞式渲染
const html = renderToString(<App />);
res.send(\`
<!DOCTYPE html>
<html>
<body>
<div id="root">\${html}</div>
<script src="/client.js"></script>
</body>
</html>
\`);
// React 18的流式SSR
import { renderToPipeableStream } from 'react-dom/server';
import App from './App';
app.get('/', (req, res) => {
const { pipe } = renderToPipeableStream(<App />, {
// Bootstrap脚本
bootstrapScripts: ['/main.js'],
// 完成回调
onShellReady() {
res.setHeader('Content-type', 'text/html');
res.write(\`
<!DOCTYPE html>
<html>
<body>
<div id="root">
\`);
pipe(res);
res.write(\`
</div>
</body>
</html>
\`);
},
onShellError(error) {
// 错误处理
res.status(500).send('<h1>出错了</h1>');
}
});
});
import { hydrateRoot } from 'react-dom/client';
import App from './App';
// React 18之前:整个应用一次性水合
hydrate(document.getElementById('root'), <App />);
// React 18:支持选择性水合
const root = hydrateRoot(document.getElementById('root'), <App />);
// 使用Suspense实现部分水合
function App() {
return (
<html>
<head>
<title>我的应用</title>
</head>
<body>
<Header />
<Sidebar />
<main>
<Suspense fallback={<Spinner />}>
<Content /> {/* 这个组件可以延迟水合 */}
</Suspense>
</main>
<Footer />
</body>
</html>
);
}
// 组件可以在服务端使用Suspense
async function Content() {
// 模拟数据获取
const data = await fetchData();
return <div>{data}</div>;
}
function App() {
return (
<div>
<Header />
<Suspense fallback={<div>加载中...</div>}>
<Content />
</Suspense>
</div>
);
}
// 服务端渲染时,Content组件会先渲染fallback
// 当数据准备好后,再发送实际的HTML
// React 18严格模式在开发环境下:
// 1. 组件会挂载两次
// 2. Effect会运行两次
// 目的是帮助发现副作用相关问题
function MyComponent() {
console.log('组件渲染'); // 开发环境会打印两次
useEffect(() => {
console.log('Effect运行'); // 开发环境会运行两次
return () => {
console.log('Effect清理'); // 也会运行两次
};
}, []);
return <div>Hello</div>;
}
// 正确应对双重调用的方式:
function MyComponent() {
const ref = useRef(null);
useEffect(() => {
// 使用ref确保只执行一次
if (ref.current) return;
ref.current = true;
// 实际的副作用逻辑
const subscription = dataSource.subscribe();
return () => {
subscription.unsubscribe();
};
}, []);
// 或者使用idempotent操作
useEffect(() => {
// 幂等操作:多次执行结果相同
document.title = '我的页面';
}, []);
}
ref="myRef"| API | 描述 | 使用场景 | 替代方案 |
|---|---|---|---|
createRoot |
新的应用根节点创建方式 | 替换ReactDOM.render |
ReactDOM.render 弃用 |
hydrateRoot |
新的SSR水合方式 | 替换ReactDOM.hydrate |
ReactDOM.hydrate 弃用 |
startTransition |
标记非紧急更新 | 优化用户交互响应性 | 无直接替代 |
useTransition |
返回过渡状态和启动函数 | 显示加载状态 | 无直接替代 |
useDeferredValue |
延迟一个值的更新 | 防抖/节流场景 | useDebounce/useThrottle |
useId |
生成唯一ID | 无障碍属性、表单标签 | 手动生成ID |
useSyncExternalStore |
安全读取外部存储 | 集成外部状态管理 | 直接订阅 |
useInsertionEffect |
DOM变更前执行 | CSS-in-JS库 | useLayoutEffect |
flushSync |
强制同步更新 | 需要立即DOM更新的场景 | 无直接替代 |
// 好的实践:区分紧急和非紧急更新
function SearchPage() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
const [isPending, startTransition] = useTransition();
function handleSearch(newQuery) {
// 紧急:立即更新输入框
setQuery(newQuery);
// 非紧急:搜索结果可以稍后更新
startTransition(() => {
searchAPI(newQuery).then(data => {
setResults(data);
});
});
}
return (
<div>
<input value={query} onChange={e => handleSearch(e.target.value)} />
{isPending && <Spinner />}
<ResultsList results={results} />
</div>
);
}
// 优化大型列表渲染
function ProductList({ products, searchTerm }) {
const deferredSearchTerm = useDeferredValue(searchTerm);
const filteredProducts = useMemo(() => {
// 根据延迟的搜索词过滤产品
return products.filter(product =>
product.name.toLowerCase().includes(deferredSearchTerm.toLowerCase())
);
}, [products, deferredSearchTerm]);
return (
<ul>
{filteredProducts.map(product => (
<ProductItem key={product.id} product={product} />
))}
</ul>
);
}
// 使用Suspense实现渐进式加载
function App() {
return (
<Suspense fallback={<FullPageSpinner />}>
<Layout>
<Navbar />
<Suspense fallback={<SidebarSkeleton />}>
<Sidebar />
</Suspense>
<Suspense fallback={<ContentSkeleton />}>
<Content />
</Suspense>
</Layout>
</Suspense>
);
}
// 配合数据获取
function ProductDetails({ id }) {
const product = use(fetchProduct(id)); // 假设use是实验性API
return (
<div>
<h1>{product.name}</h1>
<p>{product.description}</p>
</div>
);
}
// 步骤1:更新依赖
// package.json
{
"dependencies": {
"react": "^18.0.0",
"react-dom": "^18.0.0"
}
}
// 步骤2:更新入口文件
// index.js (React 17)
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>
document.getElementById('root')
);
// index.js (React 18)
import React from 'react';
import { createRoot } from 'react-dom/client';
import App from './App';
const container = document.getElementById('root');
const root = createRoot(container);
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
// 步骤3:处理类型检查
// 如果使用TypeScript,更新类型定义
npm install --save-dev @types/react @types/react-dom
// 步骤4:测试并发特性
// 逐步添加startTransition, useDeferredValue等新特性
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 组件渲染两次 | React 18严格模式 | 确保副作用是幂等的,或暂时禁用严格模式 |
| 控制台警告 | 使用了弃用的API | 更新为新的API(如createRoot) |
| 测试失败 | 测试库不兼容React 18 | 更新测试库版本,如react-testing-library |
| TypeScript错误 | 类型定义不匹配 | 更新@types/react和@types/react-dom |
| 第三方库兼容性问题 | 库未适配React 18 | 等待库更新或使用兼容层 |
| 浏览器 | 支持情况 | 最低版本 |
|---|---|---|
| Chrome | 完全支持 | 90+ |
| Firefox | 完全支持 | 89+ |
| Safari | 完全支持 | 14.1+ |
| Edge | 完全支持 | 90+ |
| Internet Explorer | 不支持 | 不支持 |
React 18为未来的功能奠定了基础: