• Babel 插件通关秘籍
  • Git 原理详解及实用指南
  • Nest 通关秘籍
  • React 通关秘籍
  • TypeScript 全面进阶指南
  • TypeScript 类型体操通关秘籍
  • 现代CSS
  • Babel 插件通关秘籍
  • Git 原理详解及实用指南
  • Nest 通关秘籍
  • React 通关秘籍
  • TypeScript 全面进阶指南
  • TypeScript 类型体操通关秘籍
  • 现代CSS
  • React 通关秘籍

    • 1.关于本小册
    • 2.一网打尽组件常用Hook
    • 3.Hook的闭包陷阱的成因和解决方案
    • 4.React组件如何写TypeScript类型
    • 5.React组件如何调试
    • 6.受控模式VS非受控模式
    • 7.组件实战:迷你Calendar
    • 8.组件实战:Calendar日历组件(上)
    • 9.组件实战:Calendar日历组件(下)
    • 10.快速掌握Storybook
    • 11.React组件如何写单测
    • 12.深入理解Suspense和ErrorBoundary
    • 13.组件实战:Icon图标组件
    • 14.组件实战:Space间距组件
    • 15.React.Children和它的两种替代方案
    • 16.三个简单组件的封装
    • 17.浏览器的5种Observer
    • 18.组件实战:Watermark防删除水印组件
    • 19.手写react-lazyload
    • 20.图解网页的各种距离
    • 21.自定义hook练习
    • 22.自定义hook练习(二)
    • 23.用react-spring做弹簧动画
    • 24.react-spring结合use-gesture手势库实现交互动画
    • 25.用react-transition-group和react-spring做过渡动画
    • 26.快速掌握Tailwind:最流行的原子化CSS框架
    • 27.用CSSModules避免样式冲突
    • 28.CSSInJS:快速掌握styled-components
    • 29.react-spring实现滑入滑出的转场动画
    • 30.组件实战:Message全局提示组件
    • 31.组件实战:Popover气泡卡片组件
    • 32.项目里如何快速定位组件源码
    • 33.一次超爽的React调试体验
    • 34.组件实战:ColorPicker颜色选择器(一)
    • 35.组件实战:ColorPicker颜色选择器(二)
    • 36.组件实战:onBoarding漫游式引导组件
    • 37.组件实战:Upload拖拽上传
    • 38.组件实战:Form表单组件
    • 39.React组件库都是怎么构建的
    • 40.组件库实战:构建esm和cjs产物,发布到npm
    • 41.组件库实战:构建umd产物,通过unpkg访问
    • 42.数据不可变:immutable和immer
    • 43.基于ReactRouter实现keepalive
    • 44.Historyapi和ReactRouter实现原理
    • 45.ReactContext的实现原理和在antd里的应用
    • 46.ReactContext的性能缺点和解决方案
    • 47.手写一个Zustand
    • 48.原子化状态管理库Jotai
    • 49.用react-intl实现国际化
    • 50.国际化资源包如何通过Excel和GoogleSheet分享给产品经理
    • 51.基于react-dnd实现拖拽排序
    • 52.react-dnd实战:拖拽版TodoList
    • 53.ReactPlayground项目实战:需求分析、实现原理
    • 54.ReactPlayground项目实战:布局、代码编辑器
    • 55.ReactPlayground项目实战:多文件切换
    • 56.ReactPlayground项目实战:babel编译、iframe预览
    • 57.ReactPlayground项目实战:文件增删改
    • 58.ReactPlayground项目实战:错误显示、主题切换
    • 59.ReactPlayground项目实战:链接分享、代码下载
    • 60.ReactPlayground项目实战:WebWorker性能优化
    • 61.ReactPlayground项目实战:总结
    • 62.手写MiniReact:思路分析
    • 63.手写MiniReact:代码实现
    • 64.手写MiniReact:和真实React源码的对比
    • 65.React18的并发机制是怎么实现的
    • 66.Ref的实现原理
    • 67.低代码编辑器:核心数据结构、全局store
    • 68.低代码编辑器:拖拽组件到画布、拖拽编辑json
    • 69.低代码编辑器:画布区hover展示高亮框
    • 70.低代码编辑器:画布区click展示编辑框
    • 71.低代码编辑器:组件属性、样式编辑
    • 72.低代码编辑器:预览、大纲
    • 73.低代码编辑器:事件绑定
    • 74.低代码编辑器:动作弹窗
    • 75.低代码编辑器:自定义JS
    • 76.低代码编辑器:组件联动
    • 77.低代码编辑器:拖拽优化、Table组件
    • 78.低代码编辑器:Form组件、store持久化
    • 79.低代码编辑器:项目总结
    • 80.快速掌握ReactFlow画流程图
    • 81.ReactFlow振荡器调音:项目介绍
    • 82.ReactFlow振荡器调音:流程图绘制
    • 83.ReactFlow振荡器调音:合成声音
    • 84.AudioContext实现在线钢琴
    • 85.React服务端渲染:从SSR到hydrate
    • 86.小册总结

