使用 useLayoutEffect 响应页面/布局转换
React page / layout transitions with useLayoutEffect
我定义了以下结构,以便根据路径名在布局之间设置动画转换。
<LayoutTransition pathname={pathname}>
{pathname.includes('/registration') && <RegistrationLayout />}
{pathname.includes('/dashboard') && <DashboardLayout />}
</LayoutTransition>
RegistrationLayout
和 DashboardLayout
内部结构相似,但它们根据路径名而不是布局显示不同的页面。
在我的 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 时看到我的 "current" children 然后当 activeChildren
设置为空时,我看到我的 "new" children 在 在 .
中设置动画时
唯一的问题是,当我设置 setActiveChildren(children);
布局 re-renders 时,我看到布局正在显示 returns 到其初始状态的闪烁和页面。
有没有办法避免这种情况并在我们制作动画时有点冻结 children,所以它们不会发生 re-render?
编辑:来自 react-native 项目的完整代码片段
核心思想是我们订阅路由器上下文,当 rootPathname 更改时,我们将当前布局 (children) 动画化,然后将新布局动画化。
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;
首先,我将通过列出当用户从“/registration”开始然后切换到“/dashboard”时发生的步骤来描述我认为您当前的代码正在发生的事情:
-
AnimRouteLayout
与 rootPathname='/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
,但我认为有一种更简洁的方法可以解决此问题。与其试图冻结 children,我认为你最好冻结 pathname。在编写 时,我对这些想法进行了大量试验。为了保持这一点,我想出的术语是区分:
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);
}
由于我没有尝试为此做一个工作示例,所以我不能保证我在上面没有语法错误,但我相当有信心这种方法是合理的。
我定义了以下结构,以便根据路径名在布局之间设置动画转换。
<LayoutTransition pathname={pathname}>
{pathname.includes('/registration') && <RegistrationLayout />}
{pathname.includes('/dashboard') && <DashboardLayout />}
</LayoutTransition>
RegistrationLayout
和 DashboardLayout
内部结构相似,但它们根据路径名而不是布局显示不同的页面。
在我的 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 时看到我的 "current" children 然后当 activeChildren
设置为空时,我看到我的 "new" children 在 在 .
唯一的问题是,当我设置 setActiveChildren(children);
布局 re-renders 时,我看到布局正在显示 returns 到其初始状态的闪烁和页面。
有没有办法避免这种情况并在我们制作动画时有点冻结 children,所以它们不会发生 re-render?
编辑:来自 react-native 项目的完整代码片段
核心思想是我们订阅路由器上下文,当 rootPathname 更改时,我们将当前布局 (children) 动画化,然后将新布局动画化。
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;
首先,我将通过列出当用户从“/registration”开始然后切换到“/dashboard”时发生的步骤来描述我认为您当前的代码正在发生的事情:
-
AnimRouteLayout
与rootPathname='/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
,但我认为有一种更简洁的方法可以解决此问题。与其试图冻结 children,我认为你最好冻结 pathname。在编写
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);
}
由于我没有尝试为此做一个工作示例,所以我不能保证我在上面没有语法错误,但我相当有信心这种方法是合理的。