skip to Main Content

I am trying to create a filter component that reads filters values from the search params. I want to set just the defaultChecked state of the checkboxes on mount.

Component looks like this:

import "./styles.css";
import {
  Checkbox,
  FormControlLabel,
  Box,
  FormGroup,
  Button,
} from "@mui/material";
import { useSearchParams } from "react-router-dom";

const filterValues = [
  {
    value: 1,
    label: "One",
  },
  {
    value: 2,
    label: "Two",
  },
] as const;

export default function Filters() {
  const [searchParams, setSearchParams] = useSearchParams();
  const onFilterChange = (
    event: React.ChangeEvent<HTMLInputElement>,
    checked: boolean
  ) => {
    let filters = searchParams.get("filters")?.split(",") || [];

    const filter = event.target.value;

    if (checked) filters.push(filter);
    else filters = filters.filter((_filter) => filter !== _filter);

    if (filters.length === 0) searchParams.delete("filters");
    else searchParams.set("filters", filters.join(","));
  };

  return (
    <Box>
      <FormGroup>
        {filterValues.map(({ label, value }) => (
          <FormControlLabel
            key={label}
            control={
              <Checkbox
                defaultChecked={searchParams
                  .get("filters")
                  ?.split(",")
                  .includes(value.toString())}
                onChange={onFilterChange}
              />
            }
            value={value}
            label={label}
          />
        ))}
      </FormGroup>
      <Button variant="contained" onClick={() => setSearchParams(searchParams)}>
        Set Filters
      </Button>
    </Box>
  );
}

The problem is, when I set the new search params after setting up the filters I get the following error:

 MUI: A component is changing the default checked state of an uncontrolled SwitchBase after being initialized.

Codesandbox example:

Edit search-params-mui-default-checked

The possible solution I see is to save initial checked states using useState, which would read all filters from the url, like this:

  const [checkedStates] = useState(
    filterValues.map(({ value }) => ({
      checked: searchParams.get('filters')?.split(',').includes(value.toString()),
    })),
  );

and setting the defaultChecked from it:

<Checkbox defaultChecked={checkedStates[index].checked}/>

It feels like unnecessary state duplication to me. Is there a better way of handling this?

2

Answers


  1. Chosen as BEST ANSWER

    So, based on the @MGreif answers I understand that I need to make the checkboxes controlled components and use the checked prop instead a defaultChecked. So what I've done is:

    I created a new state to read the filter values from search params:

      const [filters, setFilters] = useState(searchParams.get('filters')?.split(',') || []);
    

    Then in the checkbox onChange handler I modify the state:

      const onFilterChange = (event: React.ChangeEvent<HTMLInputElement>, checked: boolean) => {
        const filter = event.target.value;
        if (checked) setFilters((prev) => [...prev, filter]);
        else setFilters((prev) => prev.filter((_filter) => filter !== _filter));
      };
    

    And then if all filters are set correctly, and the Set Filters button is clicked I call the setSearchParams to navigate to the new url and fire new HTTP request:

      const setFilters = () => {
        if (filters.length === 0) searchParams.delete('filters');
        else searchParams.set('filters', filters.join(','));
        setSearchParams(searchParams);
      };
    

  2. Setting the SearchParams in your onFilterChange callback will cause the useSearchParams to reset its state, thus rerendering your component. This will rerender your MUI Checkboxes and set a different value as the defaultChecked value.

    I believe using the checked prop instead of the defaultChecked prop would solve the issue. Unfortunately i cannot edit your sandbox to test it myself.

    As you are just manipulating the searchParams yourself while staying on the same page, i think using a dedicated state (which changes on each checkbox click), would fit better, as you dont have to "apply" the filter anymore. I dont really know your usecase

    Hope this helps somehow,
    Mika

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search