#How to properly derive state from the URL

9 messages · Page 1 of 1 (latest)

heady trail
#

I'm currently building a search page and trying to use the URL and search params as the source of truth for the state but I'm running into some issues.

I initially had it as something simpler to below, but found that the back button didn't work (the react state caused it to redirect immediately), so I've had to do this. But it feels overly complicated, which makes me think it's the wrong approach.

For example, I have a select that changes the way the list is sorted and it looks like this:

  className,
  sort,
}: {
  className?: string;
  sort: string;
}) {
  const [selected, setSelected] = React.useState(sort);

  const [isStateUpdating, setIsStateUpdating] = React.useState(false);
  const router = useRouter();
  const pathname = usePathname();
  const searchParams = useSearchParams();
  const newSearchParams = React.useMemo(
    () => new URLSearchParams(searchParams.toString()),
    [searchParams]
  );
  selected !== ""
    ? newSearchParams.set("sort", selected)
    : newSearchParams.delete("sort");
  if (selected !== searchParams.get("sort")) {
    newSearchParams.delete("page");
  }

  React.useEffect(() => {
    if (
      isStateUpdating &&
      searchParams.toString() !== newSearchParams.toString()
    ) {
      setIsStateUpdating(false);
      router.push(`${pathname}?${newSearchParams.toString()}`);
    }
  }, [isStateUpdating, newSearchParams, pathname, router, searchParams]);

  if (
    !isStateUpdating &&
    searchParams.toString() !== newSearchParams.toString()
  ) {
    setSelected(searchParams.get("sort") || "relevant");
  }

return <Insert Select Component here />
}

Is there a better way to do this?

fringe kindleBOT
#

🔎 This post has been indexed in our web forum and will be seen by search engines so other users can find it outside Discord

      🕵️ Your user profile is private by default and won't be visible to users outside Discord, if you want to be visible in the web forum you can add the "Public Profile" role in <id:customize>

      ✅ You can mark a message as the answer for your post with `Right click -> Apps -> Mark Solution`
      (if you don't see the option, try refreshing Discord with Ctrl + R)
dawn island
#

If you want to keep the URL the only source of truth, you don't need any additional state.
The useEffect also seems to be redundant.

function CompanySelect() {
  const searchParams = useSearchParams();
  const router = useRouter();

  return (
    <select
        onChange={(e) => {

          // Create a new copy of url params
          const params = new URLSearchParams(
            Array.from(searchParams.entries())
          );

          // Update URL with the new sort value but keep other search params intact
          params.set("sort", e.target.value);
          router.push(`${pathname}?${params.toString()}`);
        }}
      >
        <option value="asc">Asc</option>
        <option value="desc">Desc</option>
      </select>
  )
}
heady trail
#

Hey @dawn island thanks! That does simplify it for this component. Would the same hold true for a more complicated component like a combobox with multi select?

sleek saddle
heady trail
#

@sleek saddle - so I would just derive it all via the URL instead of having the state in react + delete from url instead of deleting from stand in the unselect/handlekeydown events?

  const [open, setOpen] = React.useState(false);
  const results = json.results;
  const [inputValue, setInputValue] = React.useState("");
  const inputRef = React.useRef<HTMLInputElement>(null);

  const initialSearchParams = useSearchParams();
  const benefits = initialSearchParams.get("benefits")?.split(",");
  const benefitsArr = getInitialBenefits({ benefits, results });
  const [selected, setSelected] = React.useState<Perk[]>(benefitsArr);

  const newSearchParam = selected
    .map((item) => item.value.toLowerCase().replace(/ /g, "-"))
    .join(",");

  const { searchParams, isStateUpdating, setIsStateUpdating } =
    useUpdateSearchParam({
      key: "benefits",
      newSearchParam: newSearchParam,
    });

  const handleUnselect = React.useCallback(
    (result: Perk) => {
      setSelected((prev) => prev.filter((s) => s.value !== result.value));
    },
    [setSelected]
  );

  const handleKeyDown = React.useCallback(
    (e: React.KeyboardEvent<HTMLDivElement>) => {
      const input = inputRef.current;
      if (input) {
        if (e.key === "Delete" || e.key === "Backspace") {
          if (input.value === "") {
            setSelected((prev) => {
              const newSelected = [...prev];
              newSelected.pop();
              return newSelected;
            });
          }
        }
      }
    },
    [setSelected]
  );

  const selectables = matchSorter(results, inputValue, {
    keys: ["value"],
  }).filter(
    (result) =>
      !selected.some((selectedItem) => selectedItem.value === result.value)
  );```
sleek saddle
heady trail
#

In this situation, wouldn't this mean that the combobox wouldn't re-render because there would be no state change to trigger a re-render in react? e.g. at the moment, the component re-renders whenever selected changes but it selected is derived from the URL there wouldn't be any reason for it to re-render? Or am I not thinking about it properly..

sleek saddle