Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ReactRouterRoute breaks with React Router v6 #108

Closed
dfijoev opened this issue Jun 5, 2020 · 21 comments
Closed

ReactRouterRoute breaks with React Router v6 #108

dfijoev opened this issue Jun 5, 2020 · 21 comments

Comments

@dfijoev
Copy link

dfijoev commented Jun 5, 2020

This library no longer works with React Router v6 (currently in alpha).

Warning: Failed prop type: Invalid prop `children` supplied to `Route`, expected a ReactNode.
    in Route (created by QueryParamProvider)

React Router v6 no longer allows for functions as child components. The following code from this library breaks:

<ReactRouterRoute>
{(routeProps: any) => {
return (
<LocationProvider
stringifyOptions={stringifyOptionsCached}
{...getLocationProps(routeProps)}
>
{children}
</LocationProvider>
);
}}
</ReactRouterRoute>

The new signature of Route in React Router v6 is:

export interface RouteProps {
  caseSensitive?: boolean;
  children?: React.ReactNode;
  element?: React.ReactElement | null;
  path?: string;
}

https://github.com/ReactTraining/react-router/blob/dev/packages/react-router/index.tsx#L235-L240

@fgblomqvist
Copy link

Any update on this? @pbeshai are you planning on supporting React Router v6? It will probably be out soon.

@pbeshai
Copy link
Owner

pbeshai commented Aug 26, 2020

If anyone has ideas on how to support it, please feel free to create a PR or share your idea here. I haven't even looked at RR 6 yet. I'd be ok with adding a different prop to the QueryParamProvider for RR6 specifically if that's required. Otherwise, I'll probably look at it if I ever decide to switch to RR 6.

@pbeshai pbeshai added the help wanted Extra attention is needed label Aug 26, 2020
@Zertz
Copy link

Zertz commented Oct 16, 2020

For what it's worth, react-router v6 now includes useSearchParams. It's lower level and not as convenient as this library but could serve as a building block toward adding RR6 compatibility.

@pbeshai
Copy link
Owner

pbeshai commented Oct 16, 2020

Wowwww it took so many years for this to happen, finally my dreams of them just providing it are coming close to being realized 😭 😁 Thank you for sharing! Perhaps it will be sufficient to combine their useSearchParams hook with the encoders from https://github.com/pbeshai/serialize-query-params to get a similar experience in the meantime.

@lukasa1993
Copy link

so how we can use this with v6 ?

@noah-potter
Copy link

I don't know if the ReactRouterRoute prop can be supported anymore. That component was being used to get the location, but the React Router v6 Route doesn't seem to support that. I was able to get it working by using the not-yet-merged HistoryRouter similar to this:

import { createBrowserHistory } from 'history'

const history = createBrowserHistory()

function App() {
  return (
    <QueryParamProvider history={history} location={history.location}>
      <ActualApp />
    </QueryParamProvider>
  )
}

ReactDOM.render(
  <HistoryRouter history={history}>
    <App />
  </HistoryRouter>,
  document.getElementById('root')
);

I tried adding this to the examples folder, but I ran into lots of issues trying to get the examples running.

@pbeshai
Copy link
Owner

pbeshai commented Feb 24, 2021

Hi all, finally took some time to see what was needed to get this working. @noah-potter's suggestion might work too, I didn't investigate, but an alternative style is available in the examples directory.

https://github.com/pbeshai/use-query-params/tree/master/examples/react-router-6

Update See a TypeScript version here

Perhaps after release I'll look towards building it into the library, but for now it should give you an idea. The basics repeated here:

import * as React from 'react';
import {
  BrowserRouter,
  Routes,
  Route,
  useNavigate,
  useLocation,
} from 'react-router-dom';
import {
  NumberParam,
  QueryParamProvider,
  useQueryParam,
} from 'use-query-params';

const App = ({}) => {
  return (
    <BrowserRouter>
      {/* adapt for react-router v6 */}
      <QueryParamProvider ReactRouterRoute={RouteAdapter}>
        <Routes>
          <Route path="/" element={<Home />} />
        </Routes>
      </QueryParamProvider>
    </BrowserRouter>
  );
};

/**
 * This is the main thing you need to use to adapt the react-router v6
 * API to what use-query-params expects.
 *
 * Pass this as the `ReactRouterRoute` prop to QueryParamProvider.
 */
const RouteAdapter = ({ children }) => {
  const navigate = useNavigate();
  const location = useLocation();

  const adaptedHistory = React.useMemo(
    () => ({
      replace(location) {
        navigate(location, { replace: true, state: location.state });
      },
      push(location) {
        navigate(location, { replace: false, state: location.state });
      },
    }),
    [navigate]
  );
  return children({ history: adaptedHistory, location });
};

@pbeshai pbeshai removed the help wanted Extra attention is needed label Mar 6, 2021
@SimonSchick
Copy link

