Функция, вызываемая в setTimeout, не использует текущее состояние реакции

#javascript #reactjs #react-hooks

#javascript #reactjs #реагирующие крючки

Вопрос:

краткое изложение

Я пытаюсь создать кнопку, которая имеет как обычный щелчок, так и отдельное действие, которое происходит, когда пользователь нажимает и удерживает ее, аналогично кнопке «Назад» в Chrome.

То, как я это делаю, включает в себя setTimeout() обратный вызов, который проверяет наличие чего-либо в состоянии. По какой-то причине обратный вызов использует состояние с момента, когда setTimeout() был вызван, а не в то время, когда вызывается обратный вызов (1 секунда спустя).

Вы можете просмотреть его в codesandbox

как я пытаюсь этого добиться

Чтобы получить эту функцию, я вызываю setTimeOut() OnMouseDown. Я также установил значение isHolding, которое находится в состоянии, в true.

В Mouseup я устанавливаю значение isHolding равным false, а также запускаю clickHandler() , что является опорой, если функция удержания не успела быть вызвана.

Обратный вызов в setTimeOut() проверит, имеет ли значение isHolding значение true, и если это так, он будет запущен clickHoldHandler() , что является опорой.

проблема

Удержание находится в состоянии (я использую перехваты), но когда setTimeout() запускается обратный вызов, я возвращаю не текущее состояние, а то, какое состояние было при setTimetout() первом вызове.

мой код

Вот как я это делаю:

 const Button = ({ clickHandler, clickHoldHandler, children }) => {
  const [isHolding, setIsHolding] = useState(false);
  const [holdStartTime, setHoldStartTime] = useState(undefined);
  const holdTime = 1000;

  const clickHoldAction = e => {
    console.log(`is holding: ${isHolding}`);
    if (isHolding) {
      clickHoldHandler(e);
    }
  };

  const onMouseDown = e => {
    setIsHolding(true);
    setHoldStartTime(new Date().getTime());

    setTimeout(() => {
      clickHoldAction(e);
    }, holdTime);
  };

  const onMouseUp = e => {
    setIsHolding(false);

    const totalHoldTime = new Date().getTime() - holdStartTime;
    if (totalHoldTime < holdTime || !clickHoldHandler) {
      clickHandler(e);
    }
  };

  const cancelHold = () => {
    setIsHolding(false);
  };

  return (
    <button
      onMouseDown={onMouseDown}
      onMouseUp={onMouseUp}
      onMouseLeave={cancelHold}
    >
      {children}
    </button>
  );
};
  

Комментарии:

1. Всякий раз, когда компонент перезапускается, все функции внутри воссоздаются. setTimeout содержит ссылку на «старую» функцию, которая содержит ссылку на «старое» состояние.

2. Привет, Зак, я бы предложил взглянуть на эту статью, в которой объясняется разница между функциональными компонентами и компонентами класса и то, как они фиксируют состояние. overreacted.io /…

Ответ №1:

Вы должны обернуть эту задачу обратного вызова в редуктор и запустить тайм-аут в качестве эффекта. Да, это, безусловно, усложняет ситуацию (но это «лучшая практика»):

   const Button = ({ clickHandler, clickHoldHandler, children }) => {
     const holdTime = 1000;
     const [holding, pointer] = useReducer((state, action) => {
        if(action === "down") 
           return { holding: true, time: Date.now()  };
        if(action === "up") {
          if(!state.holding)
              return { holding: false };
          if(state.time   holdTime > Date.now()) {
                clickHandler();
          } else {
                clickHoldHandler();
          }
          return { holding: false };
        }
        if(action === "leave")
          return { holding: false };
     }, { holding: false, time: 0 });

     useEffect(() => {
       if(holding.holding) {
         const timer = setTimeout(() => pointer("up"), holdTime - Date.now()   holding.time);
         return () => clearTimeout(timer);
       }
     }, [holding]);

     return (
       <button
         onMouseDown={() => pointer("down")}
         onMouseUp={() => pointer("up")}
         onMouseLeave={() => pointer("leave")}
       >
         {children}
        </button>
    );
  };
  

рабочая изолированная среда:https://codesandbox.io/s/7yn9xmx15j


В качестве запасного варианта, если редуктор становится слишком сложным, вы можете запомнить объект настроек (не лучшая практика):

  const state = useMemo({
   isHolding: false,
   holdStartTime: undefined,
 }, []);

 // somewhere
 state.isHolding = true;
  

Комментарии:

1. @zach извиняется за беспорядок, мне определенно нужно поспать сейчас… и спасибо за редактирование 🙂

2. нет проблем, вы проделали 99% пути. Я просто поменял местами две функции. 🙂