有的时候,直接展示一个组件会过于突兀,需要一些过渡效果。

比如这样:

点击结算的时候,展示结算页面的组件,点击返回的时候隐藏。

这里是通过滑入滑出的动画来实现的过渡。

这个效果是我公司的项目里真实在用的,这节我们来一起实现下。

我们说的转场动画、过渡动画是一个东西,你看下英文翻译就知道了:

这里的过渡动画很明显可以用我们学过的 react-transition-group 或者 react-spring 来做。

我们就用 reac-spring 结合上节学的 styled-components 来实现下。

创建个 vite 项目:

npx create-vite slide-in-out-transition

安装用到的包:

npm install

npm install --save @react-spring/web

npm install --save styled-components

要实现这样的效果:

首先,我们需要一个 div 包裹它。

创建 src/Overlay.tsx

import styled from "styled-components";

const Overlay = styled.div`
    position: fixed;
    top: 0;
    right: 0;
    bottom: 0;
    left: 0;
    background-color: rgba(0, 0, 0, 0.3);
    z-index: 10;
`;

export default Overlay;

然后在 src/SlideInOverlay.tsx 里用一下:

import React, { FC, PropsWithChildren } from "react";
import { useTransition, animated } from "@react-spring/web";
import Overlay from "./Overlay";

const DURATION = 300;

interface SlideInOverlayProps extends PropsWithChildren {
    isVisible: boolean;
    from?: "right" | "bottom";
}

const SlideInOverlay: FC<SlideInOverlayProps> = (props) => {
    const { isVisible, from = "right", children } = props;

    const x = React.useMemo(
        () => (from === "right" ? window.screen.width : window.screen.height),
        [from]
    );

    const transitions = useTransition(isVisible, {
        x,
        opacity: 1,
        from: {
            x,
            opacity: 1,
        },
        enter: { x: 0, opacity: 1 },
        leave: { x, opacity: 0 },
        config: { duration: DURATION },
    });

    const translate = React.useCallback(
        (x: number) => {
            switch (from) {
                case "right":
                    return `translateX(${x}px)`;
                case "bottom":
                    return `translateY(${x}px)`;
            }
        },
        [from]
    );

    return (
        <>
            {transitions(
                (props, isVisible) =>
                    isVisible && (
                        <Overlay
                            as={animated.div}
                            style={{
                                transform: props.x.to((x) =>
                                    x === 0 ? "none" : translate(x)
                                ),
                                opacity: props.opacity,
                            }}>
                            {children}
                        </Overlay>
                    )
            )}
        </>
    );
};

export { SlideInOverlay, DURATION };

代码比较多,我们一部分一部分来看下:

首先,这个 SlideInOverlay 组件有 3 个 props:

isVisible 是是否展示。

from 是从右向左还是从下向上来运动,取值为 right 或 bottom。

children 传入具体的内容。

用 react-spring 的 useTransition 来做动画,改变 x、opcity 属性。

设置初始值、from 的值、enter 的值,以及 leave 的值。

也就是进入动画开始、进入动画结束、离开动画结束的值。

然后下面的 div 使用 react-spring 传入的 x、opcity 来设置样式就好了。

Overlay 是样式组件,用 as 转为用 animated.div 渲染。

初始值 x 根据 from 参数是 right 还是 bottom 来设置 window.screen.width 或者 height。

这里用 useMemo 的好处是只要 from 参数没变,就直接用之前的值。

然后 react-spring 传入的 x 还需要根据 from 来转为 translateX 或者 translateY 的样式。

这样,转场动画就完成了。

我们来试一下:

去掉 main.tsx 的 StrictMode 和 index.css。

在 App.tsx 里用一下:

import { useState } from "react";
import reactLogo from "./assets/react.svg";
import viteLogo from "/vite.svg";
import "./App.css";
import { SlideInOverlay } from "./SlideInOverlay";

