Handling checkbox filters in a NextJS app with Groq queries and React state
I'll help you understand how to filter Sanity documents based on checkbox inputs in your Next.js app! This is a common pattern that combines client-side state management with GROQ queries.
The Basic Approach
The key is to build dynamic GROQ queries based on your checkbox state. Here's a practical example:
1. Set up your checkbox state:
// components/FilteredPosts.jsx
import { useState, useEffect } from 'react';
import { client } from '@/sanity/lib/client';
export default function FilteredPosts() {
const [filters, setFilters] = useState({
featured: false,
published: false,
hasImage: false
});
const [posts, setPosts] = useState([]);
// Handle checkbox changes
const handleFilterChange = (filterName) => {
setFilters(prev => ({
...prev,
[filterName]: !prev[filterName]
}));
};
// Fetch filtered posts
useEffect(() => {
const fetchPosts = async () => {
const query = buildQuery(filters);
const result = await client.fetch(query);
setPosts(result);
};
fetchPosts();
}, [filters]);
return (
<div>
<div className="filters">
<label>
<input
type="checkbox"
checked={filters.featured}
onChange={() => handleFilterChange('featured')}
/>
Featured Only
</label>
<label>
<input
type="checkbox"
checked={filters.published}
onChange={() => handleFilterChange('published')}
/>
Published Only
</label>
<label>
<input
type="checkbox"
checked={filters.hasImage}
onChange={() => handleFilterChange('hasImage')}
/>
Has Image
</label>
</div>
<div className="posts">
{posts.map(post => (
<div key={post._id}>{post.title}</div>
))}
</div>
</div>
);
}2. Build dynamic GROQ queries:
function buildQuery(filters) {
// Start with base query
let conditions = ['_type == "post"'];
// Add conditions based on active filters
if (filters.featured) {
conditions.push('featured == true');
}
if (filters.published) {
conditions.push('publishedAt != null');
}
if (filters.hasImage) {
conditions.push('defined(mainImage)');
}
// Join conditions with && operator
const filterString = conditions.join(' && ');
// Return complete GROQ query
return `*[${filterString}] | order(publishedAt desc) {
_id,
title,
slug,
publishedAt,
featured,
mainImage
}`;
}Key GROQ Filtering Concepts
Based on GROQ's syntax structure, here are the filtering patterns you'll use:
- Equality checks:
fieldName == trueorfieldName == "value" - Existence checks:
defined(fieldName)- checks if a field exists (uses the defined() function) - Null checks:
fieldName != null- ensures field has a value - Combining conditions: Use
&&(AND) or||(OR) - Array filtering:
"value" in arrayField[]
More Advanced Filtering Example
If you have multiple categories or tags:
function buildAdvancedQuery(filters) {
let conditions = ['_type == "post"'];
// Filter by multiple selected categories
if (filters.categories && filters.categories.length > 0) {
const categoryRefs = filters.categories.map(id => `"${id}"`).join(', ');
conditions.push(`references(*[_type == "category" && _id in [${categoryRefs}]]._id)`);
}
// Filter by boolean fields
if (filters.featured) {
conditions.push('featured == true');
}
const filterString = conditions.join(' && ');
return `*[${filterString}] {
_id,
title,
categories[]-> {
_id,
title
}
}`;
}Server Components Alternative (Next.js 13+)
If you're using App Router, you can also handle this with URL search params:
// app/posts/page.jsx
import { client } from '@/sanity/lib/client';
export default async function PostsPage({ searchParams }) {
const params = await searchParams; // await if using Next.js 15
const featured = params.featured === 'true';
const published = params.published === 'true';
let conditions = ['_type == "post"'];
if (featured) conditions.push('featured == true');
if (published) conditions.push('publishedAt != null');
const query = `*[${conditions.join(' && ')}]`;
const posts = await client.fetch(query);
return (
<div>
<FilterForm /> {/* Client component with checkboxes */}
<PostsList posts={posts} />
</div>
);
}The beauty of GROQ is that it handles all the filtering server-side, so you only get back the documents that match your criteria. No need to filter arrays in JavaScript after fetching! The pipeline structure means you're querying efficiently at the database level.
Sanity – Build the way you think, not the way your CMS thinks
Sanity is the developer-first content operating system that gives you complete control. Schema-as-code, GROQ queries, and real-time APIs mean no more workarounds or waiting for deployments. Free to start, scale as you grow.