使用 .map 渲染组件会重置组件内的 setTimeout

Rendering components using .map resets setTimeout within components

我构建了一个 'repeater' 组件,它接受一个组件作为道具重复。有添加、删除和清除函数来添加或删除一个实例,或清除所有实例。

我正在尝试使用此转发器组件来呈现另一个内部具有 setTimeout 的组件。如果您添加或删除另一个组件,所有其他组件的 setTimouts 都会重置,因此它们会同时触发。

我正在使用的转发器组件:

class Repeater extends React.Component {
  state = {
    elements: [],
  };

  component = Notification; // For simplicity - actually dynamic from props
  count = 0;

  getKey = () => {
    return this.count++;
  };

  add = props => {
    const { elements } = this.state;    
    props.key = this.getKey();
    elements.push(props);

    this.setState({ elements });
  };

  remove = key => {
    const { elements } = this.state;
    const newElements = elements.filter(element => {
      return element.key !== key;
    });

    this.setState({ elements: newElements });
  };

  clear = () => {
    this.setState({ elements: [] });
  };

  render() {
    const { elements } = this.state;
    const Component = this.component;

    return (
      <React.Fragment>
        {elements.map(element => {
          return <Component key={element.key} {...element} />;
        })}
      </React.Fragment>
    );
  }
}

我正在渲染的组件:

export function Notification() {
  const {
    isOpen = true,
    message,
    title,
    duration = 4.5,
  } = props;
  const [openState, setOpenState] = React.useState(isOpen);
  let timer;

  const clearTimer = () => {
    window.clearTimeout(timer);
  };

 const handleClose = () => {
    clearTimer();
    console.log('Test');
  };

  const setTimer = () => {
    if (duration) timer = setTimeout(() => handleClose(), duration * 1000);
  };

  if (openState) {
    setTimer();
    return (
      <div
        onMouseEnter={() => clearTimer()}
        onMouseLeave={() => setTimer()}>
        <div>
          <Icon />
        </div>
        <div>{title}</div>
        <div>{message}</div>
      </div>
    );
  }
  return null;
}

在这种情况下使用 .map 函数是一个问题。您需要单独渲染每个组件,否则 map 函数将 运行 每次触发更新并将元素重置为初始状态。

我更新了解决方案以使用最新的 ES6+ 功能。从您想要实现的功能和您的代码的完整上下文来看,还有一些需要改进的地方,但是这里是第一次迭代,应该可以工作,因为 setState 函数引用了先前的状态。

import React, { useState, useEffect } from 'react';

export function Notification({ duration=4500, isOpen=true, message, title }) {
  const [open, setOpen] = useState(isOpen);

  let timer;

  useEffect(() => {
    setTimer();
  });

  const handleClose = () => {
    clearTimer();
  };

  const clearTimer = () => {
    window.clearTimeout(timer);
  };

  const setTimer = () => {
    timer = setTimeout(() => handleClose(), duration);
  };

  if (!open) return null;

  return (
    <div
      onMouseEnter={() => clearTimer()}
      onMouseLeave={() => setTimer()}>
      <div>
        <Icon />
      </div>
      <div>{title}</div>
      <div>{message}</div>
    </div>
  );
};

export function Repeater({ component }) {
  const [elements, setElements] = useState([]);
  const [count, setCount] = useState(0);

  const Component = component ? component : Notification;

  const getKey = () => {
    setCount(count => count + 1);
    return count;
  };

  const add = (props) => {
    setElements((elements) => elements.push({ ...props, key: getKey }));
  };

  const remove = (key) => {
    setElements((elements) => elements.filter((e) => e.key !== key));
  };

  const clear = () => {
    setElements([]);
  };

  const renderedElements = elements.map(({ key, ...rest }) => (
    <Component key={key} {...rest} />
  ));

  return (
    <>
      {renderedElements}
    </>
  );
};

我能够通过在第一次加载时将通知组件的呈现时间存储在状态中并在我的 setTimer 方法中找到它与当前时间之间的差异来解决这个问题。

...

const [renderTime] = React.useState(Date.now());

...

const setTimer = () => {
  if (duration) {
    const elapsed = Date.now() - openTime;
    const timeout = duration - elapsed;
    timer = setTimeout(() => handleClose(), timeout);
  }
};