首页 > 解决方案 > React 中的 setTimeout 隐式输出数字到 DOM

问题描述

我正在尝试在 React 中实现延迟输入动画,该动画在启动时会删除占位符文本。我这样做的尝试是在超时后设置状态,然后在状态为真时渲染动画并删除占位符。

但是,使用 setTimeout 在其容器中输出一些“随机”数字,我无法弄清楚原因 - 我假设呈现的数字是以毫秒为单位的超时时间,它们在停止前只更改了几次。

输出可以在这里看到:

在此处输入图像描述

整个组件的示例可以在这里看到:

在此处输入图像描述

本质上,我正在尝试为聊天通信制作动画,并且需要渲染一个看起来像输入字段的 div。div 有一个默认的占位符文本,需要在 xxxx 毫秒后删除,然后呈现 Typist 文本以显示打字动画。

下面描述的聊天组件使用数字状态以及增加数字的功能。数字状态用于识别哪些聊天气泡已经被渲染,因为气泡有一个动画回调,它是改变状态的地方 - 以确保下一个聊天气泡不会开始动画,直到前一个完全完成完毕。

问题是我需要在渲染“输入字段”时发生超时,因为在触发 Typist 的打字动画之前,用户必须看到占位符几秒钟。

聊天.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;

聊天选择.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;

标签: javascriptreactjsrenderingsettimeout

解决方案


在 JSX 中,{...}输出其中的表达式的结果。(例如,您在其他地方依赖它className={classes.InputText}。)您正在评估setTimeoutin {},它返回一个计时器句柄,它是一个数字。

你根本不应该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>
  )
}

现场示例:

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,您也会一次又一次地这样做。如果您只想在它是时这样做,请添加一个分支:showInputTexttruefalse

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>
  )
}

现场示例:

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. 为此,请使用带有(and ) 作为依赖项的useEffect回调,并设置计时器 if :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]);

现场示例:

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钩子中的清理函数取消计时器以避免过时的调用:

// *** `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]);

现场示例:

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>


推荐阅读