React 18 新特性
2022年3月29日发布
import React from 'react'
// 18
// import ReactDOM from 'react-dom/client'
// 17
import './index.css'
import App from './App'
import reportWebVitals from './reportWebVitals'
import ReactDOM from 'react-dom'
// 装载
ReactDOM.render(<App />, document.getElementById('root'))
// 卸载
ReactDOM.unmountComponentAtNode(document.getElementById('root'))
平滑升级:如果在18中使用这个API,会有一个警告,表明已经被弃用并且换到新的 API,意味着如果直接把项目版本升级到18版本不会造成break change,在整个18版 本中都为可用兼容状态,并保持版本的特性。
// 18
import ReactDOM from 'react-dom/client'
const root = ReactDOM.createRoot(document.getElementById('root'))
// 装载
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
)
// 卸载
root.unmount()
目的:React组件生态很庞大,很多组件会用到ReactDOM.render直接渲染,比如UI库 中的Model.confirm类似的API,这时就需要一个版本的周期让这些生态组件升级上来。
useEffect(() => {
console.log('组件挂载了')
return () => {
console.log('组件卸载了')
}
}, [])
更新到18后,在开发环境下会挂载两次,避免这种行为的方法是去掉StrictMode标签,但也会把其他提示去掉。
root.render(
//
<App />
//
)
还可以用ref解决
const firstRenderRef = useRef(true)
useEffect(() => {
if (firstRenderRef.current) {
firstRenderRef.current = false
return
}
console.log('组件挂载了')
return () => {
console.log('组件卸载了')
}
}, [])
return <div></div>
在React 18以前,React 只能在组件的生命周期函数或者合成事件函数中进行批处理。默认情况下,Promise、setTimeout 以及原生事件中是不会对其进行批处理的。
function App() {
const [count, setCount] = useState(0)
const [flag, setFlag] = useState(false)
console.log('count = ' + count, 'flag = ' + flag)
function handleClickOne() {
setCount(1)
setFlag(true)
// 批处理:会合并为一次 render
}
function handleClickTwo() {
Promise.resolve().then(() => {
setCount(2)
})
setFlag(false)
// 17 会执行两次 render
// 18 合并为一次 render
}
return (
<div style={{ textAlign: 'center' }}>
<h3>React17</h3>
<button onClick={handleClickOne}>批处理</button>
<button onClick={handleClickTwo}>同步模式</button>
</div>
)
}
export default App
在React 18中,任何情况下都可以合并渲染,上面第二个例子只会有一次的render,因为所有的更新都将自动批处理。但在下面情况会执行两次render:
async function handleClickThree() {
await setCount(3)
setFlag(true)
// React 18:会执行两次 render
}
React 18的批处理是安全的,是框架层面自动处理,如果想要退出批处理,可以使用flushSync方法。
flushSync(fn: () => R): R 它接收一个函数作为参数,并且允许有返回值。
注:flushSync 具有函数为作用域,函数内部的多个 setState时 仍然为批量更新,这样可以精准控制哪些不需要的批量更新:
const App = () => {
const [count, setCount] = useState(0)
const [flag, setFlag] = useState(false)
const [loading, setLoading] = useState(true)
console.log('count = ' + count, 'flag = ' + flag, 'loading = ' + loading)
function handleClickFlush() {
flushSync(() => {
setCount(3)
setFlag(true)
})
// 在setCount 和 setFlag 为批量更新结束后执行
setLoading(false)
// 此方法会触发两次 render
}
return (
<div style={{ textAlign: 'center' }}>
<h3>React18</h3>
<button onClick={handleClickFlush}>flushSync</button>
</div>
)
}
export default App
Transition更新分为两类:
在React 18之前,所有的更新都没有优先级之分,都是紧急的,意味着两种状态更新会被同时render。有时用户希望第一个更新更加及时,第二个更新可以稍微延迟一会。
startTransition可以解决这个问题。
import React, { startTransition, useState } from 'react'
const [content, setContent] = useState('')
const [inputValue, setInputValue] = useState('')
console.log('inputValue = ' + inputValue, 'content = ' + content)
const onChange = (e) => {
// 紧急更新
setInputValue('紧急')
// 标记回调函数内的更新为 非紧急更新
startTransition(() => {
setContent('非紧急')
})
}
简单来说,被startTransition包裹的setState触发的渲染,被标记为不紧急渲染,这些渲染可能被其他紧急渲染所抢占。
我们可以使用 startTransition 包装任何要移至后台的更新,通常,这些类型的更新分为两类:
useTransition:用来降低渲染优先级,减少重复渲染次数。
例如:搜索引擎的关键词联想。一般来说用户在输入框输入都希望是实时更新,如果此时联想词比较多,也要实时更新的话,可能会导致用户输入卡顿,这样使用户的体验感变差。对于用户输入的更新和联想词更新,前者的紧急程度大于后者。
为此提供了一个带有 两个返回值的useTransition,第一个isPending是一个布尔值,这是 react通知我们是否正在等待过度的完成。第二个startTransition是一个接收回调的函数,用来告诉react需要推迟的state。React 将在状态转换期间提供视觉反馈,并在转换发生时保持浏览器响应。
import React, { useState, useTransition } from 'react'
const [value, setValue] = useState('')
const [searchQuery, setSearchQuery] = useState([])
const [loading, startTransition] = useTransition(2000)
const handleChange = (e) => {
setValue(e.target.value)
// 延迟更新
startTransition(() => {
setSearchQuery(Array(20000).fill(e.target.value))
})
return (
<div style={{ textAlign: 'center' }}>
<h3>React18</h3>
<input value={value} onChange={handleChange} />
{loading ? (
<p>loading...</p>
) : (
searchQuery.map((item,index) => <p key={index}>{item}</p>
)}
</div>
)
}
useDeferredValue:支持变量延迟更新,同时接受一个可选的延迟更新的最大值。React尝试尽快更新延迟值,如果在给定的timeoutMs期限内未能完成,将强制更新。
优点:能很好地展现并发渲染时优先级调整的特性,可以用于延迟计算逻辑比较复杂的状态,让其他组件优先渲染,等待这个状态更新完后再更新。
import { useState, useDeferredValue } from 'react'
function App() {
const [text, setText] = useState('111')
const deferredText = useDeferredValue(text, {
timeoutMs: 5000
})
let arr = []
for (let i = 0; i < 30000; i++) {
arr.push(i)
}
function handleChange(e) {
setText(e.target.value)
}
return (
<div style={{ textAlign: 'center' }}>
<h3>React18</h3>
<input value={text} onChange={handleChange} />
<br />
{deferredText}
<ul>
{arr.map((item) => (
<li key={item}>{deferredText}</li>
))}
</ul>
</div>
)
}
export default App
useDeferredValue 与 useTransition 相同和不同:
相同:useDeferredValue 本质上的内部实现与 useTransition 一样,都是标记成延迟更新任务。
不同:useTransition 是把更新任务变成了延迟更新任务,而 useDeferredValue 是产生一个新的值,这个值作为延时状态。(一个用来包装方法,一个用来包装值)
我们可以使用 React 进行服务端渲染(SSR),在开发模式上,我们可以在客户端与服务端共享同一个 React 组件,但服务端渲染必须保证客户端与服务端生成的HTML结构相匹配,如Math.random()在SSR 面前是没法保证客户端与服务端之间的 id 唯一性。
const id = Math.random();
export default function App() {
return <div id={id}>Hello</div>
}
如果应用是CSR(客户端渲染),id是稳定的,App组件没有问题。
如果应用是SSR(服务端渲染),那么会经历:
Hello
作为HTML内容传递给客户端function Checkbox() {
// 生成唯一、稳定id
const id = useId();
return (
<>
<label htmlFor={id}>Do you like React?</label>
<input type="checkbox" name="react" id={id} />
</>
);
);
userId可以生成客户端与服务端唯一的id,并返回一个字符串,这样就可以将其作为当前组件的唯一身份。
如果在同一个组件中,我们需要多个 id,那么一定不要重复的使用 useId,而是基于一个 id 来创建不同的身份标识。额外添加不同的字符串即可。
function NameFields() {
const id = useId();
return (
<div>
<label htmlFor={id + '-firstName'}>First Name</label>
<div>
<input id={id + '-firstName'} type="text" />
</div>
<label htmlFor={id + '-lastName'}>Last Name</label>
<div>
<input id={id + '-lastName'} type="text" />
</div>
</div>
);
}
在React18之前,服务器端渲染(SSR),客户端必须一次性等待HTML数据加载到服务器上,并且等待所有JavaScript加载完毕后再开始。不支持客户端渲染常用的代码分割组合为React.lazy和Suspense。
在React 18中,基于全新的 Suspense,支持了流式 SSR,也就是允许服务端一点一点的返回页面。
假设有一个页面,包含了 NavBar、Sidebar、Post、Comments 等几个部分,在传统的 SSR 模式下,我们必须请求到 Post 数据,请求到 Comments 数据后,才能返回完整的 HTML。
<main>
<nav>
<!--NavBar -->
<a href="/">Home</a>
</nav>
<aside>
<!-- Sidebar -->
<a href="/profile">Profile</a>
</aside>
<article>
<!-- Post -->
<p>Hello world</p>
</article>
<section>
<!-- Comments -->
<p>First comment</p>
<p>Second comment</p>
</section>
</main>
在 React 18 中,我们通过 Suspense 包裹,可以告诉 React,我们不需要等这个组件,可以先返回其它内容,等这个组件准备好之后,单独返回。
<Layout>
<NavBar />
<Sidebar />
<RightPane>
<Post />
<Suspense fallback={<Spinner />}>
<Comments />
</Suspense>
</RightPane>
</Layout>
我们通过 Suspense 包裹了 Comments 组件,那服务器首次返回的 HTML 是下面这样的,
组件处通过 loading 进行了占位。
<main>
<nav>
<!--NavBar -->
<a href="/">Home</a>
</nav>
<aside>
<!-- Sidebar -->
<a href="/profile">Profile</a>
</aside>
<article>
<!-- Post -->
<p>Hello world</p>
</article>
<section id="comments-spinner">
<!-- Spinner -->
<img width=400 src="spinner.gif" alt="Loading..." />
</section>
</main>
当
组件准备好之后,React 会通过同一个流(stream)发送给浏览器(res.send 替换成 res.socket),并替换到相应位置。
<div hidden id="comments">
<!-- Comments -->
<p>First comment</p>
<p>Second comment</p>
</div>
<script>
// This implementation is slightly simplified
document.getElementById('sections-spinner').replaceChildren(
document.getElementById('comments')
);
</script>