react 生命挂钩_动画列表使用React挂钩重新排序

react 生命挂钩

By Tara Ojo

塔拉·奥乔(Tara Ojo)

My team recently worked on a feature to give users shortcuts to topics they follow in the FT’s App after we trialled it as an AB test and got successful results.

我的团队最近开发了一项功能,为用户提供了在FT应用程序中遵循的主题的快捷方式,我们将其作为AB测试进行了试用并获得了成功

When we got new data from the API we wanted to have a sleek animation from the old order of topics, into the new refreshed order. We started experimenting with the feasibility of a few different animations, including one where each bubble would smoothly slide into its new position when we got its new order from the API — influenced by the Instagram story styled bubble component.

当我们从API获取新数据时,我们希望从主题的旧顺序到新的刷新顺序具有流畅的动画效果。 我们开始尝试一些不同动画的可行性,其中包括一个动画,当我们从API获得新订单时,每个气泡将平滑滑入新位置-受Instagram故事样式气泡组件的影响。

While it can be straightforward to do a whole load of animations and transitions with CSS, it took me a while to find an example of animating the reordering of list items, especially with React. Since I’ve also started to get used to the concepts of React Hooks I wanted to use them to implement this animation too.

虽然使用CSS可以完成整个动画和过渡的工作很简单,但是我花了一些时间才找到一个示例,以动画方式对列表项进行重新排序,尤其是使用React。 由于我也开始习惯了React Hooks的概念,因此我也想用它们来实现此动画。

I found this difficult to do using React hooks because my component would automatically rerender, in its new order, when it got new data. I was trying to hook into the moment before rerendering to smoothly transition from one state to another. Without the componentWillReceiveProps function call from the class components, this was hard to do.

我发现使用React挂钩很难做到这一点,因为当我的组件获得新数据时,它会以其新顺序自动重新呈现。 我试图陷入重新渲染以从一种状态平稳过渡到另一种状态之前的那一刻。 没有类组件的componentWillReceiveProps函数调用,这很难做到。

I was under the (incorrect) assumption that there would be loads of React hooks examples out in the wild. I honestly just wanted a copypasta solution that I wouldn’t have to tweak too much 👀. I also didn’t want to bring in some huge, usually overly flexible package to reorder one small thing. I did come across a great post by Joshua Comeau (linked below). It explains how to do exactly what I needed, but with class components. With React hooks I needed to re-think some of the concepts to get it to work, but I’ve based the majority of this work on that post.

我是在(不正确的)假设下,在野外会有大量的React钩子示例。 老实说,我只是想要一个不需要过多调整的copypasta解决方案。 我也不想引入一些巨大的,通常过于灵活的包装来对一件小东西重新排序。 我的确遇到过Joshua Comeau的精彩文章(链接如下)。 它说明了如何使用类组件来完全执行我需要的操作。 使用React钩子,我需要重新考虑一些概念以使其正常工作,但是我大部分工作都是基于该帖子。

What we want to happen:

我们想要发生的事情:

  1. Keep an eye out for when our element list is going to change

    请随时注意我们的元素列表何时更改
  2. When it changes we want to calculate the previous positions and the new positions of each element in the list before the DOM updates

    当它更改时,我们想在DOM更新之前计算列表中每个元素的先前位置和新位置
  3. Also before the DOM updates with the new order of the list we want to “pause” the update and show a smooth transition of each item in the list from its old position to its new position

    同样,在DOM以列表的新顺序更新之前,我们要“暂停”更新,并显示列表中每个项目从旧位置到新位置的平滑过渡
Two bubbles with arrows showing that they will swap position

Let’s start with a parent component that just renders the children that is passed into it, AnimateBubbles:

让我们从一个父组件开始,该组件仅渲染传递给它的子代AnimateBubbles

import React from "react";


const AnimateBubbles = ({ children }) => {
  return children;
};


export default AnimateBubbles;

Then we can use that component by rendering our items inside of it. In my case I’ve created a Bubble component that adds the styles to make each image a circle, the full code is here. The Bubble component also forwards the ref onto the DOM element. This is important as we can use the ref to find where the element is rendered in the DOM, then we can calculate its position. Another important prop is the key, this is not only needed for React when mapping over elements, but we can also use later to uniquely identify each item and match its old and new positions in the DOM.

