admin管理员组

文章数量:1375762

I'm trying to make a typewriting effect in a React ponent. I pass in an array of strings and try to render them character by character after some interval (using setTimeout). Here is my code for that

  state = {
    typeColor: {
      color: "blue"
    },
    typed: ""
  };

  ponentDidMount() {
    this.props.typewriter.map(string => {
      console.log("1", string);
      return this.stringChecker(string);
    });
  }

  typeWriter(string) {
    if (string.length === 0) {
      return;
    }
    console.log("3", string);
    this.setState((state, props) => ({ typed: state.typed.concat(string[0]) }));
    console.log(this.state.typed);

    setTimeout(() => this.typeWriter(string.slice(1)), 120);
  }
  stringChecker(string) {
    console.log("2", string);
    if (string === "Qurram") {
      this.setState({ typeColor: { color: "blue" } });
    } else {
      this.setState({ typeColor: { color: "pink" } });
    }
    this.typeWriter(string);
  }

  render() {
    return <div style={this.state.typeColor}>{this.state.typed}</div>;
  }
}

Now I was expecting the execution flow of this to go like this: first element of string array is 'selected' in the map function -> i call stringchecker for that first element -> i call typewriter for that first element, and iterate over the characters. then i go back to the map function and do the same for next. It seems the flow is not like this at all,instead it alternates between the two strings for each character slice. I'd really appreciate if someone could explain that to me. Thank you very much

Sandbox link :

I'm trying to make a typewriting effect in a React ponent. I pass in an array of strings and try to render them character by character after some interval (using setTimeout). Here is my code for that

  state = {
    typeColor: {
      color: "blue"
    },
    typed: ""
  };

  ponentDidMount() {
    this.props.typewriter.map(string => {
      console.log("1", string);
      return this.stringChecker(string);
    });
  }

  typeWriter(string) {
    if (string.length === 0) {
      return;
    }
    console.log("3", string);
    this.setState((state, props) => ({ typed: state.typed.concat(string[0]) }));
    console.log(this.state.typed);

    setTimeout(() => this.typeWriter(string.slice(1)), 120);
  }
  stringChecker(string) {
    console.log("2", string);
    if (string === "Qurram") {
      this.setState({ typeColor: { color: "blue" } });
    } else {
      this.setState({ typeColor: { color: "pink" } });
    }
    this.typeWriter(string);
  }

  render() {
    return <div style={this.state.typeColor}>{this.state.typed}</div>;
  }
}

Now I was expecting the execution flow of this to go like this: first element of string array is 'selected' in the map function -> i call stringchecker for that first element -> i call typewriter for that first element, and iterate over the characters. then i go back to the map function and do the same for next. It seems the flow is not like this at all,instead it alternates between the two strings for each character slice. I'd really appreciate if someone could explain that to me. Thank you very much

Sandbox link : https://codesandbox.io/s/morning-dust-18nq5

Share Improve this question edited Jan 20, 2020 at 20:18 Yevhen Horbunkov 15.6k3 gold badges27 silver badges45 bronze badges asked Jan 17, 2020 at 11:51 Qurram ZaheerQurram Zaheer 31 silver badge3 bronze badges 2
  • Can you so a stackblitz example for the question. – Akhil Aravind Commented Jan 17, 2020 at 12:02
  • @AkhilAravind I've added sandbox link – Qurram Zaheer Commented Jan 17, 2020 at 12:47
Add a ment  | 

4 Answers 4

Reset to default 5

You may simply employ useEffect() hook that will increase visible part of the text (stored within ponent's state) after each re-render (with random delay) until the entire text is out.

Live-demo of this concept you may find below:

const { useState, useEffect } = React
const { render } = ReactDOM
      

const srcString = `That's the text I'm going to print out`

const Typewriter = ({
  srcString, 
  minTypingDelay = 50, 
  typingDelayVariation = 200
}) => {
  const [content, setContent] = useState('')
       
  useEffect(() => {
    const cursorPosition = content.length
    const typingDelay = 0|(Math.random()*typingDelayVariation+minTypingDelay)
    
    if(cursorPosition == srcString.length) return
    
    const timer = setTimeout(() => {
      setContent(content+srcString[cursorPosition])
      clearTimeout(timer)
    }, typingDelay)
    
  }, [content])
  
  return <span>{content}<span className="cursor">|</span></span>
}

render (
  <Typewriter {...{srcString}} />,
  document.getElementById('root')
)
.cursor{animation:1s blink step-end infinite}@keyframes blink{from,to{color:transparent}50%{color:#000}}@-moz-keyframes blink{from,to{color:transparent}50%{color:#000}}@-webkit-keyframes blink{from,to{color:transparent}50%{color:#000}}@-ms-keyframes blink{from,to{color:transparent}50%{color:#000}}@-o-keyframes blink{from,to{color:transparent}50%{color:#000}}
<script src="https://cdnjs.cloudflare./ajax/libs/react/16.12.0/umd/react.production.min.js"></script><script src="https://cdnjs.cloudflare./ajax/libs/react-dom/16.11.0/umd/react-dom.production.min.js"></script><div id="root"></div>

The typing logic is good, however, note that setTimeout is async, so it doesn't wait for the whole word to be typed before switching to the next one. The solution is to make the whole typing flow async, so you don't switch to the next word before the previous one has been typed.

Here's the sandbox demo: https://codesandbox.io/s/boring-greider-824nd

The main change is this one:

if (string.length === 0) {
  words = words.slice(1);
  words[0] && this.typeWord(words[0], words);
}

Check if we finished typing a word, then slice it, to get the next word. If we still have words we call typeWord with the word that should be typed and the array of words that should be typed next. If there are no words left, we exit typing.

Yup, setTimeout() can be tricky when working with tasks that need to execute synchronously (like a typing effect).

Here's another approach using async/await. I made the typeWriter method async, and so it'll only return once all of the characters in the string have been typed. After that, the NEXT string will be allowed to type. For this to work, stringChecker also needs to be async. Check it out and let me know if anything's confusing.

https://codesandbox.io/s/youthful-davinci-d5bk5?fontsize=14&hidenavigation=1&theme=dark

Also... if you just want to get it up and running, I made a TypeIt, a JS typing library, and there's also a React ponent version of it. If it's useful, check it out:

https://github./alexmacarthur/typeit-react

This worked for me, a small custom hook I wrote

export const useTypeWriter = (content = '') => {
    const [prevContent, setPrevContent] = useState(content);
    const [animatedContent, setAnimatedContent] = useState('');
    const hasContentChanged = prevContent === content;

    const generateContent = () => {
        setAnimatedContent((prevAnimatedContent) => {
            const charCount = prevAnimatedContent.length;
            if (prevAnimatedContent.length + 1 !== content.length) setTimeout(generateContent, 90);

            return prevAnimatedContent + content[charCount];
        });
    };

    useEffect(() => {
        if (!hasContentChanged) return;

        setPrevContent(content);
        generateContent(content);
    }, [hasContentChanged]);

    return animatedContent;
};

本文标签: javascriptTypewriter effect in ReactStack Overflow