codeXplainer
View RSS feed

A nice text streaming effect in React

Published on

Welcome!

In this article, we will be creating a text streaming effect using a React Hook. This effect is becoming increasingly popular, and can be used to elevate the user experience.

In essence it is about simulating what would be a conversation with a computer terminal 🤖. But it also allows to present content gradually while at the same time focusing the user's attention.

So let's get started!

Text Streaming Effect

The typing or text streaming effect is a fun way to animate text on your app or website. All this, without needing to open an actual SSE stream or use WebSockets 😅.

It also provides the benefit of being able to control the speed of the animation.

Full Text Streaming

Try out the streaming effect by adjusting the total duration

Interestingly enough using CSS to create this effect can be quite challenging.

In this article we are going the JavaScript route, by creating a React Hook that will allow you to easily implement this effect within any React component.

Letter Streaming

Try out the streaming effect by adjusting the duration per letter

This gives us the flexibility to control the duration of the animation, as well as the type of animation.

Full Text Streaming

Try out the streaming effect by adjusting the total duration

And as you can see, it can be done for any text content. By encapsulating the logic within a hook (a reusable function after all), you can easily add this effect to any component in your application.

Show me the Code

So let's take a look at the details behind this magic.

We will start with a simplified version of the React hook that streams the text letter by letter. Also allowing you to set how long it takes to print each new letter.

The Hook (Letter by Letter)

import { useState, useEffect } from "react";

export type UseStreamProps = {
wholeText: string;
duration: number;
};

export const useStreamedText = (props: UseStreamProps) => {
const [text, setText] = useState("");

useEffect(() => {
const timeDelta = props.duration * 1000

// Update text every timeDelta milliseconds
const interval = setInterval(() => {
if (
text.length < props.wholeText.length
) {
setText(props.wholeText.slice(0, text.length + 1));
}
}, timeDelta);

// Clear interval when text is fully streamed
if (text === props.wholeText) {
clearInterval(interval);
}

// Clear interval when component is unmounted
return () => clearInterval(interval);
}, [
props.wholeText,
text,
props.duration,
]);

return [text];
};

Technical Explanation

Okay, so let's break this down. We are combining a couple React hooks with the power of a setInterval function.

The useEffect hook is responsible for updating the text state every timeDelta milliseconds. While useState is used to store the current text.

timeDelta is the time it takes to draw a new letter.

The text state variable is initially set to an empty string. And on each interval, we update text by adding a new letter from the wholeText property. Which contains the complete text that we want to "stream".

A very key point to note is that we clear the interval when the text is fully streamed. This is done by checking if the current text length is equal to the length of the whole text.

Additionally, we clear the interval when the component is unmounted. This is done by returning a cleanup function from the useEffect hook. This is important to prevent memory leaks.

The useStreamedText hook takes in an object with the following properties:

  • wholeText: The text that you want to stream.
  • duration: The time it takes to draw a new letter.

And returns an array with the current text (although at this point we don't need an array, we will add more features to this hook later on).

Usage

This is how you would use the hook in a React component.

const [text] = useStreamedText({
wholeText: "To the moon 🚀",
duration: 0.03,
});

And that could be it! But where is the fun in that? Let's add some more features to this hook.

Adding Full Duration control

We are adding the ability to set the time it takes to paint the entire text. This can be set by changing the new durationType property to fullText.

import { useState, useEffect } from "react";

export type UseStreamProps = {
durationType: "fullText" | "letter";
wholeText: string;
duration: number;
};

export const useStreamedText = (props: UseStreamProps) => {
const [text, setText] = useState("");

useEffect(() => {
// Depending on the duration type, calculate the time delta
const timeDelta =
props.durationType === "letter"
? props.duration * 1000
: (props.duration * 1000) / props.wholeText.length;

const interval = setInterval(() => {
if (
text.length < props.wholeText.length
) {
setText(props.wholeText.slice(0, text.length + 1));
}
}, timeDelta);

// Clear interval when text is fully streamed
if (text === props.wholeText) {
clearInterval(interval);
}

// Clear interval when component is unmounted
return () => clearInterval(interval);
}, [
props.wholeText,
text,
props.duration,
props.durationType,
]);

return [text];
};

All that was needed besides the durationType property was to calculate the timeDelta based on the durationType. This is achieved by dividing the duration over the length of the whole text.

To support both duration types, we had to use a ternary operator to calculate the timeDelta.

The reason we multiply the duration by 1000 is to convert it to milliseconds, the unit which the setInterval function is expecting.

...
// Depending on the duration type, calculate the time delta
const timeDelta =
props.durationType === "letter"
? props.duration * 1000
: (props.duration * 1000) / props.wholeText.length;
...

Usage

This is how you would use the hook with the new durationType property.

const [text] = useStreamedText({
wholeText: "To the moon 🚀",
duration: 0.03,
durationType: "letter",
});

A final touch (adding a delay)

We will be adding a delay so that the animation doesn't start immediately. This can prove useful when staggering animations, specially when combined with durationType = 'fullText'.

This way you can set a delay for each text element, and they will start streaming at different times.

import { useState, useEffect } from "react";

export type UseStreamProps = {
durationType: "fullText" | "letter";
wholeText: string;
delay: number;
duration: number;
};

export const useStreamedText = (props: UseStreamProps) => {
const [text, setText] = useState("");
const [elapsedTime, setElapsedTime] = useState(0);

useEffect(() => {
// Depending on the duration type, calculate the time delta
const timeDelta =
props.durationType === "letter"
? props.duration * 1000
: (props.duration * 1000) / props.wholeText.length;

const interval = setInterval(() => {
setElapsedTime((prev) => prev + timeDelta);
if (
text.length < props.wholeText.length &&
elapsedTime > props.delay * 1000
) {
setText(props.wholeText.slice(0, text.length + 1));
}
}, timeDelta);

// Clear interval when text is fully streamed
if (text === props.wholeText) {
clearInterval(interval);
}

// Clear interval when component is unmounted
return () => clearInterval(interval);
}, [
props.wholeText,
text,
props.duration,
props.delay,
elapsedTime,
props.durationType,
]);

return [text, elapsedTime];
};

We added a new property delay to the UseStreamProps object. This property is used to set the delay before the animation starts.

We also started tracking the elapsedTime using the setElapsedTime function. This is used to check if the delay has passed.

...
const [elapsedTime, setElapsedTime] = useState(0);
...

Note that we will only update the text if the elapsed time is greater than the delay.

...
if (
text.length < props.wholeText.length &&
elapsedTime > props.delay * 1000
) {
setText(props.wholeText.slice(0, text.length + 1));
}
...

Check it out in action:

Full Text Streaming

Try out the streaming effect by adjusting the delay and total duration

Usage

const [text, elapsedTime] = useStreamedText({
wholeText: "To the moon 🚀",
duration: 0.03,
durationType: "letter",
delay: 1,
});

And that's it! You now have a fully functional React hook that can be used to emulate text streaming on your application.