当前位置: 首页 > 科技观察

React18升级指南

时间:2023-03-14 12:05:04 科技观察

React18RC.3版本已经发布,API已经稳定,现在主要是一些BUG修复,相信很快就会发布正式版。React团队对探索新功能非常谨慎。16.8版本已经过去3年了,并发模式的完整版终于来了。今天我们将从用户的角度探讨React17升级到18会遇到的问题和一些新功能。升级使用yarn,安装最新的React18RCyarnaddreact@rcreact-dom@rcchangeReact18已经放弃对IE11的支持,需要兼容IE的就用React17。createRootReact18提供了两个根API,我们称之为LegacyRootAPI和NewRootAPI。遗留根API:ReactDOM.render。这将创建一个以“传统”模式运行的根,其工作方式与React17完全相同。使用此API将收到警告,表明它已被弃用并切换到新根API。新建RootAPI:即createRoot。这将创建一个在React18中运行的根,它添加了React18的所有改进并允许使用并发功能。我们以Vite+TS作为脚手架开始了这个项目。项目启动后,你会在控制台看到一个警告:这意味着你可以直接将项目升级到React18版本,而不会直接导致breakchange。因为它只是给出一个警告,并且在整个18版本中都是可用和兼容的,并且保持了React17版本的特性。你为什么要这样做?因为直接升级项目比较直接,遇到了就换个地方,没有历史包袱。但是React组件生态非常庞大,很多组件会直接使用ReactDOM.render来渲染,比如常见UI库中类似Modal.confirm的API。这时候就需要一个版本周期来升级这些生态组件。//React17importReactDOMfrom'react-dom';constcontainer=document.getElementById('app');//LoadReactDOM.render(,container);//卸载ReactDOM.unmountComponentAtNode(container);//React18import{createRoot}from'反应-dom/客户端';constcontainer=document.getElementById('app');constroot=createRoot(container);//加载root.render();//卸载root.unmount();另外不得不说createRootAPI和Vue3的createApp形式完全一样。FAQ:在TypeScript中,createRoot中的参数容器可以接收HTMLElement,但不能为空。使用断言或添加判断~服务端渲染hydrateRoot如果你的应用使用注水服务端渲染,请将hydrate升级为hydrateRoot。constroot=hydrateRoot(container,);//不需要执行root.render这个版本也改进了react-dom/serverAPI,全面支持Suspense和streamingSSR服务器。作为这些更改的一部分,不支持服务器上增量Suspense流的旧节点流API将被弃用。renderToNodeStream=>renderToPipeableStream。添加了renderToReadableStream以支持Deno。继续使用renderToString(对Suspense的有限支持)。继续使用renderToStaticMarkup(对Suspense的有限支持)。setStatesynchronous/asynchronous这是这个React版本中最大的突破性更新,它不向后兼容。React中的批处理只是将多个状态更新合并为一个重新渲染以获得更好的性能。在React18之前,React只能在组件生命周期函数或合成事件函数中进行批处理。默认情况下,Promise、setTimeout和本机事件不会被批处理。如果需要保持batching,可以使用unstable_batchedUpdates来实现,但不是官方API。在React18之前:functionhandleClick(){setCount(1);设置标志(真);//批处理:将合并到一个渲染中}asyncfunctionhandleClick(){awaitsetCount(2);设置标志(假);//同步模式:Render会被执行两次//最新的计数值可以通过RefbeforesetFlagaftersetCount}在React18中,上面的第二个例子只会有一次render,因为所有的更新都会被自动批处理。这无疑是提高应用程序整体性能的好方法。flushSyncReact18中想退出批处理怎么办?提供了官方APIflushSync。flushSync(fn:()=>R):R它接受一个函数作为参数并允许返回值。函数handleClick(){flushSync(()=>{setCount(3);});//setFlag会在setCount和render之后执行setFlag(true);}注意:flushSync以函数为作用域,内部函数多个setStates仍然是批量更新,这样可以精确控制哪些批量更新不需要:函数handleClick(){flushSync(()=>{setCount(3);setFlag(true);});//setCount和setFlag是Batchupdate,结束后setLoading(false);//这个方法会触发两次渲染}这个方法会比React17和之前的方法更优雅地控制重新渲染。flushSync在某些场景下非常有用,比如在一个表单中点击save按钮,触发子表单的关闭,并同步到全局state,state更新后调用save方法:sub-form:exportdefaultfunctionChildForm({storeTo}){const[form]=Form.useForm();//当前组件卸载时,将子表单的值同步到全局//要触发父组件同步setState,必须使用useLayoutEffectuseLayoutEffect(()=>{return()=>{storeTo(form.getFieldsValue());};},[]);return();}外部容器:{//触发子表单卸载并关闭flushSync(()=>setVisible(false));//子表单值更新到全局后,触发save方法,可以保证onSave得到最新填充的表单值onSave();}}>保存

