需求
众所周知,应用如果少了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
的需求算是基本实现了,后续开发中若有新收获再来同步。
若有不足,欢迎指正。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。