v6 has been released now, any chance of integrating the above into the this library?

@jacobgavin
Copy link

jacobgavin commented Nov 24, 2021

If anyone else have come across this issue I made a custom hook that works out of the box with react-router v6. Could probably make it into it's own lib if there is an interest. I haven't had the time to test it that much but it seems to be working fine.

import { isString } from 'lodash';
import { useContext } from 'react';
import { UNSAFE_NavigationContext, useSearchParams } from 'react-router-dom';
import { QueryParamConfig, StringParam } from 'serialize-query-params';

declare type NewValueType<D> = D | ((latestValue: D) => D);
type UrlUpdateType = 'replace' | 'push' | undefined;
type UseSearchParam<D, D2 = D> = [D2, (newValue: NewValueType<D>, updateType?: UrlUpdateType) => void];

export default function useSearchParam<D, D2 = D>(
	name: string,
	config: QueryParamConfig<D, D2> = StringParam as QueryParamConfig<any>,
): UseSearchParam<D, D2> {
	const [searchParams, setSearchParams] = useSearchParams();
	const { navigator } = useContext(UNSAFE_NavigationContext);

	const setNewValue = (valueOrFn: NewValueType<D>, updateType?: UrlUpdateType): void => {
		let newValue;
		if (typeof valueOrFn === 'function') {
		        const value = searchParams.get(name);
			// eslint-disable-next-line @typescript-eslint/ban-types
			newValue = (valueOrFn as Function)(config.decode(value));
		} else {
			newValue = valueOrFn;
		}
		const encodedValue = config.encode(newValue);

		const params = new URLSearchParams((navigator as any).location.search);

		if (isString(encodedValue)) {
			params.set(name, encodedValue);
		} else {
			params.delete(name);
		}
		setSearchParams(params, { replace: updateType === 'replace' });
	};

	const decodedValue = config.decode(searchParams.get(name));
	return [decodedValue, setNewValue];
}

// usage
function MyComponent() {
    const [email, setEmail] = useSearchParam('email', StringParam);
   
    return <button onChange={() => setEmail('elon@musk.com')}>Update to {email} in URL</button>;
}

This will work but be aware that i'm using an unsafe context from react-router const { navigator } = useContext(UNSAFE_NavigationContext); so maybe not put it in production.

There is actually a PR and an issue on react-router where they propose to fix that so that the useSearchParams hook will basically act exactly the same as useQueryParams

@collinalexbell
Copy link

@pbeshai does this solution not re-render the provider and all its children every time location changes? That is what it was doing for me.

@collinalexbell
Copy link

collinalexbell commented Jan 27, 2022

I ended up writing an adapter that transformed react-router's new useSearchParms to the useQueryParams API and then removed use-query-params from my project.

@fireph
Copy link

fireph commented Jan 29, 2022

@kuberlog I have tried just about everything to get use-query-params to work with no luck. I also tried to create an adapter like you mentioned you did but ran into a critical issue. I have multiple nested useQueryParams in my app (parent and children will both be modifying different query params simultaneously) and I cannot get this to work with anything I create or find online, but this behavior works in use-query-params (in react router 5 at least). Does the adapter you wrote support this and if so could you post a code snippet?

@prasadesai
Copy link

prasadesai commented Feb 3, 2022

Hi all, finally took some time to see what was needed to get this working. @noah-potter's suggestion might work too, I didn't investigate, but an alternative style is available in the examples directory.

https://github.com/pbeshai/use-query-params/tree/master/examples/react-router-6

Update See a TypeScript version here

Perhaps after release I'll look towards building it into the library, but for now it should give you an idea. The basics repeated here:

import * as React from 'react';
import {
  BrowserRouter,
  Routes,
  Route,
  useNavigate,
  useLocation,
} from 'react-router-dom';
import {
  NumberParam,
  QueryParamProvider,
  useQueryParam,
} from 'use-query-params';

const App = ({}) => {
  return (
    <BrowserRouter>
      {/* adapt for react-router v6 */}
      <QueryParamProvider ReactRouterRoute={RouteAdapter}>
        <Routes>
          <Route path="/" element={<Home />} />
        </Routes>
      </QueryParamProvider>
    </BrowserRouter>
  );
};

/**
 * This is the main thing you need to use to adapt the react-router v6
 * API to what use-query-params expects.
 *
 * Pass this as the `ReactRouterRoute` prop to QueryParamProvider.
 */
const RouteAdapter = ({ children }) => {
  const navigate = useNavigate();
  const location = useLocation();

  const adaptedHistory = React.useMemo(
    () => ({
      replace(location) {
        navigate(location, { replace: true, state: location.state });
      },
      push(location) {
        navigate(location, { replace: false, state: location.state });
      },
    }),
    [navigate]
  );
  return children({ history: adaptedHistory, location });
};

