需求
众所周知,应用如果少了loading
,交互就显得僵硬。
本文分享如何在React
中从零到一实现并使用loading
。
实现
一个loading
,应该始终出现在视口的正中。
同时为了表示加载过程的动态性,需要适当的动画。
以及,成功和失败的提示与loading
其实本质上是一回事,所以,实现loading
的同时,也顺便将另外两个一并实现。
严格意义上,这是toast
的实现。
Toast.jsx
import l from './toast.module.css' import {FontAwesomeIcon} from '@fortawesome/react-fontawesome' import {faCheck, faSpinner, faTriangleExclamation} from '@fortawesome/pro-solid-svg-icons' import PropTypes from 'prop-types' function Toast({type, message}) { return ( <div className={l.toast} > <div className={l.container} > { type === 'success' ? <FontAwesomeIcon icon={faCheck} /> : type === 'fail' ? <FontAwesomeIcon icon={faTriangleExclamation} /> : <FontAwesomeIcon icon={faSpinner} className={l.loading} /> } </div> { message ? message : type === 'success' ? '完成' : type === 'fail' ? '失败' : '加载中' } </div> ) } Toast.propTypes = { type: PropTypes.oneOf(['loading', 'success', 'fail']), message: PropTypes.string } export default Toast
toast.module.css
:root { --toastSize: 136px; --containerSize: 40px; } .toast { width: var(--toastSize); height: var(--toastSize); position: fixed; top: calc(50vh - var(--toastSize)/2); left: calc(50vw - var(--toastSize)/2); background: #4C4C4C; border-radius: 12px; display: flex; flex-direction: column; align-items: center; justify-content: center; color: rgba(255, 255, 255, .9); } .container { width: var(--containerSize); display: inherit; flex-direction: inherit; align-items: inherit; margin: 0 auto 16px; } .container > svg { height: var(--containerSize); } .loading { animation: rotate linear 1s infinite; } @keyframes rotate { from { transform: rotate(0); } to { transform: rotate(360deg); } }
使用路由
loading
的首要应用场景就是页面间的切换。
使用lazy()
和Suspense
配合React Router实现页面间切换时新页面加载loading
的显隐:
// index.js import {Suspense, lazy} from 'react' import {createRoot} from 'react-dom/client' import {BrowserRouter, Routes, Route} from 'react-router-dom' import Toast from './components/toast/Toast' const Home = lazy(() => import('./routes/Home')) const About = lazy(() => import('./routes/About')) const container = document.getElementById('root') const root = createRoot(container) root.render( <BrowserRouter> <Suspense fallback={<Toast/>}> <Routes> <Route path="/" element={<Home />} /> <Route path="/about" element={<About />} /> </Routes> </Suspense> </BrowserRouter> )
不过,目前这个组合有一个致命缺点:
即新组件尚未完成加载时,旧组件已经隐藏,且不论新组件加载得多快,Suspense
的fallback
都会执行,这将导致页面切换时出现闪烁。
虽然官方推出了startTransition()
解决闪烁问题,但该方案目前尚未适用于路由。
所以这个组合实践中不推荐使用,因为以页面闪烁为代价实现loading
得不偿失。
过渡
React
新增的useTranstion() Hook
,搭配useState()
,实现动态组件加载时loading
的显隐。
import {useEffect, useState, useTransition} from 'react' import {url} from '../../configuration' import Toast from '../../../components/toast/Toast' import Table from '../../../components/table/Table' export default function User() { const [res, setRes] = useState({}) const [loading, startTransition] = useTransition() // loading表示过渡任务的等待状态 useEffect(() => { fetch(`${url}`, {method: 'GET'}) .then(r => r.json()) .then(d => { if (d.hasOwnProperty('err')) alert(`${d.text}:${d.err}`) else if ( d.hasOwnProperty('data') ) startTransition(() => {setRes(d)}) // 将setRes()标记为过渡任务 }) .catch(e => {alert(e)}) }, []) return ( <div> {loading && <Toast/>} <Table res={res} /> </div> ) }
useTransition()
几乎能覆盖loading
的绝大部分场景,因为在React
中,组件的更新基本都是通过在useEffect()
的依赖数组中添加state
来实现。
总结
本着官方支持的绝不自己再封装的原则,loading
的需求算是基本实现了,后续开发中若有新收获再来同步。
若有不足,欢迎指正。