How to combine react-router-dom's setSearchParams with route that has a param in a way that allows correct back button usage?

Viewed 129

I have a set of routes that look like this:

{
    path: '/things',
    element: <MainLayout />,
    children: [
        {
            path: 'search',
            element: <MainSearch />,
        },
        {
            path: 'search/:thingId',
            element: <ThingLayout />,
        },
        {
            path: '',
            element: <Navigate to="search" replace />,
        },
    ],
},

Wherein the idea is you search for something on MainSearch, are given results, click it, and are brought to a page /things/search/:thingId for that Thing.

However, ThingLayout has a tab paradigm happening too, which is set by search params, and the user clicking tabs within that component.

ThingLayout.tsx

import { useParams, useSearchParams } from 'react-router-dom';

const ThingLayout = () => {
    const [searchParams, setSearchParams] = useSearchParams();
    const activeTab = searchParams.get('tab');

    // When the user first comes here from general search route, 
    // set the default tab
    useEffect(() => {
        if (!activeTab) {
            setSearchParams({ tab: DEFAULT_TAB });
        }
    }, []);

// ...
}

My trouble is, I need (I believe) the generalized /search/:thingId route so that all my various /search/:thingId?tab=someTab routes resolve to this component, which then has code to check which tab is set via searchParams and then render the proper child component, but if a user presses the back button, the URL changes to /search/:thingId and then immediately back to /search/:thingId?tab=defaultTab. I tried using navigate rather than setSearchParams to change the URL:

import { useNavigate } from 'react-router-dom';

// ...
    const navigate = useNavigate();
    navigate(`?tab=${DEFAULT_INVENTORY_PARTS_TAB}`)

But I had the same issue: when coming from search, first the URL would be /search/:thingId, then it would become ?tab=defaultTab.

I've searched through the react router docs, as well as looked at a great many stackoverflow questions, and I'm thinking now maybe I just am following a bad pattern? Is my method of tab navigation compatible with the "right" way to use React Router? How can I combine the general :id route with my manipulated searchParams route?

My react router version is "react-router-dom": "^6.2.1"

2 Answers

The setSearchParams works similar to the navigate function but only for the URL queryString. It takes an options object that is the same type argument as navigate.

declare function useSearchParams(
  defaultInit?: URLSearchParamsInit
): [URLSearchParams, SetURLSearchParams];

type ParamKeyValuePair = [string, string];

type URLSearchParamsInit =
  | string
  | ParamKeyValuePair[]
  | Record<string, string | string[]>
  | URLSearchParams;

type SetURLSearchParams = (
  nextInit?: URLSearchParamsInit,
  navigateOpts?: : { replace?: boolean; state?: any }
) => void;

Pass a navigateOpts object that sets the navigation type to be a redirect.

import { useParams, useSearchParams } from 'react-router-dom';

const ThingLayout = () => {
  const [searchParams, setSearchParams] = useSearchParams();
  const activeTab = searchParams.get('tab');

  // When the user first comes here from general search route, 
  // set the default tab
  useEffect(() => {
    if (!activeTab) {
      searchParams.set("tab", DEFAULT_TAB);
      setSearchParams(searchParams, { replace: true });
    }
  }, []);

  // ...
}

The answer is to use the navigate function provided by react-router-dom's useNavigate hook, which has a replace parameter that can be passed to it, to replace the current item in history.

So my code that checks to see if there is a tab selected, and if not, sets the search parameter, now navigates to the URL with the proper search parameter set, rather than setting the search parameter directly and only.

import { useParams, useSearchParams } from 'react-router-dom';

const ThingLayout = () => {
    const [searchParams, setSearchParams] = useSearchParams();
    const activeTab = searchParams.get('tab');

    // When the user first comes here from general search route, 
    // set the default tab
    useEffect(() => {
        if (!activeTab) {
            setSearchParams({ tab: DEFAULT_TAB });
        }
    }, []);

// ...
}

becomes

import { useSearchParams, useNavigate } from 'react-router-dom';

const ThingLayout = () => {
    const [searchParams] = useSearchParams();
    const navigate = useNavigate();
    const activeTab = searchParams.get('tab');

    // When the user first comes here from general search route, 
    // set the default tab
    useEffect(() => {
        if (!activeTab) {
            navigate(`?tab=${DEFAULT_TAB}`, {replace: true});
        }
    }, []);

// ...
}
Related