I am working on a React component where I want to submit a URL with the active filters applied when a button is clicked. However, my handleSubmit function does not seem to submit the URL correctly when the parameters are set.
What I’ve Tried
- Ensuring that hasMounted is correctly set and used in useEffect.
- Verifying that filters and defaultFilters contain the expected values.
- Logging activeFilters and the constructed URL to confirm they are correctly populated.
The Issue And What I Need Help With
The URL is not being submitted as expected when the handleSubmit function is called. The router.push(url) doesn’t seem to trigger the navigation to the new URL with the query parameters.
Any guidance or examples would be greatly appreciated!
My code for reference
"use client";
import React, { useState, useEffect, useRef } from "react";
import { useRouter, usePathname, useSearchParams } from "next/navigation";
import MultiRangeSlider from "../../inputs/MultiRangeSlider";
import MultipleSelect from "../../inputs/MultipleSelect";
import TagInput from "../../inputs/TagInput";
import Switch from "../../Switch";
import Select from "../../inputs/Select";
import Counter from "../../inputs/Counter";
import qs from "query-string";
import { facilitiesList } from "@/app/datasets/facilitiesList";
import { servicesList } from "@/app/datasets/servicesList";
import { categoriesList } from "@/app/datasets/categoriesList";
import { useSelectedOption } from "@/app/providers/SelectedOptionProvider";
import Input from "../../inputs/Input";
interface FilterWidgetProps {
cityName: string;
initialSizeRange?: number[];
initialRentRange?: number[];
}
interface Filters {
description: string;
minRent: number;
maxRent: number;
minSize: number;
maxSize: number;
onlyAvailable: boolean;
availableFrom: string;
rentalPeriod: string;
area: string[];
minRooms: number;
minToilets: number;
minKitchens: number;
minBathrooms: number;
facilities: string[];
services: string[];
categories: string[];
}
const FilterWidget: React.FC<FilterWidgetProps> = ({
cityName,
initialRentRange = [0, 100],
initialSizeRange = [0, 100],
}) => {
const { selectedOption } = useSelectedOption(); // 'Privat' or 'Erhverv'
const router = useRouter();
const pathname = usePathname();
const searchParams = useSearchParams();
const defaultFilters: Filters = {
description: "",
minRent: initialRentRange[0],
maxRent: initialRentRange[1],
minSize: initialSizeRange[0],
maxSize: initialSizeRange[1],
onlyAvailable: false,
availableFrom: "",
rentalPeriod: "",
area: [],
minRooms: 0,
minToilets: 0,
minKitchens: 0,
minBathrooms: 0,
facilities: [],
services: [],
categories: [],
};
const [filters, setFilters] = useState<Filters>(defaultFilters);
const prevFilters = useRef<Filters>(filters);
const hasMounted = useRef(false);
useEffect(() => {
if (searchParams) {
const initialParams = qs.parse(searchParams.toString());
setFilters((prevFilters) => ({
...prevFilters,
description:
typeof initialParams.description === "string"
? initialParams.description
: "",
minRent: Number(initialParams.minRent) || initialRentRange[0],
maxRent: Number(initialParams.maxRent) || initialRentRange[1],
minSize: Number(initialParams.minSize) || initialSizeRange[0],
maxSize: Number(initialParams.maxSize) || initialSizeRange[1],
onlyAvailable: initialParams.onlyAvailable === "true" || false,
availableFrom:
typeof initialParams.availableFrom === "string"
? initialParams.availableFrom
: "",
rentalPeriod:
typeof initialParams.rentalPeriod === "string"
? initialParams.rentalPeriod
: "",
area:
typeof initialParams.area === "string"
? initialParams.area.split(",")
: [],
minRooms: Number(initialParams.minRooms) || 0,
minToilets: Number(initialParams.minToilets) || 0,
minKitchens: Number(initialParams.minKitchens) || 0,
minBathrooms: Number(initialParams.minBathrooms) || 0,
facilities:
typeof initialParams.facilities === "string"
? initialParams.facilities.split(",")
: [],
services:
typeof initialParams.services === "string"
? initialParams.services.split(",")
: [],
categories:
typeof initialParams.categories === "string"
? initialParams.categories.split(",")
: [],
}));
}
}, [searchParams, initialRentRange, initialSizeRange]);
useEffect(() => {
if (hasMounted.current) {
const activeFilters = Object.keys(filters).reduce((acc, key) => {
if (Array.isArray(filters[key as keyof Filters])) {
if ((filters[key as keyof Filters] as unknown as any[]).length > 0) {
(acc as any)[key] = filters[key as keyof Filters];
}
} else if (
filters[key as keyof Filters] !== defaultFilters[key as keyof Filters]
) {
(acc as any)[key] = filters[key as keyof Filters];
}
return acc;
}, {} as Partial<Filters>);
const prevActiveFilters = Object.keys(prevFilters.current).reduce(
(acc, key) => {
if (Array.isArray(prevFilters.current[key as keyof Filters])) {
if (
(prevFilters.current[key as keyof Filters] as unknown as any[])
.length > 0
) {
(acc as any)[key] = prevFilters.current[key as keyof Filters];
}
} else if (
prevFilters.current[key as keyof Filters] !==
defaultFilters[key as keyof Filters]
) {
(acc as any)[key] = prevFilters.current[key as keyof Filters];
}
return acc;
},
{} as Partial<Filters>
);
if (JSON.stringify(activeFilters) !== JSON.stringify(prevActiveFilters)) {
const query = {
...activeFilters,
area: (activeFilters.area as string[])?.join(","),
facilities: (activeFilters.facilities as string[])?.join(","),
services: (activeFilters.services as string[])?.join(","),
categories: (activeFilters.categories as string[])?.join(","),
};
const url = `${pathname}?${qs.stringify(query)}`;
window.history.pushState(null, "", url);
}
prevFilters.current = filters;
} else {
hasMounted.current = true;
}
}, [filters, pathname]);
const handleFilterChange = (filterName: keyof Filters, value: any) => {
setFilters((prevFilters) => ({
...prevFilters,
[filterName]: value,
}));
};
const handleSubmit = () => {
const activeFilters = Object.keys(filters).reduce((acc, key) => {
if (Array.isArray(filters[key as keyof Filters])) {
if ((filters[key as keyof Filters] as unknown as any[]).length > 0) {
(acc as any)[key] = filters[key as keyof Filters];
}
} else if (
filters[key as keyof Filters] !== defaultFilters[key as keyof Filters]
) {
(acc as any)[key] = filters[key as keyof Filters];
}
return acc;
}, {} as Partial<Filters>);
const query = {
...activeFilters,
area: (activeFilters.area as string[])?.join(","),
facilities: (activeFilters.facilities as string[])?.join(","),
services: (activeFilters.services as string[])?.join(","),
categories: (activeFilters.categories as string[])?.join(","),
};
const url = `${pathname}?${qs.stringify(query)}`;
console.log("Submitting URL:", url); // Debugging statement
router.push(url);
};
return (
<div className="p-4 lg:py-6 lg:px-8 bg-white rounded-lg shadow-lg">
<h4 className="mb-0 text-2xl font-semibold text-neutral-500">
Filter properties in {cityName}
</h4>
<div className="my-3"></div>
<div className="flex items-center justify-between px-4 pt-3">
<div className="pb-10 pt-4">
<Input
id="description"
label="Search Description"
onChange={(e) => handleFilterChange("description", e.target.value)}
name="description"
debounce
value={filters.description}
/>
</div>
<div className="pb-10 pt-4">
<TagInput
value={filters.area}
onChange={(newValue) => handleFilterChange("area", newValue)}
placeholder="Search by city or postal code"
/>
</div>
</div>
<p className="mt-10 mb-4 text-neutral-500 text-xl font-medium">Rent</p>
<div className="pb-10 pt-4">
<MultiRangeSlider
key="rent"
initialMin={filters.minRent}
initialMax={filters.maxRent}
min={0}
max={999999}
onChange={(min, max) => {
handleFilterChange("minRent", min);
handleFilterChange("maxRent", max);
}}
/>
</div>
<p className="mt-2 mb-4 text-neutral-500 text-xl font-medium">Size</p>
<div className="pb-10 pt-4">
<MultiRangeSlider
key="size"
initialMin={filters.minSize}
initialMax={filters.maxSize}
min={0}
max={999999}
onChange={(min, max) => {
handleFilterChange("minSize", min);
handleFilterChange("maxSize", max);
}}
/>
</div>
<MultipleSelect
label="Housing Type"
options={categoriesList[selectedOption]}
onChange={(newValue) => handleFilterChange("categories", newValue)}
value={filters.categories}
/>
<div className="my-6"></div>
<div className="flex items-center justify-between px-4">
<p className="mt-2 mb-4 text-neutral-500 text-xl font-medium">
Show only available properties
</p>
<Switch
checked={filters.onlyAvailable}
onChange={(e) =>
handleFilterChange("onlyAvailable", e.target.checked)
}
/>
</div>
<div className="my-6"></div>
<div className="flex items-center justify-between px-4">
<Select
id="availabilityPeriod"
label="Period"
name="period"
options={[
"Unlimited",
"Less than 12 months",
"12-24 months",
"24+ months",
]}
onChange={(newValue) => handleFilterChange("rentalPeriod", newValue)}
value={filters.rentalPeriod}
/>
</div>
<p className="mt-10 mb-5 text-neutral-500 text-xl font-medium">
Select minimum desired
</p>
<Counter
title="Rooms"
subtitle=""
value={filters.minRooms}
onChange={(newValue) => handleFilterChange("minRooms", newValue)}
/>
<div className="my-2"></div>
<Counter
title="Toilets"
subtitle=""
value={filters.minToilets}
onChange={(newValue) => handleFilterChange("minToilets", newValue)}
/>
<div className="my-2"></div>
<Counter
title="Kitchens"
subtitle=""
value={filters.minKitchens}
onChange={(newValue) => handleFilterChange("minKitchens", newValue)}
/>
<div className="my-2"></div>
<Counter
title="Bathrooms"
subtitle=""
value={filters.minBathrooms}
onChange={(newValue) => handleFilterChange("minBathrooms", newValue)}
/>
<div className="mt-10 mb-5"></div>
<MultipleSelect
label="Facilities"
options={facilitiesList[selectedOption]}
onChange={(newValue) => handleFilterChange("facilities", newValue)}
value={filters.facilities}
/>
<div className="my-5"></div>
<MultipleSelect
label="Services"
options={servicesList[selectedOption]}
onChange={(newValue) => handleFilterChange("services", newValue)}
value={filters.services}
/>
<div className="border-t my-3"></div>
<div className="border-t border-dashed my-6"></div>
<div className="flex justify-center mt-4">
<button
onClick={handleSubmit}
className="px-4 py-2 bg-blue-500 text-white rounded-lg shadow-md hover:bg-blue-600 focus:outline-none">
Apply Filters
</button>
</div>
</div>
);
};
export default FilterWidget;