然后,我们可以通过在其中渲染项目来使用该组件。 以我为例,我创建了一个Bubble组件,该组件添加了样式以使每个图像变成一个圆形, 完整代码在此处Bubble组件还将ref转发到DOM元素上。 这很重要,因为我们可以使用ref查找元素在DOM中的呈现位置,然后可以计算其位置。 另一个重要的Struts是key ,这不仅是React在元素上进行映射时所必需的,而且以后我们还可以使用它来唯一地标识每个项目并匹配其在DOM中的新旧位置。

import React, { useState, createRef } from "react";
import Bubble from "./Bubble";
import AnimateBubbles from "./AnimateBubbles";
import initialImages from "./initialImages";


export default function App() {
  const [images, setImages] = useState(initialImages);
  return (
    <div>
      <AnimateBubbles>
        {images.map(({ id, text }) => (
          <Bubble key={id} id={id} text={text} ref={createRef()} />
        ))}
      </AnimateBubbles>
    </div>
  );
}

Now that we have the foundations of our components we can start building out the logic of our AnimateBubbles component.

既然我们有了组件的基础,我们就可以开始构建AnimateBubbles组件的逻辑。

密切注意React Rerenders (Keeping an eye out for React rerenders)

With React hooks, we no longer have access to lifecycle methods like componentWillReceiveProps or componentDidUpdate, instead it’s all about effects. If we want to do something when a prop changes we can do the work inside of a useEffect. The useEffect hook tells React that our component needs to do something after it renders. In our case we only want to do any work if our list changes and the new order is rendered. Adding children as a dependency allows us to do that.

使用React钩子,我们不再可以访问诸如componentWillReceivePropscomponentDidUpdate之类的生命周期方法,而是全部与效果有关。 如果我们想在道具更改时做一些事情,我们可以在useEffect内完成工作。 useEffect挂钩告诉React我们的组件在渲染后需要做一些事情。 在我们的情况下,我们只希望在列表更改并且呈现新订单时做任何工作。 将子级添加为依赖项可以使我们做到这一点。

const AnimateBubbles = ({ children }) => {
  useEffect(() => {
    // do some layout-y stuff
  }, [children]);


  return children;
};

测量DOM中的每个位置 (Measure each position in the the DOM)

To calculate the position of each child in the DOM whenever the children prop changes, we can use getBoundingClientRect. I created a separate helper function to do this:

要在子项属性发生变化时计算每个子项在DOM中的位置,我们可以使用getBoundingClientRect 。 我创建了一个单独的辅助函数来执行此操作:

import React from "react";


const calculateBoundingBoxes = children => {
  const boundingBoxes = {};


  React.Children.forEach(children, child => {
    const domNode = child.ref.current;
    const nodeBoundingBox = domNode.getBoundingClientRect();


    boundingBoxes[child.key] = nodeBoundingBox;
  });


  return boundingBoxes;
};


export default calculateBoundingBoxes;

In this function we pass in children as an argument and use the forEach function on React.Children to iterate over them, getting the measurements of each item in an object that we can later store in state. This is where setting a key on each child is important as we store each box with its key as the object key so we can match up the old position with the new position later. It is also why creating a ref for each child is important, as we use that to find the element in the DOM and measure the bounding box for it. Now, when we call this function inside of the useEffect we will get the bounding box for each child updated every rerender 🎉.

在此函数中,我们传入子项作为参数,并在React.Children上使用forEach函数对其进行迭代,以获取对象中每个项目的度量,然后将其存储在状态中。 这是在每个子项上设置键的重要位置,因为我们将每个框的键作为对象键进行存储,以便以后可以将旧位置与新位置进行匹配。 这也是为什么为每个孩子创建一个引用很重要的原因,因为我们使用它来在DOM中查找元素并测量其边界框。 现在,当我们在useEffect内部调用此函数时,我们将获得每个rerender updated更新的每个子元素的边界框。

const AnimateBubbles = ({ children }) => {
  const [boundingBox, setBoundingBox] = useState({});


  useEffect(() => {
    const newBoundingBox = calculateBoundingBoxes(children);
    setBoundingBox(newBoundingBox);
  }, [children]);


  return children;
};

