Build a Random Quote Machine - React useEffect Hook

Hi all, I was hoping for some help with the Random Quote Machine task. I have finished, but I made a couple hackish tweaks in order to get around the issue I’m having. The code is available at CodePen at https://codepen.io/anth-volk/pen/vYaYzJe or below:

JavaScript

const App = () => {
	const [data, setData] = React.useState(null);
	const [loading, setLoading] = React.useState(true);
	const [error, setError] = React.useState(null);
	const [quote, setQuote] = React.useState({
		quote: "",
		author: "",
		index: null
	});
	const [backgroundColor, setBackgroundColor] = React.useState("");

	const src = "https://gist.githubusercontent.com/camperbot/5a022b72e96c4c9585c32bf6a75f62d9/raw/e3c6895ce42069f0ee7e991229064f167fe8ccdc/quotes.json";
	
	// Function that chooses random quote from FCC quote list
	const randomQuote = () => {
		
		let randomizer = Math.floor(Math.random() * data.quotes.length);
		const quoteObj = data["quotes"][randomizer];
		setQuote({
			quote: quoteObj.quote,
			author: quoteObj.author,
			index: randomizer
		});
			
	}
	
	// Function that chooses random color from array declared inside function
	const randomColor = () => {
		
		// Array of colors
		const backgroundColors = [
			"#ac3b61",
			"#5783c9",
			"#b06d25",
			"#93b88f",
			"#b29bc2",
			"#750204"
		];
		
		const index = Math.floor(Math.random() * backgroundColors.length);
		setBackgroundColor(backgroundColors[index]);
		
	}
	
	// Click handler wrapper for above two functions
	const handleClick = () => {
		randomQuote();
		randomColor();
	}
	
	// Based on guide at https://www.freecodecamp.org/news/fetch-data-react/	
	React.useEffect(() => {
	
		// Fetch https://gist.githubusercontent.com/camperbot/5a022b72e96c4c9585c32bf6a75f62d9/raw/e3c6895ce42069f0ee7e991229064f167fe8ccdc/quotes.json within useEffect and don't re-fetch unless src changes
		
		fetch(src)
		.then (response => {
			if (response.ok) {
				return response.json();
			}
			else {
				throw response;
			}
		})
		.then(data => {
			setData(data);
			setQuote(data.quotes[Math.floor(Math.random() * data.quotes.length)]);
			randomColor();
		})
		.catch(error => {
			console.log("Error: ", error);
			setError(error);
		})
		.finally(() => {
			setLoading(false);	
		});
	}, [src]);
	
	if (loading) {
		return (
			<p className="devMessage">Loading</p>
		);
	}
	
	if (error) {
		return (
			<p className="devMessage">Error</p>
		);
	}
	
	return (
			<div id="quote-box">
				<style> {`
					:root {
						--primary-color: ${backgroundColor};
					`}
				</style>
				<div id="text">
					<h1>{quote.quote}</h1>
				</div>
				<div id="author">
					<p className="text-black">{quote.author}</p>
				</div>
				<div id="button-wrapper">
					<a id="tweet-quote" target="_blank" href={`https://www.twitter.com/intent/tweet/?text=${quote.quote}`}>
						<i className="fa-brands fa-square-twitter"></i>
					</a>
					<button className="button" onClick={handleClick}>New Quote</button>
				</div>
			</div>
	);
}


ReactDOM.render(<App />, document.getElementById("root"));

HTML

<div id="root">
</div>
<div id="attribution">
	<p>By <span class="italic white"><a href="https://www.github.com/anth-volk">anth-volk</a></span></p>
</div>

CSS

@import url('https://fonts.googleapis.com/css2?family=Nunito:wght@300&family=Unbounded:wght@300&display=swap');

/* Root vars */
:root {
	--max-quote-box-width: 700px;
	--h1-font-family: 'Unbounded', cursive;
	--h1-font-size: 1.75em;
	--h1-font-weight: 300;
	--p-font-family: 'Nunito', sans-serif;
	--p-font-size: 1.25em;
	--p-font-weight: 200;
	/* Fallback primary color */
	--primary-color: #ac3b61;
	--secondary-color: white;
}

/* CSS reset */

* {
	box-sizing: border-box;
}

html,
body,
h1,
h2,
h3,
h4,
h5,
p {
	margin: 0;
	padding: 0;
}

a {
	text-decoration: none;
	color: unset;
}

/* Begin styles */

html {
	background-color: var(--primary-color);
}

body {
	height: 100vh;
	width: 100vw;
	margin: 50px auto;
	display: flex;
	flex-direction: column;
	justify-content: center;
	align-items: center;
}

h1 {
	color: var(--primary-color);
	font-size: var(--h1-font-size);
	font-family: var(--h1-font-family);
	font-weight: var(--h1-font-weight);
}

p {
	color: var(--secondary-color);
	font-size: var(--p-font-size);
	font-family: var(--p-font-family);
	font-weight: var(--p-font-weight);
}

p.devMessage {
	font-family: monospace;
}

i {
	display: inline-block;
	font-size: calc(1.75 * var(--p-font-size) + 4px);
	color: var(--primary-color);
}

.button {
	font-size: var(--p-font-size);
	font-family: var(--p-font-family);
	padding: calc(0.125 * var(--p-font-size)) 0.5vw;
	border: 1px solid var(--primary-color);
	border-radius: 2px;
	background-color: var(--primary-color);
	color: var(--secondary-color);
}

.italic {
	font-style: italic;
}

.text-black {
	color: black;
}

#quote-box {
	width: 80vw;
	max-width: var(--max-quote-box-width);
	background-color: #fafafa;
	display: flex;
	flex-direction: column;
	justify-content: space-around;
	align-items: center;
	border-radius: 4px;
}

#text {
	width: 100%;
	height: auto;
	padding: 2vw 1.5vw 0 1.5vw;
	text-align: center;
}

#author {
	font-style: italic;
	width: 100%;
	height: auto;
	padding: 1.5vw 1.5vw 0 0;
	text-align: right;
}

#author p::before {
	content: "- ";
}

#button-wrapper {
	width: 100%;
	height: auto;
	padding: 3vw 1.5vw 1.5vw 1.5vw;
	display: flex;
	flex-direction: row;
	justify-content: space-between;
	align-items: center;
}

#attribution {
	width: var(--quote-box-width);
	min-height: 100px;
	padding: 1vw 0 0 0;
	
}

What I’m trying to understand is this: the randomQuote() function that I wrote is able to update the “quote” state variable when called outside of the Effect hook, but when I tried to utilize it to set the quote in the second .then() after fetching the data, it never worked, and I ended up coding its function into the setQuote call in that block. Why would that be? My theory is that it has to do with how React Effect hooks schedule DOM re-rendering, but is that accurate? And if so, how could I circumvent this in the future?

Also, any general comments about the code are also appreciated. Thank you very much.

Create a fork with the implementation that is not working so we know exactly how you did it. The fork button is in the footer.

Thanks for the idea. I just did that, and the new pen is available at https://codepen.io/anth-volk/pen/LYBGYzm. The only difference between the two is the calling of the randomQuote() function useEffect fetch.

I’m currently working on another personal project using the effect hook, and I’m wondering if maybe the issue is that the function I called wasn’t provided in the effect hook’s dependency array? I had run into an infinite re-fetching problem while writing this, and maybe the solution would’ve been to use useCallback()?