【问题标题】:React page / layout transitions with useLayoutEffect使用 useLayoutEffect 反应页面/布局转换
【发布时间】:2019-02-03 12:12:26
【问题描述】:

我定义了以下结构,以便根据路径名在布局之间进行动画转换。

<LayoutTransition pathname={pathname}>
  {pathname.includes('/registration') && <RegistrationLayout />}
  {pathname.includes('/dashboard') && <DashboardLayout />}
</LayoutTransition>

RegistrationLayoutDashboardLayout 内部结构相似,但它们根据路径名而不是布局显示不同的页面。

在我的 LayoutTransition 组件中,我有以下逻辑

  useLayoutEffect(() => {
    // Root paths are "/registration" "/dashboard"

    const rootPathname = pathname.split('/')[1];
    const rootPrevPathname = prevPathname.current.split('/')[1];

    if (rootPathname !== rootPrevPathname) {
      /*
       * Logic that:
       * 1. Animates component to 0 opacity
       * 2. Sets activeChildren to null
       * 3. Animates component to 1 opacity
       */
    }

    return () => {
      // Sets activeChildren to current one
      setActiveChildren(children);
    };
  }, [pathname]);

  /* renders activeChildren || children */

一般来说,这个概念是有效的,即我在动画 out 时看到我的“当前”孩子,然后当 activeChildren 设置为 null 时,我在动画 in。

唯一的问题是,似乎当我设置 setActiveChildren(children); 布局重新渲染时,我看到闪烁并且布局显示的页面返回到其初始状态。

有没有办法在我们制作动画时避免这种情况以及冻结孩子,因此不会发生重新渲染?

编辑:来自 react-native 项目的完整代码 sn-p

核心思想是我们订阅路由器上下文,当 rootPathname 更改时,我们将当前布局(子级)动画化,然后将新布局动画化。

import React, { useContext, useLayoutEffect, useRef, useState } from 'react';
import { Animated } from 'react-native';
import RouterCtx from '../context/RouterCtx';
import useAnimated from '../hooks/useAnimated';
import { durationSlow, easeInQuad } from '../services/Animation';

/**
 * Types
 */
interface IProps {
  children: React.ReactNode;
}

/**
 * Component
 */
function AnimRouteLayout({ children }: IProps) {
  const { routerState } = useContext(RouterCtx);
  const { rootPathname } = routerState;
  const [activeChildren, setActiveChildren] = useState<React.ReactNode>(undefined);
  const [pointerEvents, setPointerEvents] = useState(true);
  const prevRootPathname = useRef<string | undefined>(undefined);
  const [animatedValue, startAnimation] = useAnimated(1, {
    duration: durationSlow,
    easing: easeInQuad,
    useNativeDriver: true
  });

  function animationLogic(finished: boolean, value: number) {
    setPointerEvents(false);
    if (finished) {
      if (value === 0) {
        setActiveChildren(undefined);
        startAnimation(1, animationLogic, { delay: 150 });
      }
      setPointerEvents(true);
    }
  }

  useLayoutEffect(() => {
    if (prevRootPathname.current) {
      if (rootPathname !== prevRootPathname.current) {
        startAnimation(0, animationLogic);
      }
    }

    return () => {
      prevRootPathname.current = rootPathname;
      setActiveChildren(children);
    };
  }, [rootPathname]);

  return (
    <Animated.View
      pointerEvents={pointerEvents ? 'auto' : 'none'}
      style={{
        flex: 1,
        opacity: animatedValue.interpolate({ inputRange: [0, 1], outputRange: [0, 1] }),
        transform: [
          {
            scale: animatedValue.interpolate({ inputRange: [0, 1], outputRange: [1.1, 1] })
          }
        ]
      }}
    >
      {activeChildren || children}
    </Animated.View>
  );
}

export default AnimRouteLayout;

【问题讨论】:

  • 还有其他地方叫setActiveChildren吗?查看useState 调用(我假设setActiveChildrenuseState 的setter)以及处理该状态的任何行的确切代码将有所帮助。有些东西导致孩子们重新安装,但如果没有看到更多细节,很难确定原因。
  • @RyanCogswell我试图让我的问题保持简单,因为它基于 react-native 组件。但同样的逻辑也适用。我在问题中添加了完整的代码剪辑器并进行了一些解释。现在我需要想办法让activeChildren 成为一种不会根据路由器上下文变化而变化的子节点的冻结快照。

标签: reactjs react-native react-hooks


【解决方案1】:

首先,我将通过列出用户从“/registration”开始然后切换到“/dashboard”时发生的步骤来描述我认为当前代码发生的情况:

  • AnimRouteLayoutrootPathname='/registration' 的初始渲染
    • 初始布局效果排队
    • activeChildren 未定义,所以返回 children 进行渲染
    • &lt;RegistrationLayout /&gt; 被渲染
    • 队列布局效果执行
      • prevRootPathname.current 未定义,所以没有动画
      • 布局效果清理已注册到 React
  • 用户切换到“/dashboard”触发渲染AnimRouteLayoutrootPathname='/dashboard'
    • 由于rootPathname不同,第二个布局效果排队
    • activeChildren 仍然是未定义的,所以 children 返回被渲染
    • &lt;RegistrationLayout /&gt; 卸载和 &lt;DashboardLayout /&gt; 被渲染
    • 执行上一个布局效果的清理
      • prevRootPathname.current 设置为 '/registration'
      • activeChildren 被设置为之前的孩子,导致另一个渲染排队
    • 队列布局效果执行并启动动画
    • 由于activeChildren 状态更改,AnimRouteLayout 的另一个渲染开始
    • 一个额外的布局效果没有排队,因为rootPathname没有不同
    • activeChildren 返回渲染
    • &lt;DashboardLayout /&gt; 卸载
    • &lt;RegistrationLayout /&gt; 以新状态重新挂载并呈现
    • 动画完成并将activeChildren设置回未定义
    • AnimRouteLayout 再次渲染,这次 &lt;DashboardLayout /&gt; 将被渲染

虽然可以以防止重新安装的方式管理activeChildren,但我认为有一种更简洁的方法来解决这个问题。与其尝试冻结 children,不如冻结 pathname。在写this answer 时,我对这些想法进行了大量的实验。为了保持这一点,我想出的术语是区分:

  • targetPathname 用户指定的路径
  • renderPathname当前正在渲染的路径

大多数情况下,这些路径是相同的。例外情况是在退出转换期间renderPathname 将具有先前targetPathname 的值。使用这种方法,您将获得类似以下内容:

<AnimRouteLayout targetPathname={pathname}>
  {(renderPathname)=> {
    return <>
      {renderPathname.includes('/registration') && <RegistrationLayout />}
      {renderPathname.includes('/dashboard') && <DashboardLayout />}
    </>;
  }}
</AnimRouteLayout>

然后AnimRouteLayout 只需要适当地管理renderPathname

function AnimRouteLayout({ children, targetPathname }) {
  const [renderPathname, setRenderPathname] = useState(targetPathname);
  // End of animation would set renderPathname to targetPathname
  return children(renderPathname);
}

由于我没有尝试为此创建一个可行的示例,因此我不保证我在上面没有语法事故,但我相当有信心这种方法是合理的。

【讨论】:

  • 惊人的答案,我今天晚些时候会尝试应用你的方法,并会在这里评论结果!
  • 我必须说,这是一个简单而优雅的解决方案 Ryan。我现在可以工作了:)
猜你喜欢
  • 2019-07-21
  • 2021-05-18
  • 1970-01-01
  • 2014-06-17
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-05-14
  • 1970-01-01
相关资源
最近更新 更多