The problem now is we’re only getting the new positions, but we also need the old positions so we can do the slide animation from old position to new position.

现在的问题是,我们仅获得新职位,但我们也需要旧职位,因此我们可以将幻灯片动画从旧职位转换为新职位。

使用usePrevious钩子获取先前的状态/道具 (Getting the previous state/props with the usePrevious hook)

One way in which we can get the old positions of the children is by keeping track of the previous state of the children. The React docs already suggest a hook for this called usePrevious. They say that it may be provided out of the box in the future since it is considered a common use case. Using usePrevious means we can do the exact same thing to measure the bounding box of the old positions as we do for the new positions.

我们可以获得孩子的旧职位的一种方法是跟踪孩子的先前状态。 React文档已经建议为此使用一个钩子usePrevious 。 他们说,由于它被认为是常见的用例,因此将来可能会立即提供。 使用usePrevious意味着我们可以像测量新位置那样完全相同地测量旧位置的边界框。

const AnimateBubbles = ({ children }) => {
  const [boundingBox, setBoundingBox] = useState({});
  const [prevBoundingBox, setPrevBoundingBox] = useState({});
  const prevChildren = usePrevious(children);


  useEffect(() => {
    const newBoundingBox = calculateBoundingBoxes(children);
    setBoundingBox(newBoundingBox);
  }, [children]);


  useEffect(() => {
    const prevBoundingBox = calculateBoundingBoxes(prevChildren);
    setPrevBoundingBox(prevBoundingBox);
  }, [prevChildren]);


  return children;
};

I’ve put this into a separate useEffect as they don’t need to be done together and they both have different dependencies. These previous positions will now be recalculated every time children change.

我将其放在单独的useEffect中,因为它们不需要一起完成,并且它们都有不同的依赖项。 现在,每次孩子更换时,将重新计算这些先前的位置。

Now we have these two important pieces of information, we can move on to the actual transition 😅

现在我们有了这两个重要的信息,我们可以继续进行实际的过渡😅

采取行动 (Making moves)

When making the actual animation I looked more into FLIP as was talked about in Joshua Comeau’s post. FLIP stands for First, Last, Invert, Play, coined by Paul Lewis as a principle for rendering more performant animations.

制作实际动画时,我对Joshua Comeau帖子中谈到的FLIP有更多了解。 FLIP代表保罗·刘易斯(Paul Lewis)创造的First,Last,Invert,Play,它是渲染更高性能动画的原理。

So in our case, we find the first position of each child. We have this stored in state in prevBoundingBoxes. Then we find the last position of each child. We also have this stored in state in boundingBoxes. The next step is to invert, which is to figure out how each child has changed and apply those transformations to each child so it looks like it is in its first position.

因此,在本例中,我们找到了每个孩子的第一个位置。 我们将此状态存储在prevBoundingBoxes中 。 然后我们找到每个孩子的最后位置。 我们也将此状态存储在boundingBoxes中 。 下一步是反转,即弄清楚每个孩子的变化方式,并将这些转换应用于每个孩子,使其看起来像处于第一个位置。

We can set up a new useEffect to do this with dependencies on children, boundingBoxes and prevBoundingBoxes as we’ll use all of those values in the effect. Remember when React rerenders, it instantly updates the view with the new state, but we can use requestAnimationFrame to tell the browser that we want to perform an animation. The browser will call the function you give it before the next repaint.

我们可以建立一个新的useEffect对儿童 ,boundingBoxesprevBoundingBoxes,我们将使用所有这些值的效果依赖做到这一点。 请记住,当React重新渲染时,它会立即以新状态更新视图,但是我们可以使用requestAnimationFrame告诉浏览器我们要执行动画。 浏览器将在下一次重画之前调用您提供的功能。

const AnimateBubbles = ({ children }) => {
  // calculate bounding boxes from previous examples code here


  useEffect(() => {
    const hasPrevBoundingBox = Object.keys(prevBoundingBox).length;


    if (hasPrevBoundingBox) {
      React.Children.forEach(children, child => {
        const domNode = child.ref.current;
        const firstBox = prevBoundingBox[child.key];
        const lastBox = boundingBox[child.key];
        const changeInX = firstBox.left - lastBox.left;


        if (changeInX) {
          requestAnimationFrame(() => {
            // Before the DOM paints, invert child to old position
            domNode.style.transform = `translateX(${changeInX}px)`;
            domNode.style.transition = "transform 0s";
          });
        }
      });
    }
  }, [boundingBox, prevBoundingBox, children]);


  return children;
};

