使用useLayoutEffect

时间:2019-02-03 12:12:26

标签: reactjs react-native react-hooks

我定义了以下结构,以便根据路径名对布局之间的过渡进行动画处理。

<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,在动画时看到我的“新”子代。在

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

当我们制作动画时,有没有办法避免这种情况并冻结孩子,所以不会对它们进行重新渲染?

编辑:本机项目的完整代码段

核心思想是我们订阅路由器上下文,当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;

1 个答案:

答案 0 :(得分:1)

首先,我将介绍当用户从“ / registration”开始然后切换到“ / dashboard”时发生的步骤,以描述我当前代码所发生的情况:

  • AnimRouteLayoutrootPathname='/registration'的初始呈现
    • 初始布局效果已排队
    • activeChildren未定义,因此children返回被渲染
    • <RegistrationLayout />已呈现
    • 排队的布局效果执行
      • prevRootPathname.current未定义,因此没有动画
      • 布局效果清理已在React中注册
  • 用户切换到“ / dashboard”,使用AnimRouteLayout触发rootPathname='/dashboard'的呈现
    • 由于rootPathname不同,因此第二种布局效果已排队
    • activeChildren仍未定义,因此children返回被渲染
    • <RegistrationLayout />卸载并渲染<DashboardLayout />
    • 执行前一个布局效果的清理
      • prevRootPathname.current设置为'/ registration'
      • activeChildren设置为先前的子项,导致另一个渲染排队等候
    • 排队的布局效果执行并启动动画
    • 由于状态AnimRouteLayout的变化,activeChildren的另一次渲染开始了
    • 由于rootPathname没什么不同,没有排队的另一种布局效果
    • activeChildren返回待渲染
    • <DashboardLayout />卸载
    • <RegistrationLayout />以新状态重新安装并呈现
    • 动画完成,并将activeChildren设置为未定义
    • AnimRouteLayout再次渲染,这次<DashboardLayout />将被渲染

尽管可以通过防止重新安装的方式来管理activeChildren,但我认为有一种更干净的方法可以解决此问题。我认为您最好冻结路径名,而不是冻结孩子。在编写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);
}

由于我没有尝试给出一个可行的示例,因此我无法保证上面没有语法错误,但是我相当有信心这种方法是正确的。