Skip to content

React Hooks and Suspense

Hooks and Suspense can work together to make loading data feel pretty simple. This pulls a list of weekly trending GitHub repositories for whichever language is selected and shows a spinner while the request is still running.

Suspense for data fetching is still experimental, so react-cache and unstable_createResource might change.

unstable_createResource wraps the fetcher and caches its result using the language as the key.

import React from "react";
import ReactDOM from "react-dom";
import { unstable_createResource } from "react-cache";

const getRepos = (lang = "javascript") =>
	fetch(`https://github-trending-api.now.sh/repositories?language=${lang}&since=weekly`).then((res) => res.json());

const Repos = unstable_createResource(getRepos);
function Spinner() {
	return (
		<h1 className="Spinner">
			<span role="img" aria-label="Cyclone Emoji">
				🌀
			</span>
		</h1>
	);
}

Repos.read(language) is the important part. When that resource is not ready, read suspends the render instead of returning a loading flag. React tries rendering the list again once the request resolves.

function ListView({ language }) {
	const repos = Repos.read(language);

	return repos.map((repo, i) => (
		<div className="Repository" key={i}>
			<h3>
				<b>{repo.name}</b> / {repo.author}
				<a className="Repository__link" href={repo.url}>
					<span role="img" aria-label="Chain Link Emoji">
						🔗
					</span>
				</a>
			</h3>
			<p>{repo.description}</p>

			<aside>
				<div>
					<span role="img" aria-label="Star Emoji">
						🌟
					</span>{" "}
					{repo.stars}{" "}
					<span role="img" aria-label="Silverware Emoji">
						🍴
					</span>{" "}
					{repo.forks}
				</div>
				<div>
					<span role="img" aria-label="Book Emoji">
						📚
					</span>
					{repo.language}
				</div>
			</aside>
		</div>
	));
}
function Options({ values, selected, onChange }) {
	return (
		<div className="Options">
			{values.map(({ name, value }, idx) => {
				const checked = selected === value;
				const className = ["Radio", checked ? "active" : "inactive"].join(" ");

				return (
					<div className={className} key={idx} onClick={() => onChange(value)}>
						<span className="OptionLabel">{name}</span>
					</div>
				);
			})}
		</div>
	);
}

The language selector only updates state. That value becomes the resource key passed into the list, while the surrounding Suspense boundary supplies the loading state.

function App() {
	const [language, setLanguage] = React.useState("javascript");

	return (
		<div className="Container">
			<h1 className="Title">Repos</h1>
			<Options
				selected={language}
				onChange={setLanguage}
				values={[
					{ name: "JavaScript", value: "javascript" },
					{ name: "TypeScript", value: "typescript" },
					{ name: "Python", value: "python" },
					{ name: "Scala", value: "scala" },
					{ name: "Ruby", value: "ruby" },
				]}
			/>
			<React.Suspense fallback={<Spinner />}>
				<ListView language={language} />
			</React.Suspense>
		</div>
	);
}

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