setTimeout в React неявно выводит числа в DOM - PullRequest
0 голосов
/ 25 февраля 2020

Я пытаюсь реализовать анимацию отложенного набора в React, которая при запуске удаляет текст-заполнитель. Моя попытка сделать это состояла в том, чтобы установить состояние после истечения времени ожидания, а затем визуализировать анимацию и удалить заполнитель, когда состояние истинно.

Однако использование setTimeout выводит некоторые «случайные» числа в свой контейнер, и я не смог выяснить, почему - я предполагаю, что отображаемые числа - это время в миллисекундах для тайм-аута, они меняются только несколько раз перед остановкой.

Вывод можно увидеть здесь:

enter image description here

И пример всего компонента можно увидеть здесь:

enter image description here

По сути, я пытаюсь анимировать переписку в чате, и мне нужно сделать div похожим на поле ввода. У div есть текст-заполнитель по умолчанию, который должен быть удален через xxxx миллисекунд, после чего текст Typist отображается с анимацией ввода.

Компонент Chat, показанный ниже, использует числовое состояние, а также функцию для увеличения число. Числовое состояние используется для определения того, какие пузыри чата уже были отрисованы, поскольку пузыри имеют обратный вызов анимации, в котором изменяется состояние - чтобы гарантировать, что следующий пузырь чата не начнет анимироваться, пока предыдущий не будет полностью сделанный.

Проблема заключается в том, что при визуализации «поля ввода» мне требуется тайм-аут, поскольку пользователь должен увидеть заполнитель за пару секунд до запуска анимации набора текста в Typist.

Chat.jsx

import React, { useEffect, useRef, useState } from 'react';
import ChatBubble from './ChatBubble/ChatBubble';
import classes from './Chat.module.css';
import ScrollAnimation from 'react-animate-on-scroll';
import Typist from 'react-typist';

const Chat = () => {
  const [state, setState] = useState(0);

  const [showInputText, setShowInputText] = useState(false);

  const choices = [{ text: 'Under 2 år siden' }, { text: 'Over 2 år siden' }];

  const choices2 = [{ text: 'Ja' }, { text: 'Nej' }];

  const typistCursor = {
    hideWhenDone: true,
    hideWhenDoneDelay: 200
  };

  let inputText = <Typist cursor={typistCursor}>test@mail.com</Typist>;
  if(state >= 6) {
    setTimeout(() => {
      inputText = <div className={classes.InputText}>Indtast din email her...</div>
    }, 1000)
  }

  const inputText = <Typist cursor={typistCursor}>test@mail.com</Typist>;

  const renderNextBubble = () => {
    const newState = state + 1;
    setState(newState);
    console.log('test state', state);
  };

  return (
    <div className={classes.chatWrapper}>

      <ChatBubble
        isReply={false}
        animationDelay={0}
        animationCallback={renderNextBubble}
        chatChoices={choices}
      >
        <p>Hvornår købte du din vare?</p>
      </ChatBubble>

      {state >= 1 ? (
        <ChatBubble
          isReply={true}
          animationDelay={0}
          animationCallback={renderNextBubble}
        >
          Under 2 år siden
        </ChatBubble>
      ) : null}

      {state >= 2 ? (
        <ChatBubble
          isReply={false}
          animationDelay={0}
          animationCallback={renderNextBubble}
          chatChoices={choices2}
        >
          <p>Er det under 6 måneder siden at du bestilte/modtog dit køb?</p>
        </ChatBubble>
      ) : null}

      {state >= 3 ? (
        <ScrollAnimation
          animateIn="fadeIn"
          duration={0.5}
          delay={-0.25}
          animateOnce={true}
          afterAnimatedIn={renderNextBubble}
        >
          <div className={classes.DotContainer}>
            <div className={classes.Dot}></div>
          </div>
        </ScrollAnimation>
      ) : null}
      {state >= 4 ? (
        <ScrollAnimation
          animateIn="fadeIn"
          duration={0.5}
          delay={-0.25}
          animateOnce={true}
          afterAnimatedIn={renderNextBubble}
        >
          <div className={classes.DotContainer}>
            <div className={classes.Dot}></div>
          </div>
        </ScrollAnimation>
      ) : null}
      {state >= 5 ? (
        <ScrollAnimation
          animateIn="fadeIn"
          duration={0.5}
          delay={-0.25}
          animateOnce={true}
          afterAnimatedIn={renderNextBubble}
        >
          <div className={classes.DotContainer}>
            <div className={classes.Dot}></div>
          </div>
        </ScrollAnimation>
      ) : null}

      {state >= 6 ? (
        <>
          <ChatBubble
            isReply={false}
            animationDelay={0}
            animationCallback={renderNextBubble}
          >
            <p style={{ fontWeight: 'bold' }}>Du er næsten færdig</p>
            <p>
              Skriv din email nedenunder, så har vi en mulighed for at sende
              klagen til dig
            </p>
            <p style={{ fontWeight: 'bold' }}>
              Dobbelttjek at du har skrevet den rigtige mail!
            </p>
          </ChatBubble>
          <div className={classes.EmailInput}>
            {setTimeout(() => {
              console.log('executing timeout');
              setShowInputText(true);
            }, 1000)}
            {showInputText ? (
              inputText
            ) : (
              <div className={classes.InputText}>Indtast din email her...</div>
            )}
          </div>
        </>
      ) : null}
    </div>
  );
};