{visible&&}
但是React18中会加入unstable_batchedUpdates继续保留整个版本,因为很多开源库都用到了.卸载的组件更新状态警告我们在正常开发过程中不可避免地会犯以下错误:这个警告被广泛误解并且有点误导。原本打算用于以下场景:useEffect(()=>{functionhandleChange(){setState(store.getState());}store.subscribe(handleChange);return()=>store.unsubscribe(handleChange);},[]);如果你忘记调用unsubscribeineffectcleanup,你就会有内存泄漏。在实践中,上述情况并不常见。这在我们的代码中更为常见:asyncfunctionhandleSubmit(){setLoading(true);//组件可能会在我们等待时卸载awaitpost('/some-api');setLoading(false);}在这里,也会触发警告。但是,在这种情况下,警告具有误导性。这里没有实际的内存泄漏,Promise很快就解决了,之后它可以被垃圾收集。为了抑制这个警告,我们可能会写很多无用的isMounted判断,这会让代码变得更复杂。React18中移除了这个警告。组件返回null在React17中,如果组件在render中返回undefined,React会在运行时抛出一个错误:functionDemo(){returnundefined;}这里我们可以将undefined替换为null,程序将继续运行。此行为的目的是帮助用户发现不小心忘记返回语句的常见问题。对于React18的Suspensefallback,会出现undefined而不是错误,导致不一致。类型系统和Eslint现在都非常健壮,可以避免此类低级错误,因此React18不再检查因返回undefined导致的崩溃。StrictMode从React17开始,React会自动修改控制台方法,例如console.log()以在第二次调用生命周期函数时静默日志。但是,在某些可能有解决方法的情况下,它可能会导致不良行为。此行为已在React18中删除,如果安装了ReactDevTools>4.18.0,则第二次渲染期间的日志现在将以柔和的颜色显示在控制台中。新APIuseSyncExternalStoreuseSyncExternalStore进行了修改,由unstable_useMutableSource改为订阅外部数据源。主要帮助有外部存储需求的开发者解决撕裂问题。监听innerWidth变化的钩子最简单的例子:import{useMemo,useSyncExternalStore}from'react';functionuseInnerWidth():number{//保持subscribe固定引用,避免重复执行resizelistenerconst[subscribe,getSnapshot]=useMemo(()=>{return[(notify:()=>void)=>{//这里会使用Throttlewindow.addEventListener('resize',notify);return()=>{window.removeEventListener('resize',notify);};},//返回resize()之后需要的快照=>window.innerWidth,];},[]);返回useSyncExternalStore(subscribe,getSnapshot);}functionWindowInnerWidthExample(){constwidth=useInnerWidth();return

width:{width}

;}演示地址:https://codesandbox.io/s/usesyncexternalstore-demo-q47kyn。React自身的state已经原生解决了并发特性下的撕裂问题。useSyncExternalStore主要是针对框架开发者,比如redux,在控制state的时候可能不会直接使用React的state,而是在外部维护一个store对象,脱离React的管理,所以不能依赖React自动解析撕裂的问题。所以React对外提供了这样一个API。目前React-Redux8.0已经基于useSyncExternalStore实现。useInsertionEffectuseInsertionEffect的工作原理与useLayoutEffect大致相同,只是此时无法访问对DOM节点的引用。所以推荐的解决方案是使用这个Hook来插入样式表(或者如果你需要删除它们则引用它们):添加(规则);document.head.appendChild(getStyleForRule(规则));}});返回规则;}functionComponent(){letclassName=useCSS(rule);return;}useIduseId是一种API,用于在客户端和服务器上生成唯一ID,同时避免水合作用不匹配。用法示例:functionCheckbox(){constid=useId();return(
选择框
);}并发(concurrent)模式并发模式是React的一组新特性,可以帮助应用程序保持响应并根据用户的设备性能和网络速度进行适当调整。可中断以修复阻塞渲染限制。在Concurrent模式下,React可以同时更新多个状态。通常,当我们更新状态时,我们希望这些更改立即反映在屏幕上。期望应用程序持续响应用户输入是合理的。但是,有时我们希望更新会延迟对屏幕的响应。以前很难在React中实现此功能。并发模式提供了一系列新工具来实现这一点。Transition在React18中引入了一个新的APIstartTransition,主要是为了在大量任务下保持UI响应。这个新的API可以通过将特定更新标记为“过渡”来显着改善用户交互。概述:import{startTransition}from'react';//紧急:显示输入内容setInputValue(input);//将回调函数中的更新标记为非紧急更新startTransition(()=>{setSearchQuery(input);});简单来说就是将startTransition回调包裹的setState触发的渲染标记为非紧急渲染,这些渲染可能会被其他紧急渲染抢占。一般来说,我们需要通知用户后台正在运行。为此提供一个带有isPending转换标志的useTransition,React将在状态转换期间提供视觉反馈,并在转换发生时保持浏览器响应。从“反应”导入{useTransition};const[isPending,startTransition]=useTransition();当转换挂起时,isPending值为true,此时可以在页面中放置一个加载器。正常情况下:使用useTransition性能:Demo地址:https://codesandbox.io/s/starttransition-demo-o59ld2。我们可以使用startTransition来包装我们想要移动到后台的任何更新。通常,这些类型的更新分为两类:渲染缓慢:这些更新需要时间,因为React需要做大量工作来转换UI以显示结果。网络慢:这些更新需要时间,因为React正在等待来自网络的一些数据。这种方法与Suspense紧密集成。网速慢的场景:一个列表页面,当我们点击“下一页”时,已有的列表立即消失,然后我们看到整个页面只有一个加载提示。可以说这是一种“不受欢迎”的加载状态。如果我们可以“跳过”这个过程并等到内容加载后再过渡到新页面,那就更好了。这里结合Suspense进行加载边界处理:constmockResource=fetchMockData(1);exportdefaultfunctionDemoList(){const[resource,setResource]=useState(mockResource);const[isPending,startTransition]=useTransition();返回((2));})}>下一页{isPending&&加载中
});}functionUserList({resource}:UserListProps){constmockList=resource.read();返回({mockList.map((item)=>;({item.id}
{item.name}
{item.age}岁))});}结果展示:Demo地址:https://codesandbox.io/s/usetransition-request-demo-wgedzw将Transition集成到应用程序设计系统useTransition中是一个很常见的需求。几乎任何可能导致组件挂起的点击或交互都需要useTransition以避免意外隐藏用户正在交互的内容。这会导致组件中出现大量重复代码。通常建议将useTransition合并到应用程序的设计系统组件中。例如,我们可以将useTransition逻辑提取到我们自己的组件中:functionButton({children,onClick}){const[startTransition,isPending]=useTransition();函数handleClick(){startTransition(()=>{onClick();});}return({children}{isPending?'Loading':null});}FAQ:useTransition有一个可选参数,可以设置超时时间timeoutMs,但是目前的TS类型没有开放。useDeferredValue返回一个延迟响应值,当您有根据用户输入立即呈现的内容和需要等待获取数据的内容时,通常用于保持界面响应。import{useDeferredValue}from'react';constdeferredValue=useDeferredValue(值);从介绍上看,是不是觉得useDeferredValue和useTransition很像?同上:useDeferredValue本质上和内部实现、useTransition一样被标记为延迟更新任务。不同点:useTransition将更新任务变成延迟更新任务,useDeferredValue生成一个新值,作为延迟状态。那么它和debounce有什么区别呢?Debounce,即setTimeout,总是有固定的延迟,useDeferredValue的值只会滞后于渲染时间。在性能好的机器上,延迟会少一些,反之亦然。长的。结语以上就是本次React升级的大致内容,主要围绕并发模式展开。赶快准备正式版发布后升级吧!