Appearance
Breaking Changes破坏性变更
学习目标
通过本章学习,你将掌握:
- React 19所有破坏性变更
- 影响范围和解决方案
- 代码迁移指南
- 避免常见陷阱
- TypeScript类型变更
- 第三方库兼容性
- 升级检查清单
- 最佳实践
第一部分:已移除的API
1.1 ReactDOM.render
jsx
// ❌ React 18及更早:旧的渲染API
import ReactDOM from 'react-dom';
ReactDOM.render(<App />, document.getElementById('root'));
// ✅ React 19:必须使用createRoot
import { createRoot } from 'react-dom/client';
const root = createRoot(document.getElementById('root'));
root.render(<App />);
// 迁移步骤:
// 1. 替换导入
// 2. 创建root
// 3. 使用root.render()1.2 ReactDOM.hydrate
jsx
// ❌ React 18:旧的hydrate API
import ReactDOM from 'react-dom';
ReactDOM.hydrate(<App />, document.getElementById('root'));
// ✅ React 19:使用hydrateRoot
import { hydrateRoot } from 'react-dom/client';
hydrateRoot(document.getElementById('root'), <App />);
// SSR应用迁移:
// 1. 服务器端保持不变
// 2. 客户端使用hydrateRoot1.3 ReactDOM.unmountComponentAtNode
jsx
// ❌ React 18:旧的卸载API
import ReactDOM from 'react-dom';
const root = document.getElementById('root');
ReactDOM.render(<App />, root);
// 稍后卸载
ReactDOM.unmountComponentAtNode(root);
// ✅ React 19:使用root.unmount()
import { createRoot } from 'react-dom/client';
const container = document.getElementById('root');
const root = createRoot(container);
root.render(<App />);
// 稍后卸载
root.unmount();1.4 defaultProps(函数组件)
jsx
// ❌ React 18:defaultProps
function Button({ size = 'medium', children }) {
return <button className={`btn-${size}`}>{children}</button>;
}
Button.defaultProps = {
size: 'medium'
};
// ✅ React 19:使用默认参数
function Button({ size = 'medium', children }) {
return <button className={`btn-${size}`}>{children}</button>;
}
// 注意:类组件的defaultProps仍然支持
class Button extends React.Component {
static defaultProps = {
size: 'medium'
};
render() {
return <button>{this.props.children}</button>;
}
}1.5 propTypes
jsx
// ❌ React 18:内置propTypes支持
import PropTypes from 'prop-types';
function User({ name, age }) {
return <div>{name}: {age}</div>;
}
User.propTypes = {
name: PropTypes.string.isRequired,
age: PropTypes.number
};
// ✅ React 19:使用TypeScript
interface UserProps {
name: string;
age?: number;
}
function User({ name, age }: UserProps) {
return <div>{name}: {age}</div>;
}
// 或者继续使用prop-types库(需要单独安装)
import PropTypes from 'prop-types';
User.propTypes = {
name: PropTypes.string.isRequired,
age: PropTypes.number
};1.6 Legacy Context API
jsx
// ❌ React 18:旧的Context API
import PropTypes from 'prop-types';
class Parent extends React.Component {
static childContextTypes = {
theme: PropTypes.string
};
getChildContext() {
return { theme: 'dark' };
}
render() {
return <Child />;
}
}
class Child extends React.Component {
static contextTypes = {
theme: PropTypes.string
};
render() {
return <div>Theme: {this.context.theme}</div>;
}
}
// ✅ React 19:使用新的Context API
import { createContext, useContext } from 'react';
const ThemeContext = createContext('light');
function Parent() {
return (
<ThemeContext value="dark">
<Child />
</ThemeContext>
);
}
function Child() {
const theme = useContext(ThemeContext);
return <div>Theme: {theme}</div>;
}第二部分:API行为变更
2.1 ref清理函数
jsx
// ❌ React 18:ref callback无返回值
function Component() {
const ref = useCallback((node) => {
if (node) {
// 设置
node.focus();
} else {
// 清理(组件卸载时node为null)
// 但无法执行清理逻辑
}
}, []);
return <input ref={ref} />;
}
// ✅ React 19:ref callback可以返回清理函数
function Component() {
const ref = useCallback((node) => {
if (node) {
node.focus();
// 返回清理函数
return () => {
console.log('Cleanup');
node.blur();
};
}
}, []);
return <input ref={ref} />;
}
// 清理函数会在以下时机调用:
// 1. 组件卸载时
// 2. ref改变时
// 3. ref callback重新执行前2.2 Context.Provider简化
jsx
// ❌ React 18:必须使用Provider组件
const ThemeContext = createContext('light');
function App() {
return (
<ThemeContext.Provider value="dark">
<Page />
</ThemeContext.Provider>
);
}
// ✅ React 19:Context即Provider
const ThemeContext = createContext('light');
function App() {
// 两种方式都可以
// 方式1:新语法(推荐)
return (
<ThemeContext value="dark">
<Page />
</ThemeContext>
);
// 方式2:旧语法(仍然支持)
return (
<ThemeContext.Provider value="dark">
<Page />
</ThemeContext.Provider>
);
}2.3 useReducer初始化
jsx
// ❌ React 18:可以省略初始action
const [state, dispatch] = useReducer(reducer, initialState);
// ✅ React 19:行为更严格
// 如果reducer期望action参数,必须提供
function reducer(state, action) {
// action不能为undefined
switch (action.type) {
case 'INCREMENT':
return state + 1;
default:
return state;
}
}
// 正确用法
const [state, dispatch] = useReducer(
reducer,
0,
// 如果需要初始化函数,必须返回有效的state
(initialState) => initialState
);2.4 StrictMode更严格
jsx
// React 19的StrictMode更严格
// ❌ 会报错:副作用在渲染中执行
function Component() {
// 不要在渲染中直接修改DOM
document.title = 'New Title'; // 错误!
return <div>Content</div>;
}
// ✅ 使用useEffect
function Component() {
useEffect(() => {
document.title = 'New Title';
}, []);
return <div>Content</div>;
}
// ❌ 会报错:渲染中的异步操作
function Component() {
fetch('/api/data'); // 错误!
return <div>Content</div>;
}
// ✅ 使用useEffect或use()
function Component() {
const dataPromise = fetch('/api/data').then(r => r.json());
const data = use(dataPromise);
return <div>{data}</div>;
}第三部分:TypeScript类型变更
3.1 ref类型更新
typescript
// ❌ React 18:ref类型
import { Ref } from 'react';
interface ButtonProps {
ref?: Ref<HTMLButtonElement>;
children: React.ReactNode;
}
// ✅ React 19:ref作为普通prop
interface ButtonProps {
ref?: React.RefObject<HTMLButtonElement> |
((instance: HTMLButtonElement | null) => void | (() => void));
children: React.ReactNode;
}
// 或者使用新的类型工具
interface ButtonProps {
ref?: React.ComponentRef<'button'>;
children: React.ReactNode;
}3.2 forwardRef不再需要
typescript
// ❌ React 18:必须用forwardRef
import { forwardRef } from 'react';
interface InputProps {
placeholder?: string;
}
const Input = forwardRef<HTMLInputElement, InputProps>(
(props, ref) => {
return <input ref={ref} {...props} />;
}
);
// ✅ React 19:ref作为普通prop
interface InputProps {
ref?: React.Ref<HTMLInputElement>;
placeholder?: string;
}
function Input({ ref, ...props }: InputProps) {
return <input ref={ref} {...props} />;
}3.3 Context类型简化
typescript
// ❌ React 18:需要定义Provider类型
import { createContext, Provider } from 'react';
interface Theme {
mode: 'light' | 'dark';
colors: Record<string, string>;
}
const ThemeContext = createContext<Theme | undefined>(undefined);
type ThemeProviderProps = {
value: Theme;
children: React.ReactNode;
};
// ✅ React 19:Context即Provider
const ThemeContext = createContext<Theme | undefined>(undefined);
// 直接使用
<ThemeContext value={theme}>
<App />
</ThemeContext>
// 类型自动推导3.4 Hook返回类型更新
typescript
// use()的类型定义
function use<T>(promise: Promise<T>): T;
function use<T>(context: React.Context<T>): T;
// useActionState的类型
function useActionState<State, Payload>(
action: (state: State, payload: Payload) => Promise<State>,
initialState: State,
permalink?: string
): [state: State, dispatch: (payload: Payload) => void, isPending: boolean];
// useOptimistic的类型
function useOptimistic<State, Action>(
passthrough: State,
reducer: (state: State, action: Action) => State
): [optimisticState: State, dispatch: (action: Action) => void];第四部分:服务器端渲染变更
4.1 renderToString变更
javascript
// ❌ React 18:同步renderToString
import { renderToString } from 'react-dom/server';
app.get('/', (req, res) => {
const html = renderToString(<App />);
res.send(`<!DOCTYPE html><html><body>${html}</body></html>`);
});
// ✅ React 19:推荐使用流式渲染
import { renderToPipeableStream } from 'react-dom/server';
app.get('/', (req, res) => {
const { pipe } = renderToPipeableStream(<App />, {
onShellReady() {
res.statusCode = 200;
res.setHeader('Content-Type', 'text/html');
pipe(res);
}
});
});
// renderToString仍然支持,但:
// - 不支持Suspense
// - 不支持Server Components
// - 性能较差4.2 hydration警告更严格
jsx
// React 19对hydration不匹配更严格
// ❌ 会报错:服务器和客户端不一致
// server.js
<div>{new Date().toISOString()}</div>
// client.js
<div>{new Date().toISOString()}</div>
// ✅ 使用suppressHydrationWarning
<div suppressHydrationWarning>
{new Date().toISOString()}
</div>
// ✅ 或者确保一致性
function ServerTime() {
const [time, setTime] = useState(() => {
// 使用传递的时间
return typeof window !== 'undefined'
? window.__INITIAL_TIME__
: new Date().toISOString();
});
return <div>{time}</div>;
}第五部分:第三方库兼容性
5.1 常见库的兼容性
✅ 完全兼容:
- React Router v6.4+
- Redux Toolkit v1.9+
- React Query v5+
- Zustand v4+
- Formik v2.4+
⚠️ 需要更新:
- React Router v5 → v6
- Redux v4 → Redux Toolkit
- React Query v4 → v5
❌ 暂不兼容:
- 一些旧的HOC库
- 使用Legacy Context的库
- 依赖旧渲染API的库5.2 检查库兼容性
bash
# 检查依赖是否与React 19兼容
npx react-check-deps
# 或者手动检查
npm list react react-dom
# 查看库的React版本要求
npm info react-router-dom peerDependencies5.3 迁移常见库
jsx
// React Router v5 → v6
// ❌ v5
import { Switch, Route } from 'react-router-dom';
<Switch>
<Route path="/about" component={About} />
<Route path="/" component={Home} />
</Switch>
// ✅ v6
import { Routes, Route } from 'react-router-dom';
<Routes>
<Route path="/about" element={<About />} />
<Route path="/" element={<Home />} />
</Routes>
// Redux连接组件
// ❌ 旧方式
import { connect } from 'react-redux';
const mapStateToProps = state => ({
user: state.user
});
export default connect(mapStateToProps)(UserProfile);
// ✅ 新方式(推荐)
import { useSelector } from 'react-redux';
function UserProfile() {
const user = useSelector(state => state.user);
return <div>{user.name}</div>;
}第六部分:升级检查清单
6.1 代码检查
bash
# 使用自动化工具检查
npx react-codemod react-19 src/
# 检查项目:
✅ ReactDOM.render → createRoot
✅ ReactDOM.hydrate → hydrateRoot
✅ forwardRef → 普通ref prop
✅ Context.Provider → Context
✅ defaultProps → 默认参数
✅ propTypes → TypeScript6.2 手动检查清单
[ ] 所有ReactDOM.render已替换
[ ] 所有ReactDOM.hydrate已替换
[ ] 移除不必要的forwardRef
[ ] 简化Context.Provider
[ ] 检查ref callback清理
[ ] 更新TypeScript类型
[ ] 测试StrictMode
[ ] 检查第三方库兼容性
[ ] 运行所有测试
[ ] 检查SSR hydration
[ ] 性能测试
[ ] 用户验收测试6.3 测试验证
javascript
// 测试破坏性变更
describe('React 19 Breaking Changes', () => {
test('createRoot API', () => {
const container = document.createElement('div');
const root = createRoot(container);
act(() => {
root.render(<App />);
});
expect(container.textContent).toBe('App');
act(() => {
root.unmount();
});
});
test('ref cleanup', () => {
const cleanupSpy = jest.fn();
function Component() {
const ref = useCallback((node) => {
if (node) {
return cleanupSpy;
}
}, []);
return <div ref={ref} />;
}
const { unmount } = render(<Component />);
unmount();
expect(cleanupSpy).toHaveBeenCalled();
});
test('Context as Provider', () => {
const Context = createContext('default');
function Consumer() {
const value = useContext(Context);
return <div>{value}</div>;
}
const { getByText } = render(
<Context value="test">
<Consumer />
</Context>
);
expect(getByText('test')).toBeInTheDocument();
});
});第七部分:详细迁移指南
7.1 createRoot完整迁移
javascript
// 完整的迁移示例
// ❌ React 18代码
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
// 开发环境
if (process.env.NODE_ENV === 'development') {
ReactDOM.render(<App />, document.getElementById('root'));
} else {
ReactDOM.hydrate(<App />, document.getElementById('root'));
}
// ✅ React 19迁移后
import React from 'react';
import { createRoot } from 'react-dom/client';
import { hydrateRoot } from 'react-dom/client';
import App from './App';
const container = document.getElementById('root');
if (container.hasChildNodes()) {
// SSR情况使用hydrate
hydrateRoot(container, <App />);
} else {
// 普通渲染使用createRoot
const root = createRoot(container);
root.render(<App />);
}
// 进阶:错误处理
const container = document.getElementById('root');
if (!container) {
throw new Error('Root element not found');
}
try {
if (container.hasChildNodes()) {
hydrateRoot(container, <App />, {
onRecoverableError: (error) => {
console.error('Hydration error:', error);
// 上报错误
reportError(error);
}
});
} else {
const root = createRoot(container, {
onRecoverableError: (error) => {
console.error('Render error:', error);
reportError(error);
}
});
root.render(<App />);
}
} catch (error) {
console.error('Failed to render:', error);
// 显示错误UI
container.innerHTML = '<div>Failed to load app</div>';
}7.2 ref迁移完整示例
typescript
// 复杂的ref迁移场景
// ❌ React 18:复杂的forwardRef
import { forwardRef, useImperativeHandle, useRef } from 'react';
interface VideoPlayerProps {
src: string;
autoPlay?: boolean;
}
interface VideoPlayerRef {
play: () => void;
pause: () => void;
seek: (time: number) => void;
}
const VideoPlayer = forwardRef<VideoPlayerRef, VideoPlayerProps>(
({ src, autoPlay }, ref) => {
const videoRef = useRef<HTMLVideoElement>(null);
useImperativeHandle(ref, () => ({
play() {
videoRef.current?.play();
},
pause() {
videoRef.current?.pause();
},
seek(time: number) {
if (videoRef.current) {
videoRef.current.currentTime = time;
}
}
}));
return <video ref={videoRef} src={src} autoPlay={autoPlay} />;
}
);
// ✅ React 19:简化的ref
interface VideoPlayerProps {
ref?: React.Ref<VideoPlayerRef>;
src: string;
autoPlay?: boolean;
}
interface VideoPlayerRef {
play: () => void;
pause: () => void;
seek: (time: number) => void;
}
function VideoPlayer({ ref, src, autoPlay }: VideoPlayerProps) {
const videoRef = useRef<HTMLVideoElement>(null);
// 仍然可以使用useImperativeHandle
useImperativeHandle(ref, () => ({
play() {
videoRef.current?.play();
},
pause() {
videoRef.current?.pause();
},
seek(time: number) {
if (videoRef.current) {
videoRef.current.currentTime = time;
}
}
}));
return <video ref={videoRef} src={src} autoPlay={autoPlay} />;
}
// 使用示例
function App() {
const playerRef = useRef<VideoPlayerRef>(null);
return (
<>
<VideoPlayer ref={playerRef} src="/video.mp4" />
<button onClick={() => playerRef.current?.play()}>Play</button>
<button onClick={() => playerRef.current?.pause()}>Pause</button>
</>
);
}
// ref清理函数示例
function ScrollTracker() {
const ref = useCallback((node: HTMLDivElement | null) => {
if (node) {
const handleScroll = () => {
console.log('Scrolled:', node.scrollTop);
};
node.addEventListener('scroll', handleScroll);
// React 19新特性:返回清理函数
return () => {
node.removeEventListener('scroll', handleScroll);
console.log('Cleanup scroll listener');
};
}
}, []);
return <div ref={ref} style={{ height: 200, overflow: 'auto' }}>
{/* content */}
</div>;
}7.3 Context迁移完整示例
typescript
// 复杂的Context迁移
// ❌ React 18:复杂的Context设置
import { createContext, useContext, useState, ReactNode } from 'react';
interface Theme {
mode: 'light' | 'dark';
primaryColor: string;
fontSize: number;
}
interface ThemeContextValue {
theme: Theme;
setTheme: (theme: Theme) => void;
toggleMode: () => void;
}
const ThemeContext = createContext<ThemeContextValue | undefined>(undefined);
export function ThemeProvider({ children }: { children: ReactNode }) {
const [theme, setTheme] = useState<Theme>({
mode: 'light',
primaryColor: '#007bff',
fontSize: 16
});
const toggleMode = () => {
setTheme(prev => ({
...prev,
mode: prev.mode === 'light' ? 'dark' : 'light'
}));
};
const value = { theme, setTheme, toggleMode };
return (
<ThemeContext.Provider value={value}>
{children}
</ThemeContext.Provider>
);
}
export function useTheme() {
const context = useContext(ThemeContext);
if (!context) {
throw new Error('useTheme must be used within ThemeProvider');
}
return context;
}
// ✅ React 19:简化的Context
const ThemeContext = createContext<ThemeContextValue | undefined>(undefined);
export function ThemeProvider({ children }: { children: ReactNode }) {
const [theme, setTheme] = useState<Theme>({
mode: 'light',
primaryColor: '#007bff',
fontSize: 16
});
const toggleMode = () => {
setTheme(prev => ({
...prev,
mode: prev.mode === 'light' ? 'dark' : 'light'
}));
};
const value = { theme, setTheme, toggleMode };
// 方式1:使用简化语法(推荐)
return (
<ThemeContext value={value}>
{children}
</ThemeContext>
);
// 方式2:仍然可以用Provider(向后兼容)
// return (
// <ThemeContext.Provider value={value}>
// {children}
// </ThemeContext.Provider>
// );
}
// Hook保持不变
export function useTheme() {
const context = useContext(ThemeContext);
if (!context) {
throw new Error('useTheme must be used within ThemeProvider');
}
return context;
}
// 多Context嵌套简化
// ❌ React 18:嵌套Provider
function App() {
return (
<ThemeContext.Provider value={themeValue}>
<UserContext.Provider value={userValue}>
<I18nContext.Provider value={i18nValue}>
<RouterContext.Provider value={routerValue}>
<MainApp />
</RouterContext.Provider>
</I18nContext.Provider>
</UserContext.Provider>
</ThemeContext.Provider>
);
}
// ✅ React 19:更清晰的嵌套
function App() {
return (
<ThemeContext value={themeValue}>
<UserContext value={userValue}>
<I18nContext value={i18nValue}>
<RouterContext value={routerValue}>
<MainApp />
</RouterContext>
</I18nContext>
</UserContext>
</ThemeContext>
);
}7.4 StrictMode影响处理
jsx
// StrictMode在React 19中更严格
// ❌ React 18:可能通过的代码
function Component() {
// 直接修改props(不推荐但可能不报错)
props.data = [...props.data, newItem];
// 渲染中设置定时器
setTimeout(() => {
setState(newState);
}, 1000);
// 渲染中的fetch
fetch('/api/data').then(setData);
return <div>{data}</div>;
}
// ✅ React 19:必须修复
function Component({ data: initialData }) {
const [data, setData] = useState(initialData);
const [fetchedData, setFetchedData] = useState(null);
// 副作用放在useEffect中
useEffect(() => {
const timer = setTimeout(() => {
setData([...data, newItem]);
}, 1000);
return () => clearTimeout(timer);
}, [data]);
// 数据获取使用use()或useEffect
useEffect(() => {
fetch('/api/data')
.then(res => res.json())
.then(setFetchedData);
}, []);
return <div>{fetchedData}</div>;
}
// 或使用React 19的use() Hook
function Component() {
const dataPromise = fetch('/api/data').then(r => r.json());
const data = use(dataPromise);
return <div>{data}</div>;
}
// StrictMode双重调用处理
function Component() {
const [count, setCount] = useState(0);
useEffect(() => {
// ❌ 不应该依赖只调用一次
console.log('Effect ran'); // StrictMode下会调用两次
// ✅ 应该是幂等的
const controller = new AbortController();
fetch('/api/data', { signal: controller.signal })
.then(res => res.json())
.then(setData);
return () => {
controller.abort(); // 清理
};
}, []);
}7.5 服务器端渲染迁移
javascript
// 完整的SSR迁移
// ❌ React 18 SSR
// server.js
import express from 'express';
import React from 'react';
import { renderToString } from 'react-dom/server';
import App from './App';
const app = express();
app.get('*', (req, res) => {
const html = renderToString(<App url={req.url} />);
res.send(`
<!DOCTYPE html>
<html>
<head>
<title>My App</title>
</head>
<body>
<div id="root">${html}</div>
<script src="/client.js"></script>
</body>
</html>
`);
});
// client.js
import ReactDOM from 'react-dom';
import App from './App';
ReactDOM.hydrate(<App />, document.getElementById('root'));
// ✅ React 19 SSR(流式渲染)
// server.js
import express from 'express';
import React from 'react';
import { renderToPipeableStream } from 'react-dom/server';
import App from './App';
const app = express();
app.get('*', (req, res) => {
const { pipe, abort } = renderToPipeableStream(<App url={req.url} />, {
bootstrapScripts: ['/client.js'],
onShellReady() {
res.statusCode = 200;
res.setHeader('Content-Type', 'text/html');
pipe(res);
},
onShellError(error) {
res.statusCode = 500;
res.send('<!DOCTYPE html><html><body>Server Error</body></html>');
},
onError(error) {
console.error('SSR error:', error);
}
});
// 超时处理
setTimeout(() => {
abort();
}, 10000);
});
// client.js
import { hydrateRoot } from 'react-dom/client';
import App from './App';
hydrateRoot(document.getElementById('root'), <App />);
// 高级:支持Suspense的SSR
// server.js
app.get('*', (req, res) => {
const { pipe, abort } = renderToPipeableStream(
<React.StrictMode>
<App url={req.url} />
</React.StrictMode>,
{
bootstrapScripts: ['/client.js'],
onShellReady() {
// Shell准备好(不等待Suspense)
res.statusCode = 200;
res.setHeader('Content-Type', 'text/html');
pipe(res);
},
onAllReady() {
// 所有内容准备好(包括Suspense)
// 可用于爬虫
},
onError(error) {
console.error(error);
}
}
);
});
// 带错误恢复的hydration
// client.js
import { hydrateRoot } from 'react-dom/client';
import App from './App';
const container = document.getElementById('root');
hydrateRoot(container, <App />, {
onRecoverableError: (error, errorInfo) => {
console.error('Hydration error:', error);
console.log('Component stack:', errorInfo.componentStack);
// 上报错误
reportError({
type: 'hydration',
error: error.message,
stack: errorInfo.componentStack
});
}
});7.6 TypeScript严格类型迁移
typescript
// TypeScript严格模式下的迁移
// ❌ React 18:宽松的类型
import { FC, ReactNode } from 'react';
interface Props {
children?: ReactNode;
onClick?: Function; // 宽松
ref?: any; // 太宽松
}
const Button: FC<Props> = (props) => {
return <button {...props} />;
};
// ✅ React 19:严格的类型
import { ReactNode, MouseEventHandler, ComponentPropsWithRef } from 'react';
interface ButtonProps {
children?: ReactNode;
onClick?: MouseEventHandler<HTMLButtonElement>; // 具体类型
ref?: React.Ref<HTMLButtonElement>; // 正确的ref类型
variant?: 'primary' | 'secondary'; // 精确类型
}
function Button({ ref, variant = 'primary', ...props }: ButtonProps) {
return <button ref={ref} className={`btn-${variant}`} {...props} />;
}
// 泛型组件的类型
interface ListProps<T> {
items: T[];
renderItem: (item: T, index: number) => ReactNode;
ref?: React.Ref<HTMLUListElement>;
}
function List<T>({ items, renderItem, ref }: ListProps<T>) {
return (
<ul ref={ref}>
{items.map((item, index) => (
<li key={index}>{renderItem(item, index)}</li>
))}
</ul>
);
}
// 使用ComponentPropsWithRef
type DivProps = ComponentPropsWithRef<'div'>;
type ButtonProps = ComponentPropsWithRef<'button'>;
type CustomComponentProps = ComponentPropsWithRef<typeof CustomComponent>;
// 新Hook的类型
import { use, useActionState, useOptimistic } from 'react';
// use() Hook类型
function DataComponent() {
const dataPromise: Promise<{ id: number; name: string }> = fetchData();
const data = use(dataPromise); // 类型自动推断
return <div>{data.name}</div>;
}
// useActionState类型
type LoginState = {
error?: string;
success?: boolean;
};
function LoginForm() {
const [state, formAction, isPending] = useActionState<LoginState, FormData>(
async (prevState, formData) => {
const email = formData.get('email') as string;
const password = formData.get('password') as string;
try {
await login(email, password);
return { success: true };
} catch (error) {
return { error: (error as Error).message };
}
},
{ success: false }
);
return <form action={formAction}>...</form>;
}
// useOptimistic类型
interface Todo {
id: number;
text: string;
completed: boolean;
}
function TodoList() {
const [todos, setTodos] = useState<Todo[]>([]);
const [optimisticTodos, addOptimisticTodo] = useOptimistic<Todo[], Todo>(
todos,
(state, newTodo) => [...state, newTodo]
);
return <div>...</div>;
}第八部分:自动化迁移工具
8.1 React Codemod工具
bash
# 安装codemod
npm install -g @react-codemod/cli
# 自动迁移createRoot
npx @react-codemod/cli react-19/create-root src/
# 自动迁移ref
npx @react-codemod/cli react-19/remove-forwardref src/
# 自动迁移Context
npx @react-codemod/cli react-19/context-provider src/
# 批量运行所有迁移
npx @react-codemod/cli react-19/all src/8.2 自定义迁移脚本
javascript
// custom-migration.js
const fs = require('fs');
const { parse } = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const generate = require('@babel/generator').default;
const t = require('@babel/types');
// 迁移ReactDOM.render到createRoot
function migrateToCreateRoot(code) {
const ast = parse(code, {
sourceType: 'module',
plugins: ['jsx', 'typescript']
});
let needsCreateRoot = false;
traverse(ast, {
CallExpression(path) {
// 查找ReactDOM.render
if (
path.node.callee.type === 'MemberExpression' &&
path.node.callee.object.name === 'ReactDOM' &&
path.node.callee.property.name === 'render'
) {
needsCreateRoot = true;
const [element, container] = path.node.arguments;
// 替换为createRoot(container).render(element)
const createRootCall = t.callExpression(
t.identifier('createRoot'),
[container]
);
const renderCall = t.callExpression(
t.memberExpression(createRootCall, t.identifier('render')),
[element]
);
path.replaceWith(renderCall);
}
},
ImportDeclaration(path) {
// 更新import
if (path.node.source.value === 'react-dom') {
if (needsCreateRoot) {
path.node.source.value = 'react-dom/client';
const createRootImport = t.importSpecifier(
t.identifier('createRoot'),
t.identifier('createRoot')
);
path.node.specifiers.push(createRootImport);
}
}
}
});
return generate(ast).code;
}
// 运行迁移
const files = fs.readdirSync('./src').filter(f => f.endsWith('.jsx') || f.endsWith('.tsx'));
files.forEach(file => {
const filePath = `./src/${file}`;
const code = fs.readFileSync(filePath, 'utf-8');
const migratedCode = migrateToCreateRoot(code);
fs.writeFileSync(filePath, migratedCode);
console.log(`✓ Migrated ${file}`);
});8.3 验证工具
javascript
// validate-migration.js
const fs = require('fs');
const path = require('path');
const issues = [];
function validateFile(filePath) {
const content = fs.readFileSync(filePath, 'utf-8');
// 检查是否还在使用ReactDOM.render
if (content.includes('ReactDOM.render')) {
issues.push({
file: filePath,
issue: 'Still using ReactDOM.render',
line: findLineNumber(content, 'ReactDOM.render')
});
}
// 检查是否还在使用ReactDOM.hydrate
if (content.includes('ReactDOM.hydrate')) {
issues.push({
file: filePath,
issue: 'Still using ReactDOM.hydrate',
line: findLineNumber(content, 'ReactDOM.hydrate')
});
}
// 检查是否还在使用defaultProps
if (content.match(/\w+\.defaultProps\s*=/)) {
issues.push({
file: filePath,
issue: 'Still using defaultProps on function component',
line: findLineNumber(content, '.defaultProps')
});
}
// 检查是否还在使用Legacy Context
if (content.includes('getChildContext') || content.includes('childContextTypes')) {
issues.push({
file: filePath,
issue: 'Still using Legacy Context API',
line: findLineNumber(content, 'getChildContext')
});
}
}
function findLineNumber(content, search) {
const lines = content.split('\n');
return lines.findIndex(line => line.includes(search)) + 1;
}
// 递归检查所有文件
function walkDir(dir) {
const files = fs.readdirSync(dir);
files.forEach(file => {
const filePath = path.join(dir, file);
const stat = fs.statSync(filePath);
if (stat.isDirectory()) {
walkDir(filePath);
} else if (file.endsWith('.jsx') || file.endsWith('.tsx') || file.endsWith('.js') || file.endsWith('.ts')) {
validateFile(filePath);
}
});
}
walkDir('./src');
if (issues.length > 0) {
console.log('\n❌ Migration Issues Found:\n');
issues.forEach(issue => {
console.log(`File: ${issue.file}`);
console.log(`Line: ${issue.line}`);
console.log(`Issue: ${issue.issue}`);
console.log('---');
});
process.exit(1);
} else {
console.log('✅ All files migrated successfully!');
}注意事项
1. 渐进式迁移
不要一次性修改所有破坏性变更:
阶段1:关键API
- createRoot/hydrateRoot
- 确保应用运行
阶段2:类型更新
- TypeScript类型
- 编译通过
阶段3:代码优化
- 移除forwardRef
- 简化Context
- 添加ref清理
阶段4:测试验证
- 全面测试
- 性能测试2. 保持兼容性
jsx
// 创建兼容层
function createRootCompat(container) {
if (typeof createRoot !== 'undefined') {
// React 19
return createRoot(container);
} else {
// React 18
return {
render: (element) => ReactDOM.render(element, container),
unmount: () => ReactDOM.unmountComponentAtNode(container)
};
}
}3. 监控和回滚
javascript
// 监控破坏性变更影响
function monitorBreakingChanges() {
// 监控错误
window.addEventListener('error', (event) => {
if (event.message.includes('createRoot')) {
sendAlert('createRoot API error', event);
}
});
// 监控性能
if (performance.getEntriesByType('mark').length === 0) {
console.warn('Performance marks missing');
}
}4. 团队协作
迁移时的团队协作建议:
✅ 指定迁移负责人
✅ 创建迁移文档
✅ 进行代码审查
✅ 定期同步进度
✅ 记录遇到的问题
✅ 分享解决方案5. 回滚准备
bash
# 准备回滚方案
# 1. 保留React 18构建
npm run build:react18
# 2. 创建回滚脚本
cat > rollback.sh << 'EOF'
#!/bin/bash
echo "Rolling back to React 18..."
git checkout backup/pre-react-19
npm install
npm run build
npm run deploy
echo "Rollback complete"
EOF
chmod +x rollback.sh
# 3. 测试回滚流程
./rollback.sh --dry-run常见问题
Q1: 必须立即修复所有破坏性变更吗?
A: 不需要立即全部修复,建议分阶段迁移:
优先级排序:
P0 - 必须立即修复:
✅ createRoot/hydrateRoot(应用无法启动)
✅ 移除的API(会直接报错)
P1 - 尽快修复:
✅ TypeScript类型错误(影响开发)
✅ 弃用警告(未来会移除)
P2 - 逐步优化:
✅ forwardRef移除(性能优化)
✅ Context简化(代码清晰)
✅ ref清理函数(内存优化)
P3 - 可选优化:
✅ 代码风格统一
✅ 最佳实践应用实际示例:
javascript
// 阶段1:先让应用跑起来
// index.js
import { createRoot } from 'react-dom/client';
createRoot(document.getElementById('root')).render(<App />);
// 阶段2:修复TypeScript错误
// 逐个组件修复类型
// 阶段3:代码优化
// 批量移除forwardRef,简化Context等Q2: 如何处理第三方库的破坏性变更兼容问题?
A: 第三方库兼容问题的解决方案:
javascript
// 方法1:等待库更新
// 检查库的React 19兼容性
npm info react-select peerDependencies
// 方法2:使用版本覆盖(谨慎使用)
// package.json
{
"overrides": {
"react": "^19.0.0",
"react-dom": "^19.0.0"
}
}
// 方法3:创建适配器
// 为不兼容的库创建wrapper
import { forwardRef } from 'react';
import OldLibComponent from 'old-lib';
// 如果库还在使用forwardRef但React 19已移除支持
const AdaptedComponent = forwardRef((props, ref) => {
return <OldLibComponent {...props} innerRef={ref} />;
});
// 方法4:Fork并修复
// 如果库长期不维护
git clone https://github.com/author/old-lib
# 修复兼容性
# 发布为scoped package
npm publish @yourorg/old-lib-fixed
// 方法5:寻找替代库
// 例如从react-router v5迁移到v6
import { BrowserRouter, Routes, Route } from 'react-router-dom';
// 第三方库兼容性检查清单
const checkList = {
routing: 'react-router-dom@6+',
forms: 'react-hook-form@7+',
state: 'redux@5+ / zustand@4+',
ui: 'material-ui@5+ / antd@5+',
animation: 'framer-motion@11+',
charts: 'recharts@2.12+ / visx@3+'
};Q3: StrictMode在React 19中为何更严格?如何应对?
A: React 19的StrictMode会更积极地发现问题:
jsx
// 问题1:渲染中的副作用
// ❌ 会被StrictMode检测到
function BadComponent({ id }) {
// 直接在渲染中发起请求
fetch(`/api/user/${id}`).then(setUser);
return <div>{user?.name}</div>;
}
// ✅ 正确做法
function GoodComponent({ id }) {
const [user, setUser] = useState(null);
useEffect(() => {
const controller = new AbortController();
fetch(`/api/user/${id}`, { signal: controller.signal })
.then(res => res.json())
.then(setUser);
return () => controller.abort();
}, [id]);
return <div>{user?.name}</div>;
}
// 或使用React 19的use() Hook
function ModernComponent({ id }) {
const userPromise = useMemo(
() => fetch(`/api/user/${id}`).then(r => r.json()),
[id]
);
const user = use(userPromise);
return <div>{user.name}</div>;
}
// 问题2:不纯的渲染
// ❌ 会被检测
let renderCount = 0;
function BadCounter() {
renderCount++; // 修改外部变量
return <div>Rendered {renderCount} times</div>;
}
// ✅ 正确做法
function GoodCounter() {
const [count, setCount] = useState(0);
useEffect(() => {
setCount(c => c + 1);
}, []);
return <div>Rendered {count} times</div>;
}
// 问题3:不正确的依赖
// ❌ 会被检测
function BadEffect() {
const obj = { id: 1 }; // 每次渲染创建新对象
useEffect(() => {
fetchData(obj.id);
}, [obj]); // obj每次都不同,导致无限循环
}
// ✅ 正确做法
function GoodEffect() {
const id = 1;
useEffect(() => {
fetchData(id);
}, [id]); // 使用原始值
}
// StrictMode调试技巧
if (process.env.NODE_ENV === 'development') {
// 临时禁用StrictMode进行调试
// 但最终必须修复问题
root.render(<App />); // 而不是 <StrictMode><App /></StrictMode>
}Q4: 如何验证迁移是否成功?
A: 完整的验证流程:
javascript
// 1. 自动化测试验证
// migration-tests.spec.js
describe('React 19 Migration', () => {
it('should use createRoot instead of ReactDOM.render', () => {
const code = fs.readFileSync('src/index.js', 'utf-8');
expect(code).not.toContain('ReactDOM.render');
expect(code).toContain('createRoot');
});
it('should not use defaultProps on function components', () => {
const files = glob.sync('src/**/*.{js,jsx,ts,tsx}');
files.forEach(file => {
const code = fs.readFileSync(file, 'utf-8');
const hasFunctionComponent = /^function \w+\(/.test(code);
const hasDefaultProps = /\w+\.defaultProps\s*=/.test(code);
if (hasFunctionComponent && hasDefaultProps) {
throw new Error(`${file} has defaultProps on function component`);
}
});
});
it('should not use Legacy Context', () => {
const code = findInFiles(['getChildContext', 'childContextTypes']);
expect(code).toHaveLength(0);
});
});
// 2. 运行时验证
// runtime-validator.js
function validateReact19() {
const checks = [];
// 检查React版本
if (!React.version.startsWith('19')) {
checks.push({
level: 'error',
message: `Wrong React version: ${React.version}`
});
}
// 检查是否使用了createRoot
if (typeof window.__REACT_ROOT__ === 'undefined') {
checks.push({
level: 'error',
message: 'App not rendered with createRoot'
});
}
// 检查是否有hydration错误
window.addEventListener('error', (event) => {
if (event.message.includes('Hydration')) {
checks.push({
level: 'error',
message: 'Hydration mismatch detected',
details: event.message
});
}
});
return checks;
}
// 在应用启动时运行
if (process.env.NODE_ENV === 'development') {
setTimeout(() => {
const issues = validateReact19();
if (issues.length > 0) {
console.error('Migration issues:', issues);
} else {
console.log('✅ React 19 migration successful');
}
}, 2000);
}
// 3. 性能验证
// performance-validator.js
function comparePerformance() {
// 收集性能指标
const metrics = {
fcp: performance.getEntriesByName('first-contentful-paint')[0]?.startTime,
lcp: performance.getEntriesByType('largest-contentful-paint')[0]?.startTime,
fid: performance.getEntriesByType('first-input')[0]?.processingStart,
cls: getCLS(),
ttfb: performance.getEntriesByType('navigation')[0]?.responseStart
};
// 与React 18基线比较
const baseline = getBaselineMetrics();
Object.keys(metrics).forEach(key => {
const current = metrics[key];
const base = baseline[key];
const change = ((current - base) / base) * 100;
console.log(`${key}: ${current.toFixed(2)}ms (${change > 0 ? '+' : ''}${change.toFixed(1)}%)`);
});
}
// 4. E2E测试验证
// e2e/migration.spec.js
test('应用在React 19下正常运行', async ({ page }) => {
await page.goto('/');
// 检查是否有错误
const errors = [];
page.on('console', msg => {
if (msg.type() === 'error') {
errors.push(msg.text());
}
});
// 执行关键用户流程
await page.click('[data-testid="login-button"]');
await page.fill('input[name="email"]', 'test@example.com');
await page.fill('input[name="password"]', 'password');
await page.click('[type="submit"]');
await page.waitForSelector('[data-testid="dashboard"]');
// 验证没有错误
expect(errors).toHaveLength(0);
});
// 5. 视觉回归测试
// visual-regression.spec.js
test('UI在React 19下保持一致', async ({ page }) => {
await page.goto('/');
// 截图对比
const screenshot = await page.screenshot();
const diff = await compareWithBaseline(screenshot, 'homepage-react-18.png');
expect(diff.percentage).toBeLessThan(0.1); // 小于0.1%差异
});Q5: 如何处理SSR中的Breaking Changes?
A: SSR特定的迁移策略:
javascript
// 完整的SSR迁移方案
// 1. 服务器端代码迁移
// server-react-19.js
import { renderToPipeableStream } from 'react-dom/server';
import { ServerRouter } from './router';
app.get('*', async (req, res) => {
// 预加载数据(可选)
const initialData = await fetchInitialData(req.url);
let didError = false;
const { pipe, abort } = renderToPipeableStream(
<ServerRouter url={req.url} initialData={initialData} />,
{
bootstrapScripts: ['/client.js'],
bootstrapScriptContent: `window.__INITIAL_DATA__=${JSON.stringify(initialData)}`,
onShellReady() {
res.statusCode = didError ? 500 : 200;
res.setHeader('Content-Type', 'text/html');
pipe(res);
},
onShellError(error) {
res.statusCode = 500;
res.send('<!DOCTYPE html><p>Loading...</p>');
},
onError(error) {
didError = true;
console.error('SSR error:', error);
// 上报错误
logError(error);
}
}
);
// 设置超时
setTimeout(() => {
abort();
}, 10000);
});
// 2. 客户端Hydration迁移
// client-react-19.js
import { hydrateRoot } from 'react-dom/client';
import { BrowserRouter } from './router';
const initialData = window.__INITIAL_DATA__;
const container = document.getElementById('root');
// 错误边界
class HydrationErrorBoundary extends React.Component {
state = { hasError: false };
static getDerivedStateFromError() {
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
console.error('Hydration error:', error, errorInfo);
// 回退到客户端渲染
import('./client-render').then(({ render }) => {
render(container, initialData);
});
}
render() {
if (this.state.hasError) {
return <div>Loading...</div>;
}
return this.props.children;
}
}
// Hydrate with error handling
hydrateRoot(
container,
<HydrationErrorBoundary>
<BrowserRouter initialData={initialData} />
</HydrationErrorBoundary>,
{
onRecoverableError: (error, errorInfo) => {
console.error('Recoverable hydration error:', error);
logError({
type: 'hydration',
error: error.message,
componentStack: errorInfo.componentStack
});
}
}
);
// 3. Hydration Mismatch调试
// hydration-debugger.js
function debugHydrationMismatch() {
// 收集所有hydration错误
const errors = [];
const originalError = console.error;
console.error = (...args) => {
const message = args.join(' ');
if (message.includes('Hydration')) {
errors.push({
message,
stack: new Error().stack,
timestamp: Date.now()
});
// 提取组件信息
const componentMatch = message.match(/in (\w+)/);
if (componentMatch) {
console.warn(`Hydration mismatch in component: ${componentMatch[1]}`);
}
}
originalError.apply(console, args);
};
// 页面加载完成后报告
window.addEventListener('load', () => {
if (errors.length > 0) {
console.table(errors);
// 发送到监控
fetch('/api/log-hydration-errors', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ errors, url: location.href })
});
}
});
}
if (process.env.NODE_ENV === 'development') {
debugHydrationMismatch();
}
// 4. 流式SSR with Suspense
// streaming-ssr.js
import { Suspense } from 'react';
function App() {
return (
<html>
<head>
<title>My App</title>
</head>
<body>
<div id="root">
{/* 立即发送的Shell */}
<Header />
<Nav />
{/* 延迟加载的内容 */}
<Suspense fallback={<Spinner />}>
<Comments />
</Suspense>
<Suspense fallback={<Spinner />}>
<Recommendations />
</Suspense>
<Footer />
</div>
</body>
</html>
);
}
// 服务器配置
app.get('*', (req, res) => {
const { pipe } = renderToPipeableStream(<App />, {
bootstrapScripts: ['/client.js'],
onShellReady() {
// Shell准备好就发送(不等待Suspense)
res.setHeader('Content-Type', 'text/html');
pipe(res);
}
});
});Q6: TypeScript类型迁移遇到问题怎么办?
A: TypeScript迁移常见问题和解决方案:
typescript
// 问题1:ref类型错误
// ❌ React 18
import { forwardRef, Ref } from 'react';
interface Props {
value: string;
}
const Input = forwardRef<HTMLInputElement, Props>((props, ref) => {
return <input ref={ref} {...props} />;
});
// ✅ React 19
interface Props {
value: string;
ref?: React.Ref<HTMLInputElement>;
}
function Input({ value, ref }: Props) {
return <input ref={ref} value={value} />;
}
// 或使用ComponentPropsWithRef
import { ComponentPropsWithRef } from 'react';
type InputProps = ComponentPropsWithRef<'input'> & {
customProp?: string;
};
function Input(props: InputProps) {
return <input {...props} />;
}
// 问题2:Context类型错误
// ❌ React 18
const MyContext = createContext<ContextType>(defaultValue);
function Provider({ children }: { children: ReactNode }) {
return (
<MyContext.Provider value={value}>
{children}
</MyContext.Provider>
);
}
// ✅ React 19(简化Provider)
const MyContext = createContext<ContextType>(defaultValue);
function Provider({ children }: { children: ReactNode }) {
return (
<MyContext value={value}>
{children}
</MyContext>
);
}
// 问题3:新Hook的类型
import { use, useActionState, useOptimistic } from 'react';
// use() Hook
function Component() {
// 需要明确Promise类型
const promise: Promise<User> = fetchUser();
const user = use(promise);
return <div>{user.name}</div>;
}
// useActionState类型
type State = {
data?: string;
error?: string;
};
function Form() {
const [state, action] = useActionState<State, FormData>(
async (prevState, formData) => {
try {
const data = await submit(formData);
return { data };
} catch (error) {
return { error: (error as Error).message };
}
},
{ data: undefined, error: undefined }
);
}
// useOptimistic类型
interface Todo {
id: string;
text: string;
done: boolean;
}
function TodoList() {
const [todos] = useState<Todo[]>([]);
const [optimisticTodos, addOptimistic] = useOptimistic<Todo[], Todo>(
todos,
(state, newTodo) => [...state, newTodo]
);
}
// 问题4:事件处理器类型
// ❌ 宽松的类型
interface ButtonProps {
onClick?: Function;
}
// ✅ 具体的事件类型
import { MouseEventHandler, FormEventHandler } from 'react';
interface ButtonProps {
onClick?: MouseEventHandler<HTMLButtonElement>;
}
interface FormProps {
onSubmit?: FormEventHandler<HTMLFormElement>;
}
// 类型迁移辅助工具
// migrate-types.ts
import type {
ComponentPropsWithRef,
ComponentPropsWithoutRef,
ForwardedRef,
PropsWithChildren,
PropsWithoutRef
} from 'react';
// 旧代码中的Ref类型
export type LegacyRef<T> = ForwardedRef<T>;
// 新代码推荐的类型
export type NewProps<T extends keyof JSX.IntrinsicElements> =
ComponentPropsWithRef<T>;
// 迁移辅助函数
export function migrateComponentProps<T extends keyof JSX.IntrinsicElements>(
element: T
): ComponentPropsWithRef<T> {
return {} as ComponentPropsWithRef<T>;
}Q7: 如何处理性能回归问题?
A: 性能问题诊断和优化流程:
javascript
// 1. 性能诊断
// performance-diagnosis.js
class PerformanceDiagnostics {
constructor() {
this.metrics = new Map();
this.setupMonitoring();
}
setupMonitoring() {
// Web Vitals监控
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
getCLS(this.recordMetric.bind(this, 'CLS'));
getFID(this.recordMetric.bind(this, 'FID'));
getFCP(this.recordMetric.bind(this, 'FCP'));
getLCP(this.recordMetric.bind(this, 'LCP'));
getTTFB(this.recordMetric.bind(this, 'TTFB'));
});
// React组件性能
this.setupReactProfiler();
}
setupReactProfiler() {
// 使用React Profiler API
const onRenderCallback = (
id, phase, actualDuration, baseDuration, startTime, commitTime
) => {
this.metrics.set(`${id}-${phase}`, {
actualDuration,
baseDuration,
renderTime: commitTime - startTime
});
// 检测慢渲染
if (actualDuration > 100) {
console.warn(`Slow render in ${id}: ${actualDuration}ms`);
// 上报性能问题
this.reportSlowRender({
component: id,
phase,
duration: actualDuration
});
}
};
return onRenderCallback;
}
recordMetric(name, metric) {
this.metrics.set(name, metric.value);
// 与基线比较
const baseline = this.getBaseline(name);
if (baseline && metric.value > baseline * 1.2) {
console.error(`${name} regression: ${metric.value}ms vs ${baseline}ms baseline`);
this.reportRegression(name, metric.value, baseline);
}
}
async reportRegression(metric, current, baseline) {
await fetch('/api/performance-regression', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
metric,
current,
baseline,
url: location.href,
userAgent: navigator.userAgent,
timestamp: Date.now()
})
});
}
}
// 使用Profiler组件
function App() {
return (
<Profiler id="App" onRender={onRenderCallback}>
<Router>
<Profiler id="MainContent" onRender={onRenderCallback}>
<MainContent />
</Profiler>
</Router>
</Profiler>
);
}
// 2. 常见性能问题修复
// React 19迁移后的性能优化
// 问题1:过度重渲染
// ❌ 导致性能问题
function ParentComponent() {
const [count, setCount] = useState(0);
// 每次渲染都创建新对象
const config = { value: count };
return <ChildComponent config={config} />;
}
// ✅ 使用memo和useMemo
const ChildComponent = memo(function ChildComponent({ config }) {
return <div>{config.value}</div>;
});
function ParentComponent() {
const [count, setCount] = useState(0);
const config = useMemo(() => ({ value: count }), [count]);
return <ChildComponent config={config} />;
}
// 或利用React 19 Compiler自动优化
// 无需手动memo和useMemo
function ParentComponent() {
const [count, setCount] = useState(0);
const config = { value: count };
return <ChildComponent config={config} />;
}
// 问题2:大列表性能
// ❌ 渲染大量项目
function List({ items }) {
return (
<div>
{items.map(item => <Item key={item.id} data={item} />)}
</div>
);
}
// ✅ 使用虚拟滚动
import { FixedSizeList } from 'react-window';
function List({ items }) {
return (
<FixedSizeList
height={500}
itemCount={items.length}
itemSize={50}
width="100%"
>
{({ index, style }) => (
<div style={style}>
<Item data={items[index]} />
</div>
)}
</FixedSizeList>
);
}
// 问题3:不必要的Effect
// ❌ 会导致额外渲染
function Component({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
fetchUser(userId).then(setUser);
}, [userId]);
const greeting = user ? `Hello ${user.name}` : 'Loading...';
return <div>{greeting}</div>;
}
// ✅ 使用React 19的use() Hook
function Component({ userId }) {
const userPromise = useMemo(
() => fetchUser(userId),
[userId]
);
const user = use(userPromise);
return <div>Hello {user.name}</div>;
}
// 3. 性能监控和报警
// monitoring.js
class PerformanceMonitoring {
constructor() {
this.thresholds = {
FCP: 1800, // First Contentful Paint
LCP: 2500, // Largest Contentful Paint
FID: 100, // First Input Delay
CLS: 0.1, // Cumulative Layout Shift
TTI: 3800 // Time to Interactive
};
}
async monitor() {
const metrics = await this.collectMetrics();
Object.entries(metrics).forEach(([name, value]) => {
const threshold = this.thresholds[name];
if (value > threshold) {
this.alert({
level: 'warning',
metric: name,
value,
threshold,
message: `${name} exceeded threshold: ${value} > ${threshold}`
});
}
});
}
async collectMetrics() {
return new Promise(resolve => {
new PerformanceObserver((entryList) => {
const entries = entryList.getEntries();
const metrics = {};
entries.forEach(entry => {
metrics[entry.name] = entry.value || entry.startTime;
});
resolve(metrics);
}).observe({ entryTypes: ['paint', 'navigation', 'measure'] });
});
}
alert(data) {
// 发送警报
console.error('Performance Alert:', data);
fetch('/api/performance-alert', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
}
}
// 4. A/B测试性能对比
// ab-testing.js
function setupPerformanceABTest() {
const variant = Math.random() < 0.5 ? 'react-19' : 'react-18';
// 记录variant
sessionStorage.setItem('ab-variant', variant);
// 收集性能数据
window.addEventListener('load', () => {
const metrics = {
variant,
fcp: performance.getEntriesByName('first-contentful-paint')[0]?.startTime,
lcp: performance.getEntriesByType('largest-contentful-paint')[0]?.startTime,
// ... 其他指标
};
// 上报AB测试数据
fetch('/api/ab-performance', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(metrics)
});
});
}Q8: 迁移后出现Hydration错误怎么办?
A: Hydration错误的调试和修复:
javascript
// 1. 定位Hydration错误
// hydration-error-detector.js
function detectHydrationErrors() {
// 捕获hydration错误
const errors = [];
const originalError = console.error;
console.error = function(...args) {
const message = args.join(' ');
if (message.includes('Hydration') || message.includes('did not match')) {
errors.push({
message,
stack: new Error().stack,
timestamp: Date.now()
});
// 提取组件信息
const componentMatch = message.match(/Text content did not match.*?<(.*?)>/);
if (componentMatch) {
console.warn('Hydration mismatch in:', componentMatch[1]);
}
}
originalError.apply(console, args);
};
return errors;
}
// 2. 常见Hydration问题修复
// 问题1:服务器和客户端生成不同内容
// ❌ 会导致mismatch
function Component() {
return <div>{new Date().toLocaleString()}</div>;
}
// ✅ 延迟到客户端
function Component() {
const [time, setTime] = useState(null);
useEffect(() => {
setTime(new Date().toLocaleString());
}, []);
return <div>{time || 'Loading...'}</div>;
}
// 或使用客户端标记
function Component() {
const [isClient, setIsClient] = useState(false);
useEffect(() => {
setIsClient(true);
}, []);
return <div>{isClient ? new Date().toLocaleString() : null}</div>;
}
// 问题2:条件渲染差异
// ❌ SSR和CSR逻辑不一致
function Component() {
const isMobile = window.innerWidth < 768;
return isMobile ? <MobileView /> : <DesktopView />;
}
// ✅ 使用useEffect确保一致性
function Component() {
const [isMobile, setIsMobile] = useState(false);
useEffect(() => {
const checkMobile = () => {
setIsMobile(window.innerWidth < 768);
};
checkMobile();
window.addEventListener('resize', checkMobile);
return () => window.removeEventListener('resize', checkMobile);
}, []);
// SSR时渲染通用视图
if (typeof window === 'undefined') {
return <UniversalView />;
}
return isMobile ? <MobileView /> : <DesktopView />;
}
// 问题3:第三方库DOM操作
// ❌ 库直接修改DOM
function Component() {
const ref = useRef();
useEffect(() => {
// 第三方库修改DOM
$(ref.current).somePlugin();
}, []);
return <div ref={ref}>Content</div>;
}
// ✅ 使用suppressHydrationWarning
function Component() {
const ref = useRef();
useEffect(() => {
$(ref.current).somePlugin();
}, []);
return <div ref={ref} suppressHydrationWarning>Content</div>;
}
// 3. Hydration错误监控
// hydration-monitor.js
class HydrationMonitor {
constructor() {
this.errors = [];
this.setupErrorBoundary();
}
setupErrorBoundary() {
if (typeof window === 'undefined') return;
window.addEventListener('error', (event) => {
if (event.message.includes('Hydration')) {
this.recordError({
type: 'hydration',
message: event.message,
url: location.href,
timestamp: Date.now()
});
}
});
}
recordError(error) {
this.errors.push(error);
// 达到阈值时报警
if (this.errors.length > 5) {
this.alert('High hydration error rate detected');
}
// 上报
fetch('/api/hydration-errors', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(error)
});
}
alert(message) {
console.error('[Hydration Monitor]', message);
// 发送通知
}
}
const monitor = new HydrationMonitor();Q9: 如何确保团队顺利迁移到React 19?
A: 团队迁移最佳实践:
团队迁移策略:
1. 准备阶段(1-2周)
✅ 评估现有代码库
✅ 识别潜在问题
✅ 创建迁移计划
✅ 准备培训材料
2. 培训阶段(1周)
✅ 举办React 19技术分享
✅ 讲解破坏性变更
✅ 演示迁移示例
✅ Q&A答疑
3. 试点阶段(2-3周)
✅ 选择1-2个小项目试点
✅ 记录遇到的问题
✅ 总结最佳实践
✅ 更新迁移文档
4. 推广阶段(4-8周)
✅ 团队分批迁移
✅ 代码审查把关
✅ 持续监控问题
✅ 定期同步进度
5. 完成阶段(1-2周)
✅ 清理旧代码
✅ 更新文档
✅ 性能优化
✅ 总结经验
团队协作工具:
# 迁移checklist
- [ ] 更新package.json依赖
- [ ] 运行codemod自动迁移
- [ ] 修复TypeScript错误
- [ ] 更新测试
- [ ] 本地测试通过
- [ ] 代码审查
- [ ] 部署到staging
- [ ] E2E测试
- [ ] 性能测试
- [ ] 生产部署
- [ ] 监控观察
沟通渠道:
✅ 创建#react-19-migration Slack频道
✅ 每周迁移进度会议
✅ 共享文档记录问题和解决方案
✅ 配对编程解决难题Q10: 迁移过程中应该注意哪些安全问题?
A: 迁移安全注意事项:
javascript
// 1. 依赖安全检查
// 检查新版本的安全漏洞
npm audit
// 修复漏洞
npm audit fix
// 2. XSS防护
// React 19仍然会自动转义,但要注意:
function Component({ userInput }) {
// ❌ 危险:dangerouslySetInnerHTML
return <div dangerouslySetInnerHTML={{ __html: userInput }} />;
// ✅ 安全:自动转义
return <div>{userInput}</div>;
// ✅ 如果必须使用HTML,先sanitize
const sanitized = DOMPurify.sanitize(userInput);
return <div dangerouslySetInnerHTML={{ __html: sanitized }} />;
}
// 3. Server Actions安全
// server-actions.js
'use server';
import { verifyAuth } from './auth';
export async function updateUser(formData) {
// ❌ 没有验证用户身份
const userId = formData.get('userId');
await db.users.update(userId, data);
// ✅ 验证身份和权限
const session = await verifyAuth();
if (!session) throw new Error('Unauthorized');
const userId = formData.get('userId');
if (session.userId !== userId) {
throw new Error('Forbidden');
}
await db.users.update(userId, data);
}
// 4. 环境变量安全
// ❌ 暴露敏感信息
const API_KEY = process.env.REACT_APP_SECRET_API_KEY;
// ✅ 区分公开和私密变量
// 客户端可访问(以REACT_APP_开头)
const PUBLIC_API = process.env.REACT_APP_PUBLIC_API;
// 服务器端专用
const SECRET_KEY = process.env.SECRET_KEY; // 不会暴露到客户端
// 5. CSP策略更新
// 更新Content Security Policy以支持React 19
const helmet = require('helmet');
app.use(
helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"],
scriptSrc: [
"'self'",
// React 19可能使用inline scripts
"'unsafe-inline'", // 谨慎使用
// 或使用nonce
(req, res) => `'nonce-${res.locals.cspNonce}'`
],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", "data:", "https:"],
}
})
);总结
主要破坏性变更回顾
1. 核心API变更
已移除的API:
✅ ReactDOM.render() → createRoot().render()
✅ ReactDOM.hydrate() → hydrateRoot()
✅ ReactDOM.unmountComponentAtNode() → root.unmount()
✅ ReactDOM.renderToNodeStream() → renderToPipeableStream()
废弃的特性:
✅ 函数组件的defaultProps
✅ 函数组件的propTypes
✅ Legacy Context API (getChildContext)
✅ string refs2. 行为变更
ref处理:
✅ ref作为普通prop传递
✅ ref callback支持返回清理函数
✅ 不再需要forwardRef包装
Context简化:
✅ <Context value={value}> 替代 <Context.Provider value={value}>
✅ 向后兼容旧语法
useReducer:
✅ 初始化函数参数改变
✅ 更严格的类型检查
StrictMode:
✅ 更积极地检测副作用
✅ 开发环境双重调用组件
✅ 检测不纯的渲染逻辑3. TypeScript类型变更
ref类型:
✅ React.Ref<T> 替代 ForwardedRef<T>
✅ ComponentPropsWithRef<T> 简化类型定义
Hook类型:
✅ use() Hook的类型推断
✅ useActionState<State, Payload>
✅ useOptimistic<State, Action>
事件类型:
✅ 更严格的事件处理器类型
✅ 移除宽松的Function类型4. SSR变更
流式渲染:
✅ renderToPipeableStream(Node.js)
✅ renderToReadableStream(Edge)
✅ Suspense原生支持
Hydration:
✅ hydrateRoot API
✅ onRecoverableError回调
✅ Selective Hydration迁移策略总结
1. 准备阶段
评估工作:
✅ 分析代码库使用的弃用API
✅ 识别第三方库兼容性问题
✅ 评估迁移工作量
✅ 制定迁移时间表
准备工作:
✅ 创建备份分支
✅ 准备迁移工具和脚本
✅ 搭建测试环境
✅ 准备回滚方案2. 执行阶段
迁移步骤:
1️⃣ 更新依赖包到React 19
2️⃣ 运行自动化迁移工具(codemod)
3️⃣ 修复TypeScript类型错误
4️⃣ 更新入口文件(createRoot/hydrateRoot)
5️⃣ 逐个修复破坏性变更
6️⃣ 更新测试代码
7️⃣ 执行全面测试
8️⃣ 性能基准测试
9️⃣ 部署到staging环境
🔟 生产环境灰度发布
质量保证:
✅ 单元测试覆盖率 > 80%
✅ E2E测试关键流程
✅ 性能指标不低于基线
✅ 无严重bug
✅ 浏览器兼容性测试3. 验证阶段
功能验证:
✅ 所有功能正常运行
✅ 无Console错误和警告
✅ 用户流程完整可用
✅ 第三方集成正常
性能验证:
✅ FCP < 1.8s
✅ LCP < 2.5s
✅ FID < 100ms
✅ CLS < 0.1
✅ 无性能回归
监控验证:
✅ 错误监控正常
✅ 性能监控到位
✅ 用户行为分析
✅ 报警机制完善4. 优化阶段
代码优化:
✅ 移除forwardRef(利用新特性)
✅ 简化Context使用
✅ 添加ref清理函数
✅ 清理旧的兼容代码
性能优化:
✅ 启用React Compiler
✅ 优化资源加载(preload/preinit)
✅ 使用Server Components(如适用)
✅ 优化Bundle大小
文档更新:
✅ 更新技术文档
✅ 记录迁移经验
✅ 分享最佳实践
✅ 培训团队成员最佳实践建议
1. 渐进式迁移
javascript
// 不要一次性修改所有代码
// 建议的迁移顺序:
// 第一步:核心入口
createRoot(document.getElementById('root')).render(<App />);
// 第二步:关键路径组件
// 修复编译错误和类型错误
// 第三步:非关键组件
// 逐步优化和清理
// 第四步:删除旧代码
// 确认无问题后清理2. 充分测试
javascript
// 测试金字塔
// /\
// /E2E\ ← 少量关键流程测试
// /------\
// /集成测试\ ← 适量接口和组件测试
// /----------\
// / 单元测试 \ ← 大量单元测试
// /--------------\
// 自动化测试覆盖:
✅ 单元测试:组件逻辑、Hooks、工具函数
✅ 集成测试:组件交互、API集成
✅ E2E测试:关键用户流程
✅ 视觉回归:UI一致性
✅ 性能测试:加载速度、渲染性能3. 监控和回滚
javascript
// 完善的监控体系
const monitoring = {
错误监控: {
工具: 'Sentry / Bugsnag',
指标: ['错误率', '错误类型', '影响用户数']
},
性能监控: {
工具: 'Lighthouse / Web Vitals',
指标: ['FCP', 'LCP', 'FID', 'CLS', 'TTFB']
},
用户监控: {
工具: 'Google Analytics / Mixpanel',
指标: ['活跃用户', '留存率', '转化率']
},
业务监控: {
工具: '自定义Dashboard',
指标: ['关键业务指标', 'SLA达成率']
}
};
// 回滚预案
if (errorRate > threshold) {
// 立即回滚到React 18
deployPreviousVersion();
notifyTeam('React 19 rollback initiated');
}4. 团队协作
协作要点:
沟通:
✅ 定期同步会议
✅ 文档共享
✅ 问题追踪系统
✅ 技术分享会
分工:
✅ 指定迁移负责人
✅ 明确任务分配
✅ 设置里程碑
✅ 定期Review进度
知识传递:
✅ 编写迁移文档
✅ 录制教程视频
✅ 组织培训workshop
✅ 建立Q&A知识库常见陷阱和避免方法
陷阱1:一次性全面迁移
❌ 风险:
- 工作量大,难以掌控
- 问题集中爆发
- 难以定位问题根源
- 回滚代价高
✅ 避免方法:
- 分模块渐进迁移
- 小步快跑,持续集成
- 及时发现和解决问题
- 保持可回滚能力陷阱2:忽视第三方库兼容性
❌ 风险:
- 运行时错误
- 类型错误
- 功能异常
✅ 避免方法:
- 提前检查库的兼容性
- 寻找替代方案
- 创建适配层
- 联系维护者陷阱3:测试不充分
❌ 风险:
- 生产环境bug
- 用户体验受损
- 紧急回滚
✅ 避免方法:
- 制定测试checklist
- 自动化测试覆盖
- staging环境验证
- 灰度发布策略陷阱4:性能监控缺失
❌ 风险:
- 性能回归未察觉
- 用户投诉增加
- SEO受影响
✅ 避免方法:
- 建立性能基线
- 持续性能监控
- 设置性能预算
- 及时优化迁移成功标准
功能层面
✅ 所有功能正常运行
✅ 无阻塞性bug
✅ 用户体验无降级
✅ 第三方集成完好技术层面
✅ 无Console错误
✅ 无TypeScript错误
✅ 测试全部通过
✅ 代码质量提升性能层面
✅ 性能指标达标
✅ 无性能回归
✅ Bundle大小可控
✅ 加载速度优化团队层面
✅ 团队掌握新特性
✅ 文档完善
✅ 最佳实践建立
✅ 持续改进机制后续优化方向
1. 利用React 19新特性
javascript
// React Compiler自动优化
- 移除手动memo/useMemo/useCallback
- 享受自动memoization
// Server Components
- 减少客户端Bundle大小
- 改善首屏性能
- 优化数据获取
// 新Hooks
- use()简化异步处理
- useOptimistic改善UX
- useActionState简化表单
// 资源优化API
- preload/preinit预加载
- prefetchDNS/preconnect优化连接2. 持续性能优化
定期优化:
✅ 代码分割优化
✅ 图片懒加载
✅ 第三方库精简
✅ 缓存策略优化
✅ CDN配置优化
监控优化:
✅ Core Web Vitals
✅ 自定义性能指标
✅ 用户体验监控
✅ A/B测试3. 开发体验提升
工具链:
✅ 更快的构建速度
✅ 更好的HMR体验
✅ 智能代码提示
✅ 自动化工具
流程:
✅ CI/CD优化
✅ 自动化测试
✅ 性能监控集成
✅ 错误追踪关键要点
核心原则:
1️⃣ 稳妥第一 - 确保系统稳定性
2️⃣ 渐进迁移 - 小步快跑,持续优化
3️⃣ 充分测试 - 自动化测试保障
4️⃣ 实时监控 - 及时发现和解决问题
5️⃣ 团队协作 - 知识共享,共同进步
成功关键:
✅ 详细的迁移计划
✅ 完善的测试策略
✅ 可靠的回滚方案
✅ 持续的性能监控
✅ 团队的技术提升
长期收益:
📈 更好的性能表现
🎯 更佳的开发体验
🔒 更稳定的系统
💡 更现代的技术栈
🚀 更快的迭代速度通过系统化的迁移策略、严格的质量保证和持续的优化改进,可以安全、平稳地完成从React 18到React 19的升级,并充分享受新版本带来的性能提升和开发体验改善。
影响评估
✅ 代码修改量:中等
✅ 迁移难度:低到中等
✅ 收益:性能提升+新特性
✅ 风险:可控了解破坏性变更是成功迁移的关键!