export default Chat;

ChatBubble.jsx

import React from 'react';
import classes from './ChatBubble.module.css';
import Typist from 'react-typist';
import ChatChoices from '../ChatChoices/ChatChoices';
import ScrollAnimation from 'react-animate-on-scroll';

const chatBubble = (props) => {
  const { isReply, animationDelay, animationCallback, chatChoices } = props;
  let text = props.children;

  const typistCursor = {
    hideWhenDone: true,
    hideWhenDoneDelay: 200
  };

  if (props.typist) {
    text = (
      <Typist cursor={typistCursor}>
        <Typist.Delay ms={600} />
        {props.children}
      </Typist>
    );
  }

  return (
    <ScrollAnimation
      animateIn="fadeIn"
      duration={1}
      delay={animationDelay}
      animateOnce={true}
      afterAnimatedIn={animationCallback}
    >
      <div
        className={`${classes.chatLine} ${
          isReply ? classes.chatLineWhite : classes.chatLineBlue
        }`}
      >
        <div
          className={`${
            isReply ? classes.chatBubbleBlue : classes.chatBubbleWhite
          } ${classes.chatBubble}`}
        >
          <div>{text}</div>
        </div>
      </div>
      {chatChoices ? <ChatChoices choices={chatChoices} /> : null}
    </ScrollAnimation>
  );
};

export default chatBubble;

ChatChoices.jsx

import React from 'react';
import classes from './ChatChoices.module.css';

const chatChoices = ({ choices }) => {
  return (
    <div className={classes.chatLine}>
      <div className={classes.wrapper}>
        <p>VÆLG EN MULIGHED</p>
        <div className={classes.choicesWrapper}>
          {choices
            ? choices.map((choice) => (
                <div key={choice.text} className={classes.choice}>
                  {choice.text}
                </div>
              ))
            : null}
        </div>
      </div>
    </div>
  );
};

export default chatChoices;

1 Ответ

3 голосов
/ 25 февраля 2020

В JSX {...} выводит результат выражения внутри него. (Вы полагаетесь на это в другом месте, например className={classes.InputText}.) Вы оцениваете setTimeout в {}, который возвращает дескриптор таймера, который является числом.

Вы не должны быть используя setTimeout в вашем JSX вообще. Вместо этого просто запустите его в теле вашего компонента, , если , вы действительно хотите, чтобы он запускался каждый раз, когда ваш компонент отображается:

const Chat = () => {

  const [showInputText, setShowInputText] = useState(false)

  const typistCursor = {
    hideWhenDone: true,
    hideWhenDoneDelay: 200,
  }

  const inputText = (<Typist cursor={typistCursor}>test@mail.com</Typist>)

  // *** Moved
  setTimeout(() => {
    console.log('executing timeout');
    setShowInputText(true);
  }, 1000)
  // ***

  return (
    <div className={classes.EmailInput}>
      {showInputText ? (inputText) : (<div className={classes.InputText}>Indtast din email her...</div>)}
    </div>
  )
}

Live Пример:

const { useState } = React;

const classes = {
    InputText: {
        color: "green"
    }
};

const Chat = () => {

  const [showInputText, setShowInputText] = useState(false)

  const typistCursor = {
    hideWhenDone: true,
    hideWhenDoneDelay: 200,
  }

  // *** Replaced Typist here just for demo purposes
  const inputText = (<div>test@mail.com</div>)

  // *** Moved
  setTimeout(() => {
    console.log('executing timeout');
    setShowInputText(true);
  }, 1000)
  // ***

  return (
    <div className={classes.EmailInput}>
      {showInputText ? (inputText) : (<div className={classes.InputText}>Indtast din email her...</div>)}
    </div>
  )
}

ReactDOM.render(<Chat />, document.getElementById("root"));
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.11.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.11.0/umd/react-dom.production.min.js"></script>

Но , учтите, что если сделать setTimeout безусловным, вы будете продолжать делать это снова и снова, даже если showInputText уже true. Если вы хотите сделать это только тогда, когда оно false, добавьте ветку:

const Chat = () => {

  const [showInputText, setShowInputText] = useState(false)

  const typistCursor = {
    hideWhenDone: true,
    hideWhenDoneDelay: 200,
  }

  const inputText = (<Typist cursor={typistCursor}>test@mail.com</Typist>)

  // *** Added `if`
  if (!showInputText) {
    // *** Moved
    setTimeout(() => {
      console.log('executing timeout');
      setShowInputText(true);
    }, 1000)
    // ***
  }

  return (
    <div className={classes.EmailInput}>
      {showInputText ? (inputText) : (<div className={classes.InputText}>Indtast din email her...</div>)}
    </div>
  )
}

Live Пример:

const { useState } = React;

const classes = {
    InputText: {
        color: "green"
    }
};

const Chat = () => {

  const [showInputText, setShowInputText] = useState(false)

  const typistCursor = {
    hideWhenDone: true,
    hideWhenDoneDelay: 200,
  }

  // *** Replaced Typist here just for demo purposes
  const inputText = (<div>test@mail.com</div>)

  // *** Added `if`
  if (!showInputText) {
    // *** Moved
    setTimeout(() => {
      console.log('executing timeout');
      setShowInputText(true);
    }, 1000)
    // ***
  }
  
  return (
    <div className={classes.EmailInput}>
      {showInputText ? (inputText) : (<div className={classes.InputText}>Indtast din email her...</div>)}
    </div>
  )
}

ReactDOM.render(<Chat />, document.getElementById("root"));
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.11.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.11.0/umd/react-dom.production.min.js"></script>

В комментарии вы сказали, что беспокоитесь о времени ожидания, которое начинается до показа компонента, и о том, что время ожидания должно начинаться только тогда, когда state >= 6. Для этого используйте обратный вызов useEffect с stateshowInputText) в качестве зависимостей и установите таймер, если !showInputText && state >= 6:

// *** `useEffect` depending on `state` and `showInputText`
useEffect(() => {
  // You'll see this console log every time the component is rendered
  // with an updated `showInputText` or `state`
  console.log("useEffect callback called");
  // *** Added `if`
  if (!showInputText && state >= 6) {
    console.log("Setting timer");
    // *** Moved
    setTimeout(() => {
      // You'll only see this one when `showInputText` was falsy when
      // the `useEffect` callback was called just after rendering
      console.log('executing timeout');
      setShowInputText(true);
    }, 1000)
    // ***
  }
}, [showInputText, state]);

Live Пример:

const { useState, useEffect } = React;

const classes = {
    InputText: {
        color: "green"
    }
};

