使用 React 重新触发 css 动画

re-triggering css animations with react

我在 table 中有一个项目列表。由于某些外部事件,我希望某个项目的行短暂闪烁并突出显示。

我有一个 CSS 动画 fadingHighlight 来实现所需的视觉效果,当事件发生时,我在所需的行上设置 class last-updated触发动画。

但是,当同一行连续多次更新时,只有第一次更新会导致闪烁。 (我相信这是因为 React 将 last-updated class 保留在行上而不是重新渲染它,因此 css 不会重新启动动画。)

如果同一项目连续更新多次,如何重新触发动画?

演示和代码:https://codesandbox.io/s/pensive-lamarr-d71zh?file=/src/App.js

反应的相关部分:

const Item = ({ id, isLastUpdated }) => (
  <div className={isLastUpdated ? "last-updated" : ""}>Item {id}</div>
);

const App = () => {
  const [lastUpdatedId, setLastUpdatedId] = React.useState(undefined);

  const itemIds = ["1", "2", "3"];
  return (
    <div className="App">
      <h2>The list of items</h2>
      {itemIds.map((id) => (
        <Item key={id} id={id} isLastUpdated={lastUpdatedId === id} />
      ))}

      <h2>Trigger updates</h2>
      {itemIds.map((id) => (
        <button onClick={() => setLastUpdatedId(id)}>Update item {id}</button>
      ))}
    </div>
  );
}

样式:


@keyframes fadingHighlight {
  0% {
    background-color: #ff0;
  }
  100% {
    background-color: #fff;
  }
}

.last-updated {
  animation: fadingHighlight 2s;
}

这里我们反对不要更新不会改变的东西的React原则,我从来没有做过这样的事情,但是,它似乎解决了你的问题。

由于等待代码 运行 的反应行为,然后将结果与当前值进行比较,这将不起作用:

// Let suppose it is currently `1`.

setLastUpdatedId(undefined); // <-- it does not apply.
setLastUpdatedId(1);         // <-- it does not trigger any changes.

解决方案(某种程度上):

我使用 useEffect 和一个辅助状态 (waitAndHold) 来玩 useState & useEffect 的异步行为,而不是仅仅设置新值 -符合 setLastUpdatedId。所以我们可以重置为 undefined,等待更改生效,然后再次重置为与之前相同的值。

工作解决方案here.

App 中的新增内容:

const [waitAndHoldId, setWaitAndHoldId] = React.useState([]);

useEffect(() => {
  setLastUpdatedId(waitAndHoldId[0]);
}, [waitAndHoldId]);

function resetLastUpdatedId(id) {
  if (lastUpdatedId !== id) {
    // If it's different: just set it and we are done.
    setLastUpdatedId(id);
  } else {
    // If not:
    setWaitAndHoldId([id]);

    /**
     * Setting an array will always trigger the useEffect (even if
     * it remains the same). The useEffect will take an instant to
     * run. Meanwhile, force the `lastUpdatedId` to reset with a
     * different value (e.g.: undefined).
     */

    // This will run before the useEffect.
    setLastUpdatedId(undefined);
  }
}

// ...

<button onClick={() => resetLastUpdatedId(id)}>
  Update item {id}
</button>

更新

这个解决方案有效,但我从队友那里得到一些反馈,set... 函数通常不应在渲染循环中调用,而应该在 useEffect 中调用。当我进行更改时,演示仍然有效,但在我们的生产代码中却没有。

我的猜测是,依赖多个 useEffect 会导致对 React 的事件循环内部结构敏感,例如这些效果调用的顺序以及它们何时被折叠成一个渲染步骤。

躲避该问题导致 (这也涉及较少的代码。)

原回答

(and in particular the use of useEffect) works! And it led me to a revised updated solution (demo here) 具有一些理想的特性。

与Leo的回答相比,主要变化是每个Item组件现在负责独立管理高亮效果的状态,并且需要传递最近一次更新的时间。 (或者,更准确地说,它需要在每次更新时传递一些新值,但时间似乎是最自然的。)

这种方法的一个很好的结果是可以同时突出显示多个项目(例如,如果一个项目的更新在另一个项目的突出显示消失之前进入)。

const Item = ({ id, updateTime }) => {
  const [lastUpdateTime, setLastUpdateTime] = React.useState(undefined);
  const [showHighlight, setShowHighlight] = React.useState(false);
  const [triggerHighlight, setTriggerHighlight] = React.useState(false);

  if (updateTime && updateTime !== lastUpdateTime) {
    // Update time has changed! 
    setLastUpdateTime(updateTime);
    // Remove the highlighting class so that re-adding
    // it restarts the animation.
    setShowHighlight(false);
    // And record that we need to re-add the class (but don't do it yet).
    setTriggerHighlight(true);
  }

  useEffect(() => {
    if (triggerHighlight) {
      // Re-add the highlight class inside useEffect so 
      // that it will happen in a separate render step.
      setShowHighlight(true);
      setTriggerHighlight(false);
    }
  }, [triggerHighlight]);

  return <div className={showHighlight ? "updated" : ""}>Item {id}</div>;
};

我们需要应用级状态来跟踪最近的更新时间:

  const [updateTimes, setUpdateTimes] = React.useState({});

  // ...

        <Item key={id} id={id} updateTime={updateTimes[id]} />

  // ...
        <button
          onClick={() => {
            setUpdateTimes({ ...updateTimes, [id]: Date.now() });
          }}
        >

   // ...

(再次感谢 @leo for 帮助我完成了这个!)

我通过

得到了想要的行为(强制动画重新开始)
  1. 立即移除高亮class,并且
  2. 设置超时(10 毫秒似乎可以)以添加回高亮 class。

超时似乎强制做出反应以单独渲染没有突出显示 class 的组件,然后再次使用突出显示 class,导致动画重新启动。 (如果没有超时,React 可能会将这些更改合并为一个渲染步骤,导致 DOM 将整个事情视为空操作。)

每个 Item 管理自己的突出显示状态的这种方法的一个很好的结果是可以同时突出显示多个项目(例如,如果一个项目的更新在另一个项目的突出显示消失之前进入) .

Demo here

const Item = ({ id, updateTime }) => {
  const [showHighlight, setShowHighlight] = React.useState(false);

  // By putting `updateTime` in the dependency array of `useEffect,
  // we re-trigger the highlight every time `updateTime` changes.
  useEffect(() => {
    if (updateTime) {
      setShowHighlight(false);
      setTimeout(() => {
        setShowHighlight(true);
      }, 10);
    }
  }, [updateTime]);

  return <div className={showHighlight ? "updated" : ""}>Item {id}</div>;
};

const App = () => {
  // tracking the update times at the top level
  const [updateTimes, setUpdateTimes] = React.useState({});

  // ...
        <Item key={id} id={id} updateTime={updateTimes[id]} />

  // ...
        <button
          onClick={() => {
            setUpdateTimes({ ...updateTimes, [id]: Date.now() });
          }}
        >
   // ...
}