Headless Craft CMS with GraphQL and React, Part 6: Refactoring the Listing Component
December 10, 2025
So far we've been keeping most of our code in a single listing page component. That worked fine when we didn't have much functionality to keep track of, but as our site becomes more complex we should do a little housekeeping to make sure the main component doesn't become too cluttered.
Move the Entry Listing to a Sub-Component
The listing grid itself does a lot of heavy-lifting for our application, so we're going to move it to its own sub-component which will make it easier to apply conditional display logic depending on how our data is returned.
Create a /src/components directory if you don't already have one and create a new file called RecipeListing.js. We're going to move our listing HTML as well as our query error checks to this file:
import Link from "next/link";
import Image from "next/image";
export default function RecipeListing({data, error}) {
// if there was a GraphQL error log it and return a message to the user
if (error) {
console.error(error);
return <p>There was an error fetching the entries.</p>;
}
// prevent an error if the component mounts before the data has loaded
if (!data) return null;
return (
<div className="listing">
{ data.entries.map( (entry) => (
<div className="item" key={entry.id}>
<Link href={entry.uri} className="recipe-card">
<div className="media">
<Image src={entry.image[0].url} alt={entry.title} width="500" height="250" />
</div>
<h6>{entry.title}</h6>
</Link>
</div>
)) }
</div>
);
}
(<Image /> and <Link /> are Next.js-provided components. Use 'em if you got 'em, don't if you don't.)
This method also lets us display query status messages to the user from within the page layout rather than overriding the entire page.
Now we'll import the component to our /src/pages/index.js page and pass the query data as props:
import RecipeListing from "@/components/RecipeListing";
// ...
export default function Home() {
// ...
<section className="layout-section">
<RecipeListing
data={data}
error={error}
/>
{/* add a check if data exists before trying to access its properties */}
{data && data.entries.length < data.entryCount &&
<nav className="pager">
<button onClick={handleLoadMore}>Load More</button>
</nav>
}
</section>
}
Move Search and Category Filters to Sub-Components
We haven't done much with these fields yet, but we will. So let's split them out into their own components as well:
/components/SearchFilter.js:
export default function SearchFilter() {
return (
<div className="search-filter">
<input id="search" name="search" type="search" />
</div>
);
}
/components/CategoryFilter.js:
export default function CategoryFilter() {
return (
<div className="category-filter">
<div className="fieldgroup">
<input type="checkbox" id="vegan" name="vegan" />
<label htmlFor="vegan">Vegan</label>
</div>
<div className="fieldgroup">
<input type="checkbox" id="gluten-free" name="gluten-free" />
<label htmlFor="gluten-free">Gluten Free</label>
</div>
<div className="fieldgroup">
<input type="checkbox" id="low-carb" name="low-carb" />
<label htmlFor="low-carb">Low Carb</label>
</div>
<div className="fieldgroup">
<input type="checkbox" id="low-sodium" name="low-sodium" />
<label htmlFor="low-sodium">Low Sodium</label>
</div>
</div>
);
}
Once we add the new components to our main listing template, things are looking a lot cleaner:
import Head from "next/head";
import GlobalHeader from "@/components/GlobalHeader";
import GlobalFooter from "@/components/GlobalFooter";
import RecipeListing from "@/components/RecipeListing";
import SearchFilter from "@/components/SearchFilter";
import CategoryFilter from "@/components/CategoryFilter";
import { useQuery } from '@apollo/client/react';
import GET_RECIPE_ENTRIES from "@/data/recipe-entries-query";
import { useRef } from "react";
export default function Home() {
const queryOffset = useRef(0);
const queryVariables = {
section: ['recipes'],
limit: 4,
offset: queryOffset.current
}
const { error, data, fetchMore } = useQuery(GET_RECIPE_ENTRIES, { variables: queryVariables });
const handleLoadMore = () => {
fetchMore({
variables: {
offset: queryOffset.current = data.entries.length
}
});
}
return (
<>
<Head>
<title>The Crafty Cook</title>
</Head>
<GlobalHeader />
<main>
<section className="layout-section">
<div className="filters">
<SearchFilter />
<CategoryFilter />
</div>
</section>
<section className="layout-section">
<RecipeListing
data={data}
error={error}
/>
{data && data.entries.length < data.entryCount &&
<nav className="pager">
<button onClick={handleLoadMore}>Load More</button>
</nav>
}
</section>
</main>
<GlobalFooter />
</>
);
}
Now we're in a much better position to start adding complex filtering logic while keeping our project organized!