网络日志

React loading

需求

众所周知,应用如果少了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>
)

不过,目前这个组合有一个致命缺点

即新组件尚未完成加载时,旧组件已经隐藏,且不论新组件加载得多快,Suspensefallback都会执行,这将导致页面切换时出现闪烁

虽然官方推出了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的需求算是基本实现了,后续开发中若有新收获再来同步。

若有不足,欢迎指正。