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