const Chat = () => {

  const [state, setState] = useState(0);
  const [showInputText, setShowInputText] = useState(false)

  const typistCursor = {
    hideWhenDone: true,
    hideWhenDoneDelay: 200,
  }

  // *** Replaced Typist here just for demo purposes
  const inputText = (<div>test@mail.com</div>)

  // *** `useEffect` depending on `state` and `showInputText`
  useEffect(() => {
    // You'll see this console log every time the component is rendered
    // with an updated `showInputText` or `state`
    console.log("useEffect callback called");
    // *** Added `if`
    if (!showInputText && state >= 6) {
      console.log("Setting timer");
      // *** Moved
      setTimeout(() => {
        // You'll only see this one when `showInputText` was falsy when
        // the `useEffect` callback was called just after rendering
        console.log('executing timeout');
        setShowInputText(true);
      }, 1000)
      // ***
    }
  }, [showInputText, state]);
  
  return (
    <div className={classes.EmailInput}>
      {showInputText ? (inputText) : (<div className={classes.InputText}>Indtast din email her...</div>)}
      <input type="button" onClick={
          /* Just a really quick and dirty button to let us increment `state` */
          () => setState(s => s + 1)
          } value={`State: ${state} - Increment`} />
    </div>
  )
}

ReactDOM.render(<Chat />, document.getElementById("root"));
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.11.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.11.0/umd/react-dom.production.min.js"></script>

Наконец, если ваш компонент может быть перерисован по какой-то другой причине, чем вызов setShowInputText(true) выше, вы можете захотеть отмените таймер, чтобы избежать устаревших вызовов, с помощью функции очистки в useEffect hook:

// *** `useEffect` depending on `state` and `showInputText`
useEffect(() => {
  // You'll see this console log every time the component is rendered
  // with an updated `showInputText` or `state`
  console.log("useEffect callback called");
  // *** Added `if`
  if (!showInputText && state >= 6) {
    console.log("Setting timer");
    // *** Moved
    const timer = setTimeout(() => {
      // You'll only see this one when `showInputText` was falsy when
      // the `useEffect` callback was called just after rendering
      console.log('executing timeout');
      setShowInputText(true);
    }, 1000)
    // ***
    // *** This is the cleanup function. It's a no-op if the timer has
    // already fired; if the timer hasn't fired, it prevents it firing
    // twice.
    return () => clearTimeout(timer);
  }
}, [showInputText, state]);

Live Пример:

const { useState, useEffect } = React;

const classes = {
    InputText: {
        color: "green"
    }
};

const Chat = () => {

  const [state, setState] = useState(0);
  const [showInputText, setShowInputText] = useState(false)

  const typistCursor = {
    hideWhenDone: true,
    hideWhenDoneDelay: 200,
  }

  // *** Replaced Typist here just for demo purposes
  const inputText = (<div>test@mail.com</div>)

  // *** `useEffect` depending on `state` and `showInputText`
  useEffect(() => {
    // You'll see this console log every time the component is rendered
    // with an updated `showInputText` or `state`
    console.log("useEffect callback called");
    // *** Added `if`
    if (!showInputText && state >= 6) {
      // *** Moved
      console.log("Setting timer");
      const timer = setTimeout(() => {
        // You'll only see this one when `showInputText` was falsy when
        // the `useEffect` callback was called just after rendering
        console.log('executing timeout');
        setShowInputText(true);
      }, 1000)
      // ***
      // *** This is the cleanup function. It's a no-op if the timer has
      // already fired; if the timer hasn't fired, it prevents it firing
      // twice.
      return () => {
        console.log("Clearing timer");
        clearTimeout(timer);
      };
    }
  }, [showInputText, state]);
  
  return (
    <div className={classes.EmailInput}>
      {showInputText ? (inputText) : (<div className={classes.InputText}>Indtast din email her...</div>)}
      <input type="button" onClick={
          /* Just a really quick and dirty button to let us increment `state` */
          () => setState(s => s + 1)
          } value={`State: ${state} - Increment`} />
    </div>
  )
}

ReactDOM.render(<Chat />, document.getElementById("root"));
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.11.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.11.0/umd/react-dom.production.min.js"></script>
...