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