2024年3月13日
By: Chase

聊聊React的新特性们

前言

上次面试, 聊到React近几年更新的大版本, 我才意识到我比较深入的认知还停留在Hooks刚出来的时候, 近期对社区的关注也就是只是停留在知道出了18.2, 并且已经全面拥抱Next.js了.

比起年龄, 更让程序员焦虑的, 是对新变化的不了解.

本文不是为了详细讲解新的hooks, 讲解肯定是官网最详细, 主要还是写一些demo帮助自己理解.

在blog中引入18.2


        console.log('React version:', React.version)
        console.log('ReactDOM version:', ReactDOM.version)
    

useLayoutEffect

这是个17.几的版本就出来的特性了, 一直没用过.

官网的描述: useLayoutEffect 是 useEffect 的一个版本,在浏览器重新绘制屏幕之前触发。

为了证明useLayoutEffect对渲染有阻拦作用, 写了下面一个简单的demo:

鼠标点击红框内即加载小蓝框, 点击绿框即隐藏小蓝框.

为了能看出差别, 加了一个比较重的循环, 多点几次, 在把示例代码改成useEffect可以明显看到有闪烁.

const { createRoot } = ReactDOM
const { useEffect, useLayoutEffect, useState } = React

const root = createRoot(document.getElementById('useLayoutEffect'))

const ChildDom = ({ position }) => {
    const [state, setState] = useState({ x: 0, y: 0 })

    // !!!!把这改成useEffect试试!!!!
    useLayoutEffect(() => {
        for (let i = 0; i < 1e8; i += 1) {
            const a = Math.random()
        }
        setState(position)
    }, [])

    return (
        <div style={{
            position: 'absolute',
            left: state.x,
            top: state.y,
            width: '100px',
            height: '100px',
            background: 'blue'
        }}>
            child
        </div>
    )
}

const App = () => {
	const [childPosition, setChildPosition] = useState(null)

    return (
		<div
            style={{
                display: 'flex'
            }}
        >
            <div
                onClick={e => {
                    setChildPosition({
                        x: e.nativeEvent.offsetX,
                        y: e.nativeEvent.offsetY
                    })
                }}
                style={{
                    border: '1px solid red',
                    width: '200px',
                    height: '200px',
                    position: 'relative'
                }}
            >
                click to show area
                
                {childPosition && (
                    <ChildDom
                        position={childPosition}
                    />
                )}
            </div>

            <div
                onClick={() => setChildPosition(null)}
                style={{
                    border: '1px solid green',
                    width: '200px',
                    height: '200px',
                    position: 'relative'
                }}
            >
                click to disappear area
            </div>
        </div>
    )
}

root.render(<App />);

这个功能我知道但是一直没用过确实是有原因的, 一时半会想不出使用的业务场景. 比如上面我写的demo, 把组件内部的初始stateset值的那一步完全可以想办法规避掉.

18的重头戏

react18讲的最多的就是并发渲染, 解决的是啥问题呢, 上个demo先, 请来回切换两个button, 卡吧?

卡就对了, activeB渲染了1万个div.

官方提供了两个hooks函数useTransitionuseDeferredValue, 前者允许并发一个与渲染activeB下div同级别的pendingUI事件, 从而达到友善的交互. 后者延迟一个activeB且允许被打断的渲染, 同样提升了用户交互感受.

const { createRoot } = ReactDOM
const { useState } = React

const root = createRoot(document.getElementById('demo'))

const ChildA = () => {
    console.log('renderA')

    return (
        <div>
            childA
        </div>
    )
}

const ChildB = () => {
    console.log('render heavy B')

    return (
        <div>
            {new Array(1e5).fill(0).map((_, i) => (
                <div key={i}>
                    childB
                </div>
            ))}
        </div>
    )
}

const App = () => {
    const [activeA, setActiveA] = useState(true)

    const handleClickButton = (param) => {
        setActiveA(param)
    }

    return (
        <div>
            <button
                style={{ color: activeA ? 'red' : 'black' }}
                onClick={() => handleClickButton(true)}
            >
                activate A
            </button>
            <button
                style={{ color: !activeA ? 'red' : 'black' }}
                onClick={() => handleClickButton(false)}
            >
                activate B
            </button>

            {activeA ? <ChildA /> : <ChildB />}
        </div>
    )
}

root.render(<App />);

useTransition

在这里例子中, 点击activeB后, "并发"了一个pending UI(你也可以改成activeB按钮的预激活状态), 直到ChildB渲染完成. 从UI交互的层面来说,卡顿的感受明显的减轻了.

const { createRoot } = ReactDOM
const { useTransition, useState } = React

const root = createRoot(document.getElementById('demo-useTransition'))

const ChildA = () => {
    console.log('renderA')

    return (
        <div>
            childA
        </div>
    )
}

const ChildB = () => {
    console.log('render heavy B')

    return (
        <div>
            {new Array(1e5).fill(0).map((_, i) => (
                <div key={i}>
                    childB
                </div>
            ))}
        </div>
    )
}

const App = () => {
    const [activeA, setActiveA] = useState(true)
    const [isPending, startTransition] = useTransition();

    const handleClickButton = (param) => {
        startTransition(() => {
            setActiveA(param)
        })
    }

    return (
        <div>
            <button
                style={{ color: activeA ? 'red' : 'black' }}
                onClick={() => handleClickButton(true)}
            >
                activate A
            </button>
            <button
                style={{ color: !activeA ? 'red' : 'black' }}
                onClick={() => handleClickButton(false)}
            >
                activate B
            </button>

            {isPending ? <div>loading...</div>
            : activeA ? <ChildA /> : <ChildB />}
        </div>
    )
}

root.render(<App />);

useDeferredValue

在这个例子中, 我将控制激活activeB按钮渲染activeB列表的state拆开了, 两个'state'"并发"触发, 将较重的renderListA放在了useDeferredValue中.

可以看到例子中, activeB的按钮是立刻激活的, 但是列表的渲染是延迟的.

这并不是单纯的拆分两个state就可以做到, 你可以将例子中renderListA改为const renderListA = activeA试试.

const { createRoot } = ReactDOM
const { useDeferredValue, useState, useMemo } = React

const root = createRoot(document.getElementById('demo-useDeferredValue'))

const ChildA = () => {
    console.log('renderA')

    return (
        <div>
            childA
        </div>
    )
}

const ChildB = () => {
    console.log('render heavy B')

    return (
        <div>
            {new Array(1e5).fill(0).map((_, i) => (
                <div key={i}>
                    childB
                </div>
            ))}
        </div>
    )
}

const App = () => {
    const [activeA, setActiveA] = useState(true)
    const renderListA = useDeferredValue(activeA)

    return (
        <div>
            <button
                style={{ color: activeA ? 'red' : 'black' }}
                onClick={() => setActiveA(true)}
            >
                activate A
            </button>
            <button
                style={{ color: !activeA ? 'red' : 'black' }}
                onClick={() => setActiveA(false)}
            >
                activate B
            </button>

            {renderListA ? <ChildA /> : <ChildB />}
        </div>
    )
}

root.render(<App />);
Tags: useLayoutEffect useTransition React useDeferredValue