This same example does not work in the test config file with React router v6, not able to set query parameter with useQueryParams from unit testcases. Could you please suggest?

@utkarsh22garg
Copy link

Hi, any updates here to make use-query-params compatible with react-router v6?

@Resetand
Copy link

Resetand commented May 7, 2022

I ran into some weird bug using this workaround solution with RouteAdapter:

Navigating to a different location causes the inside of the adapted history to be replaced with the stale path and prevents navigation. On the second try, navigation works fine.

It seems that navigation inside adaptedHistory doesn't need to pass the entire location, so I changed the implementation with only search param passing

const RouteV6Adapter: FC<{ children?: ReactNode }> = ({ children }) => {
    const navigate = useNavigate();
    const location = useLocation();

    const adaptedHistory = useMemo(
        () => ({
            push: ({ search, state }: Location) => navigate({ search }, { state }),
            replace: ({ search, state }: Location) => navigate({ search }, { replace: true, state }),
        }),
        [navigate],
    );

    // eslint-disable-next-line @typescript-eslint/ban-ts-comment
    // @ts-ignore
    return children({ history: adaptedHistory, location });
};

@pbeshai what do u think? Maybe better to suggest this solution in examples

@ghost
Copy link

ghost commented Jun 27, 2022

I ran into some weird bug using this workaround solution with RouteAdapter:

Navigating to a different location causes the inside of the adapted history to be replaced with the stale path and prevents navigation. On the second try, navigation works fine.

It seems that navigation inside adaptedHistory doesn't need to pass the entire location, so I changed the implementation with only search param passing

const RouteV6Adapter: FC<{ children?: ReactNode }> = ({ children }) => {
    const navigate = useNavigate();
    const location = useLocation();

    const adaptedHistory = useMemo(
        () => ({
            push: ({ search, state }: Location) => navigate({ search }, { state }),
            replace: ({ search, state }: Location) => navigate({ search }, { replace: true, state }),
        }),
        [navigate],
    );

    // eslint-disable-next-line @typescript-eslint/ban-ts-comment
    // @ts-ignore
    return children({ history: adaptedHistory, location });
};

@pbeshai what do u think? Maybe better to suggest this solution in examples

how do you use this in a React App?

@ghost
Copy link

ghost commented Jun 27, 2022

Here is an Example App code.

import { FC, ReactNode, useMemo } from 'react';
import { BrowserRouter as Router, useNavigate, useLocation } from 'react-router-dom';
import { Provider } from 'react-redux';
import { PersistGate } from 'redux-persist/integration/react';
import { CssBaseline } from '@mui/material';
import { ThemeProvider } from '@mui/material/styles';
import { QueryParamProvider } from 'use-query-params';

import { FullscreenProgress } from './shared/components';
import { persistor, store } from './shared/redux/store';
import theme from './theme';

import Auth from './features/auth/Auth';

import './index.css';

const App = () => {
  return (
    <Provider store={store}>
      <PersistGate loading={<FullscreenProgress />} persistor={persistor}>
        <Router>
          {/* adapt for react-router v6 */}
          <QueryParamProvider ReactRouterRoute={RouteV6Adapter}>
            <ThemeProvider theme={theme}>
              <CssBaseline />
              <Auth />
            </ThemeProvider>
          </QueryParamProvider>
        </Router>
      </PersistGate>
    </Provider>
  );
};

/**
 * This is the main thing you need to use to adapt the react-router v6
 * API to what use-query-params expects.
 *
 * Pass this as the `ReactRouterRoute` prop to QueryParamProvider.
 */
const RouteV6Adapter: FC<{ children?: ReactNode }> = ({ children }) => {
  const navigate = useNavigate();
  const location = useLocation();

  const adaptedHistory = useMemo(
    () => ({
      push: ({ search, state }: Location) => navigate({ search }, { state }),
      replace: ({ search, state }: Location) =>
        navigate({ search }, { replace: true, state })
    }),
    [navigate]
  );

  // eslint-disable-next-line @typescript-eslint/ban-ts-comment
  // @ts-ignore
  return children({ history: adaptedHistory, location });
};

export default App;

@ghost
Copy link

ghost commented Jun 27, 2022

image

@mfka
Copy link

mfka commented Jul 6, 2022

add type definition:

import type { Location } from 'history'

@pbeshai
Copy link
Owner

pbeshai commented Jul 25, 2022

Hi there, please give the React Router 6 adapter in v2.0.0-rc.0 a try and let me know if you run into issues. See the changelog for details.

@pbeshai pbeshai closed this as completed Jul 25, 2022
@finnbuga
Copy link

finnbuga commented Nov 6, 2022

This is how I've typed Location:

import type { Location } from "react-router-dom";
...
replace(location: Location) {

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests