Code Buckets

Buckets of code

HTML and CSS React

Fading text in and out with React

Bukowski Quotes

The Task

I want to fade text in and out of a React app. I’m populating a series of quotes from an API and I want them to change gracefully.

The Demo App

The demo is a React application showing quotes from Charles Bukowski. Charles Bukowski was an American author and poet writing in the 1960s. He’s just a really quotable guy and his books and poetry are great too.

The quotes are fed in from a mock API i.e. an array of 10 quotes with a delay whenever they are accessed. They are displayed sequentially and change when they are fully faded out.

The demo app is published to

http://bukowskiquotes.codebuckets.com/

so you can get an idea of what the end effect is.

Overview

The idea is that the lifecycle of a quote display is split up into 10 phases. The API and the UI is coordinated through the phases. The phases are

  • Phase 1. Quote is fully hidden on the UI. The API retrieves the next quote and loads it into the hidden UI element.
  • Phase 2. Quote is made visible on the UI. They fade in with CSS transitions.
  • Phases 3 to 9. The quote is fully visible, and the user can read it until ….
  • Phase 10. The quote starts to fade out with CSS transitions.
  • Phase 1., Cycle starts again

The application starts on phase 3 so that the first quote is fully loaded – it looks a bit weird otherwise. The length of the phase can be whatever works for you. I found about 1.5 seconds per phase felt about right.

Implementation

The structure of the React app is very simple i.e.

  • App Component
    • Header Component
    • Quote Component
    • Footer Component

The fade is done by the Quote component, which you can find on GitHib

https://github.com/timbrownls20/bukowski-quotes/blob/main/src/components/Quote.tsx

And the code is ..

import React, { useEffect, useState, useRef } from "react";
import config from "../config";
import BukowskiQuotes from "../data/Bukowski";

enum Phase {
  GetQuote = 1,
  ShowQuote = 2,
  QuoteVisible = 3,
  HideQuote = 10,
}

async function sleep(msec: number) {
  return new Promise((resolve) => setTimeout(resolve, msec));
}

const Quote = () => {

  const [quote, setQuote]: [string, Function] = useState(BukowskiQuotes[0]);
  const [quoteVisible, setQuoteVisible]: [boolean, Function] = useState(true);
  const quoteVisibleRef: React.MutableRefObject<boolean> = useRef(false);
  const quoteNumberRef: React.MutableRefObject<number> = useRef(0);

  const showQuote = (show: boolean) => {
    quoteVisibleRef.current = show;
    setQuoteVisible(quoteVisibleRef.current);
  };

  useEffect(() => {
    const getQuote = async (
      callback: (quote: string) => void
    ): Promise<void> => {
      //.. simulating API call
      await sleep(750);

      quoteNumberRef.current =
        quoteNumberRef.current < BukowskiQuotes.length - 1
          ? quoteNumberRef.current + 1
          : 0;
      callback(BukowskiQuotes[quoteNumberRef.current]);
    };

    let count: number = Phase.QuoteVisible;

    setInterval(() => {
      let phase = (count % 10) + 1;

      if (phase === Phase.GetQuote) {
        getQuote((quote) => setQuote(quote));
      } else if (phase === Phase.ShowQuote) {
        showQuote(true);
      } else if (phase === Phase.HideQuote) {
        showQuote(false);
      }
      count = count + 1;
    }, config.interval);
  }, []);

  return (
      <div className={"quote" + (quoteVisible ? "" : " hidden")}>
        <div className={"fadein-text" + (quoteVisible ? "" : " hidden")}>
          <pre>{quote}</pre>
        </div>
      </div>
     
  );
};

export default Quote;

The main work is in the useEffect hook which triggers when the application is first loaded. A setInterval call moves the phases on a timer.

    setInterval(() => {
      let phase = (count % 10) + 1;

      if (phase === Phase.GetQuote) {
        getQuote((quote) => setQuote(quote));
      } else if (phase === Phase.ShowQuote) {
        showQuote(true);
      } else if (phase === Phase.HideQuote) {
        showQuote(false);
      }
      count = count + 1;
    }, config.interval);
  }, []);

The component needs to track the quote visibility and the current quote so it can display the right quote at the right time. Since these are set within a setInterval method a naked variable would be captured in a stale closure and would not change so the quote wouldn’t move on properly. To get round this, both variables are placed in a useRef hook so that it will be updated correctly.

The text is faded in and out by a CSS class called hidden being added and removed

.fadein-text
{
  transition: opacity ease-in-out 2s;
  opacity: 1;
  color: white;
}

.hidden
{
  opacity: 0;
}

The CSS changes the opacity from 0 to 1 which triggers the transition.

In additional there is a bunch of media queries so the pages displays nicely on mobile devices. The details of them is not relevant for the implementation – but it’s good that they are there.

The Code

As ever the code is available on my GitHub site. It is is written in TypeScript, but it shouldn’t be much work to convert to JavaScript if required.

https://github.com/timbrownls20/bukowski-quotes

There is also an alternative version in a branch which shows addition debug information – the source code is here

https://github.com/timbrownls20/bukowski-quotes/tree/quotes-visible-phase

It is this version that is published to

http://bukowskiquotes.codebuckets.com/

if you put a Debug parameter on the query string like so

http://bukowskiquotes.codebuckets.com/?Debug

then the footer is changed to display which phase the quote component is on which I find instructive

Additional phase information in bottom right footer

There is also a similar but more complex implementation for Buddha Quotes which I’ve blogged about previously

That changes the quote and the background image and has a lot a proper API to feed the quotes.

Useful Links

https://dmitripavlutin.com/react-hooks-stale-closures/    
Good article describing the issue of stale closures

https://stackoverflow.com/questions/62806541/how-to-solve-the-react-hook-closure-issue
Stack overflow article on the use of useRef hooks to get round the closure issue

https://reactjs.org/docs/hooks-intro.html
React hooks documentation. The app uses useEffect, useState and useRef to implement the fading

https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Transitions/Using_CSS_transitions
CSS transitions. Required for the solution but not sufficient for the entire implementation

http://bukowskiquotes.codebuckets.com/
Published Bukowski quote website. Add Debug param (case sensitive) to query string for extra info.

http://buddhaquotes.codebuckets.com/
Infinite Buddha quotes. The alternative implementation with additional changing images when the page is reloaded

LEAVE A RESPONSE

Your email address will not be published.