This we can do with first — last = inverted-value on the left values of the box. Since we also have a reference to the DOM node, we can apply the transform straight onto the node with a transition of 0 seconds so it inverts instantly.

我们可以使用框的左侧值的first – last =倒数值来做到这一点。 由于我们也有对DOM节点的引用,因此我们可以将转换直接以0秒的过渡时间应用到该节点上,以便立即进行转换。

The final step is to play the animation. To do this we wait for the child elements to be inverted, then we remove the transform and apply a smooth transition. The elements then slide into their new positions 💃🏾💃🏾💃🏾.

最后一步是播放动画。 为此,我们等待子元素被反转,然后删除变换并应用平滑过渡。 元素然后滑入其新位置💃🏾💃🏾💃🏾。

const AnimateBubbles = ({ children }) => {
  // calculate bounding boxes from previous examples code here


  useEffect(() => {
    const hasPrevBoundingBox = Object.keys(prevBoundingBox).length;


    if (hasPrevBoundingBox) {
      React.Children.forEach(children, child => {
        const domNode = child.ref.current;
        const firstBox = prevBoundingBox[child.key];
        const lastBox = boundingBox[child.key];
        const changeInX = firstBox.left - lastBox.left;


        if (changeInX) {
          requestAnimationFrame(() => {
            // Before the DOM paints, invert child to old position
            domNode.style.transform = `translateX(${changeInX}px)`;
            domNode.style.transition = "transform 0s";


            requestAnimationFrame(() => {
              // After the previous frame, remove
              // the transistion to play the animation
              domNode.style.transform = "";
              domNode.style.transition = "transform 500ms";
            });
          });
        }
      });
    }
  }, [boundingBox, prevBoundingBox, children]);


  return children;
};

消除故障 (Remove the glitch)

While I started work with useEffect I found that it was looking super glitchy, then after digging around in the React docs I found this tip:

当我开始使用useEffect时,我发现它看起来超级毛刺,然后在React文档中进行挖掘之后,我发现了这个提示:

“Unlike componentDidMount or componentDidUpdate, effects scheduled with useEffect don’t block the browser from updating the screen. This makes your app feel more responsive. The majority of effects don’t need to happen synchronously. In the uncommon cases where they do (such as measuring the layout), there is a separate useLayoutEffect Hook with an API identical to useEffect.” — React docs

“与componentDidMount或componentDidUpdate不同,使用useEffect安排的效果不会阻止浏览器更新屏幕。 这使您的应用程序感觉更灵敏。 大多数效果不需要同步发生。 在不常见的情况下(例如测量布局),会有一个单独的useLayoutEffect挂钩,其API与useEffect相同。” React文档

Since our aim is to measure the layout of elements in the DOM, what we actually need is useLayoutEffect. As noted, useLayoutEffect has an identical API to useEffect so I could easily switch out one for the other and it made the whole animation look super smooth 😎

由于我们的目的是测量DOM中元素的布局,因此我们实际需要的是useLayoutEffect 。 如前所述,useLayoutEffect具有相同的API来useEffect所以我可以很容易地切换出一个为其他,它使整个动画的外观超光滑😎

上线? (Going Live?)

While this is technically pretty cool, it’s not actually live 👀. While working with our designer we decided on a simpler animation that would avoid the bubble text crossing over other bubble text during the transition. We opted for a fade out and in animation instead which meant we got the smarter aesthetic we were looking for.

尽管从技术上讲这很酷,但实际上并不活。 在与我们的设计师合作时,我们决定使用一种更简单的动画,该动画可以避免过渡期间气泡文本与其他气泡文本交叉。 我们选择了淡入淡出和动画效果,这意味着我们得到了我们所寻找的更智能的美学。

附加功能 (Extras)

翻译自: https://medium.com/ft-product-technology/animating-list-reordering-with-react-hooks-1aa0d78a24dc

react 生命挂钩

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值