function App() {
    const [count, setCount] = useState(0);
    const [show, setShow] = useState(false);

    return (
        <>
            <button
                onClick={() => {
                    setShow(true);
                }}>
                开启
            </button>
            <SlideInOverlay
                isVisible={show}
                from="right"
                className={"guangguang"}
                style={{
                    border: "2px solid #000",
                }}>
                <div>
                    <button
                        onClick={() => {
                            setShow(false);
                        }}>
                        关闭
                    </button>
                    <a href="https://vitejs.dev" target="_blank">
                        <img src={viteLogo} className="logo" alt="Vite logo" />
                    </a>
                    <a href="https://react.dev" target="_blank">
                        <img
                            src={reactLogo}
                            className="logo react"
                            alt="React logo"
                        />
                    </a>
                </div>
                <h1>Vite + React</h1>
                <div className="card">
                    <button onClick={() => setCount((count) => count + 1)}>
                        count is {count}
                    </button>
                    <p>
                        Edit <code>src/App.tsx</code> and save to test HMR
                    </p>
                </div>
                <p className="read-the-docs">
                    Click on the Vite and React logos to learn more
                </p>
            </SlideInOverlay>
        </>
    );
}

export default App;

我们加了一个 state 来保存显示隐藏状态,加了两个 button,点击的时候切换。

跑一下:

npm run dev

可以看到,滑入滑出的转场动画(或者叫过渡动画)生效了。

而且因为只是改变了 translate,组件不会销毁,所以状态也可以保留。

再来试下另一种效果,把 from 改为 bottom:

完美!

用在真实项目里就是这样的:

然后我们再完善一下细节:

加上 className 和 style 两个 props。

interface SlideInOverlayProps extends PropsWithChildren {
    isVisible: boolean;
    from?: "right" | "bottom";
    className?: string | string[];
    style?: CSSProperties;
}

传入样式组件:

安装用到的 classnames 包

npm install --save classnames

测试下:

d-14.png

可以看到 style 生效了,className 也加上了。

我们继续完善,添加 onEnter 参数:

然后加一下处理逻辑:

useEffect(() => {
    let timer = null;

    if (isVisible === true && onEnter != null) {
        timer = setTimeout(onEnter, DURATION);
    }

    return () => {
        if (timer != null) {
            clearTimeout(timer);
        }
    };
}, [isVisible, onEnter]);

因为我们设置了动画的时长是 DURATION 常量,所以这里用一个 setTimeout 就可以实现 onEnter

判断下 isVisible 是 true 的时候再执行 onEnter 的定时器。

并且当 isVisible、onEnter 变化的时候,销毁上次的定时器,重新跑。

测试下:

没啥问题。

接下来继续实现 onExit。

添加参数,然后加上 useEffect 通过 setTimeout 触发:

useEffect(() => {
    let timer = null;

    if (isVisible === false && onExit != null) {
        timer = setTimeout(onExit, DURATION);
    }

    return () => {
        if (timer != null) {
            clearTimeout(timer);
        }
    };
}, [isVisible, onExit]);

跑一下:

可以看到,滑入滑出时的回调没问题,但是最开始多回调了一次 onExit。

如何判断出最开始那一次呢?

记录下 isVisible 参数就可以了,如果是从 true 变为 false 才触发。

用 useRef 保存上次的 isVisible 参数的值,如果上次的是 true 而当前 isVisible 是 false 就触发。

const visibleRef = useRef(isVisible);

useEffect(() => {
    let timer = null;

    if (isVisible === false && visibleRef.current === true && onExit != null) {
        timer = setTimeout(onExit, DURATION);
    }

    visibleRef.current = isVisible;

    return () => {
        if (timer != null) {
            clearTimeout(timer);
        }
    };
}, [isVisible, onExit]);

测试下:

可以看到,现在最开始多的一次调用就没有了。

这样,这个 SlideInOverlay 组件就完成了。

当然,你还可以做更多的扩展,比如点击商品的时候从下面滑入商品详情:

这里是距离顶部有一段距离的,这个距离也可以作为参数传入。

如果想实现这种和手势结合的动画呢?

这个我们做过了呀,可以回去看看手势库那节。

拖动速度、方向、距离这类需求都可以用手势库搞定。

案例代码上传了小册仓库

总结

很多场景下,加上转场动画会使交互体验更好。

这节我们用 react-spring 实现了滑入滑出的转场动画(或者叫过渡动画)。

支持了 isVisible、from、children、onExit、onEnter、className、style 参数。

from 可以设置 right 或 bottom,然后根据它来设置 x 参数初始值为 window.screen.width 或者 window.screen.height。

改变 x、opacity 就可以实现滑入滑出的动画。

我们通过 useRef 记录之前的参数来实现了 onExit 的回调。

用 styled-components 写了外层 div 的样式。

这样的 SlideInOverlay 组件就比较完善了,可以直接用在项目里。

上次更新: 6/21/25, 9:42 AM
贡献者: YNight
Prev
28.CSSInJS:快速掌握styled-components
Next
30.组件实战:Message全局提示组件