Oct 25, 2023
0
Level 3
Laravel API + React Query Filter, Sort, Paginate
I have simple app with search, filter, sort and pagination. Can someone give me feedback if everything is ok or maybe something should be improved.
In Laravel Controller:
public function index(Request $request)
{
$perPage = $request->get('perPage', 10);
return Post::with(['user:id,name', 'category:id,name'])
->when($request->filled('category'), function ($query) use ($request) {
return $query->where('category_id', $request->get('category'));
})
->when($request->filled('search'), function ($query) use ($request) {
return $query->where('title', 'like', '%' . $request->get('search') . '%');
})
->when($request->filled('sortColumn'), function ($query) use ($request) {
if ($request->get('sortColumn') === 'category') {
$query->join('categories', 'categories.id', '=', 'posts.category_id')
->select('posts.*', 'categories.name')
->orderBy('categories.name', $request->get('sortOrder', 'asc'));
} else {
$query->orderBy($request->get('sortColumn'), $request->get('sortOrder', 'asc'));
}
})
->simplePaginate($perPage);
}
public function categories()
{
return Category::get(['name', 'id']);
}
and React:
export default function Posts() { const [params, setParams] = useSearchParams({ sortOrder: 'asc', }); const [data, setData] = useState()
function handlePerPageUpdate(event) {
event.preventDefault();
const newParams = new URLSearchParams(params);
newParams.set('perPage', event.target.value);
setParams(newParams);
}
function handleColumnSort(column) {
const newParams = new URLSearchParams(params);
newParams.set('sortColumn', column);
if (params.get('sortOrder') === 'asc') {
newParams.set('sortOrder', 'desc');
} else {
newParams.set('sortOrder', 'asc');
}
setParams(newParams);
}
function handleCategoryUpdate(event) {
event.preventDefault();
const newParams = new URLSearchParams(params);
if (event.target.value === '') {
newParams.delete('category');
} else {
newParams.set('category', event.target.value);
}
setParams(newParams);
newParams.set('page', '1');
setParams(newParams);
}
function handleSearchUpdate(event) {
event.preventDefault();
setTimeout(() => {
const newParams = new URLSearchParams(params);
if (event.target.value === '') {
newParams.delete('search');
} else {
newParams.set('search', event.target.value);
}
setParams(newParams);
newParams.set('page', '1');
setParams(newParams);
}, 1000);
}
const { isPending, error, data: posts, refetch } = useQuery({
queryKey: ['posts'],
queryFn: () => axios.get('http://127.0.0.1:8000/api/posts', {params: params})
.then((res) => {
setData(res.data);
return res.data.data
}
),
refetchOnWindowFocus: true,
});
useEffect(() => {
refetch()
}, [params, refetch]);
const { isPending: isPendingCategories, error: errorCategories, data: categories } = useQuery({
queryKey: ['categories'],
queryFn: () =>
fetch('http://127.0.0.1:8000/api/categories').then(
(res) => res.json(),
),
});
if (isPending || isPendingCategories) return 'Loading...';
if (error) return 'An error has occurred: ' + error.message;
if (errorCategories) return 'An error has occurred: ' + errorCategories.message;
function handleChangePage(button) {
const newParams = new URLSearchParams(params);
if (button === 'next') {
const page = data.current_page + 1;
newParams.set('page', page.toString())
} else {
const page = data.current_page - 1;
newParams.set('page', page.toString())
}
setParams(newParams);
}
return (
<div className="max-w-7xl mx-auto p-4">
<div className="relative overflow-x-auto shadow-md sm:rounded-lg">
<div className="flex items-center justify-between pb-4">
<div>
<select onChange={handleCategoryUpdate} value={params.get('category') || ''} className="rounded-xl" name="category" id="category">
<option value="">All categories</option>
{categories.map((category) => (
<option key={category.id} value={category.id}>{category.name}</option>
))}
</select>
</div>
<label htmlFor="table-search" className="sr-only">Search</label>
<div className="relative">
<div className="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none">
<svg className="w-5 h-5 text-gray-500 dark:text-gray-400" aria-hidden="true" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fillRule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z" clipRule="evenodd"></path></svg>
</div>
<input defaultValue={params.get('search')} onChange={handleSearchUpdate} type="text" id="table-search" className="block p-2 pl-10 text-sm text-gray-900 border border-gray-300 rounded-lg w-80 bg-gray-50 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="Search by title" />
</div>
</div>
{posts.length
?
<table className="w-full text-sm text-left text-gray-500 dark:text-gray-400">
<thead className="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400">
<tr>
<th onClick={() => handleColumnSort('id')} scope="col" className="px-6 py-3 cursor-pointer hover:bg-gray-100">
ID
</th>
<th onClick={() => handleColumnSort('title')} scope="col" className="px-6 py-3 cursor-pointer hover:bg-gray-100">
Title
</th>
<th onClick={() => handleColumnSort('category')} scope="col" className="px-6 py-3 cursor-pointer hover:bg-gray-100">
Category
</th>
<th scope="col" className="px-6 py-3">
Body
</th>
<th scope="col" className="px-6 py-3">
Created at
</th>
</tr>
</thead>
<tbody>
{posts.map((post) => (
<Post key={post.id} post={post} />
))}
</tbody>
</table>
: <div className="p-2">No results found...</div>
}
<div className="p-2">
<select onChange={handlePerPageUpdate} value={params.get('perPage') || 10} name="perPage" id="perPage" className="rounded-xl">
<option value="10">10 per page</option>
<option value="20">20 per page</option>
<option value="50">50 per page</option>
<option value="100">100 per page</option>
</select>
</div>
<div className="flex items-center justify-between p-4">
<button
onClick={() => handleChangePage('prev')}
disabled={!data.prev_page_url}
className={`rounded-xl px-4 py-1 border-2 border-black hover:border-cyan-500`}
>Prev</button>
<button
onClick={() => handleChangePage('next')}
disabled={!data.next_page_url}
className={`rounded-xl px-4 py-1 border-2 border-black hover:border-cyan-500`}
>Next</button>
</div>
</div>
</div>
)
}
Please or to participate in this conversation.