2

I have a React Native component that needs to show an Animation with the letters of a string printing letter by letter. So, logically, I need to update a React state hook inside a loop appending each character of the string to it in a 1 second interval. What I have so far is:

let [placeholder, change_placeholder] = React.useState('');
const placeholder_print = () => {
    let temp = "Search for anything"
    console.log("Value of temp is now: " + temp)
    console.log("Length of string: " + temp.length)
    for (let i = 0; i < temp.length; i++) {
        console.log("Selected character is: " + temp.charAt(i))
        change_placeholder(placeholder.concat(temp.charAt(i)))
        console.log("Placeholder is now: " + placeholder)
    }
}

and the React component with the state hook in it:

<View>
    <Text>{placeholder}</Text>
</View>

Yes, I know that this won't animate. But in the end of the for, the value of placeholder should logically be the string itself, right? If so, I might be able to get around the animation part with setTimeout() inside the for loop to run this every second. But, this is the output when placeholder_print() is run:

[Fri Jan 15 2021 16:29:30.166]  LOG      Value of temp is now: Search
[Fri Jan 15 2021 16:29:30.168]  LOG      Length of string: 6
[Fri Jan 15 2021 16:29:30.169]  LOG      Selected character is: S
[Fri Jan 15 2021 16:29:30.170]  LOG      Placeholder is now:
[Fri Jan 15 2021 16:29:30.170]  LOG      Selected character is: e
[Fri Jan 15 2021 16:29:30.170]  LOG      Placeholder is now:
[Fri Jan 15 2021 16:29:30.171]  LOG      Selected character is: a
[Fri Jan 15 2021 16:29:30.171]  LOG      Placeholder is now:
[Fri Jan 15 2021 16:29:30.171]  LOG      Selected character is: r
[Fri Jan 15 2021 16:29:30.172]  LOG      Placeholder is now:
[Fri Jan 15 2021 16:29:30.172]  LOG      Selected character is: c
[Fri Jan 15 2021 16:29:30.172]  LOG      Placeholder is now:
[Fri Jan 15 2021 16:29:30.173]  LOG      Selected character is: h
[Fri Jan 15 2021 16:29:30.173]  LOG      Placeholder is now:

When running a similar logic in Python or even vanilla JavaScript with no state hooks, this works fine. I don't know if this needs to be implemented with the <Animated> component in React Native, or if it's some fundamental difference in the way React state hooks work. I'm stuck here and helping hands will be upvoted.

Vishal DS
  • 175
  • 5
  • 16

2 Answers2

3

The reason you aren't seeing the update in your for loop is that setState() is asynchronous and the updated state value is not available until the next render. So you are just looping through your string, logging the current state value (which is an empty string) and then calling setState which will be called once the loop is finished. see: Console.log() after setState() doesn't return the updated state

When thinking about a loop like this in the context of react you need to take into account the larger state/render cycle. The 'loop' of the animation is actually successive renders triggered by state changes.

The useEffect hook gives you a built in method of leveraging this cycle, meaning that you simply need to track index through sequential renders (the snippet uses useRef to persist the value) and use it to update state, thus triggering a new render, etc.

Here is a quick snippet.

const App = () => {
  const [placeholder, setPlaceholder] = React.useState('');

  const
    string = 'This is the final string.',
    index = React.useRef(0);

  React.useEffect(() => {
    function tick() {
      setPlaceholder(prev => prev + string[index.current]);
      index.current++;
    }
    if (index.current < string.length) {
      let addChar = setInterval(tick, 500);
      return () => clearInterval(addChar);
    }
  }, [placeholder]);

  return (
    <div>
      {placeholder}
    </div>
  )
}


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

<div id="root"></div>
pilchard
  • 12,414
  • 5
  • 11
  • 23
0

Some other solution is not working out of the box but was kinda helpful. Here's a more complete working solution.

import React from 'react';

interface SlowTextProps {
  speed: number;
  text: string;
}

const SlowText = (props: SlowTextProps) => {
  const { speed, text } = props;
  const [placeholder, setPlaceholder] = React.useState(text[0]);

  const index = React.useRef(0);

  React.useEffect(() => {
    function tick() {
      index.current++;
      setPlaceholder((prev: string) => prev + text[index.current]);
    }
    if (index.current < text.length - 1) {
      let addChar = setInterval(tick, speed);
      return () => clearInterval(addChar);
    }
  }, [placeholder, speed, text]);
  return <span>{placeholder}</span>
}

const App = () => {
  return (
    <div>
      <SlowText speed={100} text={"Hello World 1"} />
      <SlowText speed={200} text={"Hello World 2"}/>
      <SlowText speed={500} text={"Hello World 3"} />
    </div>
  )
}

export default App;

Arro
  • 158
  • 1
  • 10