How to sync React Router Params with state - reactjs

Using React Router Web.
Assume we have a route: /:user?showDetails=true. We know how to get the data from the URL with the useParams hook and something like the useQuery custom hook.
Also, we know how to set this data with history.push(/baruchiro?showDetails=false).
But if we get and set this data, and in case we don't use this to redirect the user from one page to another, but to change the current component (to let the user save its current page view), it's mean that the route is state.
How can I use the route as a state without getting the component dirty with a lot of history.push and useParams?

Update
I published this custom hook as npm package: use-route-as-state
If you want to use the route as state, you need a way to get the route params, and also update them.
You can't avoid using history.push since this is the way you change your "state", your route. But you can hide this command for cleaner code.
Here is an example of how to hide the get and the update in custom hooks, that make them to looks like a regular useState hook:
To use Query Params as state:
import { useHistory, useLocation} from 'react-router-dom'
const useQueryAsState = () => {
const { pathname, search } = useLocation()
const history = useHistory()
// helper method to create an object from URLSearchParams
const params = getQueryParamsAsObject(search)
const updateQuery = (updatedParams) => {
Object.assign(params, updatedParams)
// helper method to convert {key1:value,k:v} to '?key1=value&k=v'
history.replace(pathname + objectToQueryParams(params))
}
return [params, updateQuery]
}
To use Route Params as state:
import { generatePath, useHistory, useRouteMatch } from 'react-router-dom'
const useParamsAsState = () => {
const { path, params } = useRouteMatch()
const history = useHistory()
const updateParams = (updatedParams) => {
Object.assign(params, updatedParams)
history.push(generatePath(path, params))
}
return [params, updateParams]
}
Note to the history.replace in the Query Params code and to the history.push in the Route Params code.
Usage: (Not a real component from my code, sorry if there are compilation issues)
const ExampleComponent = () => {
const [{ user }, updateParams] = useParamsAsState()
const [{ showDetails }, updateQuery] = useQueryAsState()
return <div>
{user}<br/ >{showDetails === 'true' && 'Some details'}
<DropDown ... onSelect={(selected) => updateParams({ user: selected }) />
<Checkbox ... onChange={(isChecked) => updateQuery({ showDetails: isChecked} })} />
</div>
}

I created the following NPM package to store state using URL search params with react-router-dom-v6: https://www.npmjs.com/package/react-use-search-params-state

Related

Why does react-query query not work when parameter from router.query returns undefined on refresh?

When page is refreshed query is lost, disappears from react-query-devtools.
Before Next.js, I was using a react and react-router where I would pull a parameter from the router like this:
const { id } = useParams();
It worked then. With the help of the, Next.js Routing documentation
I have replaced useParams with:
import { usePZDetailData } from "../../hooks/usePZData";
import { useRouter } from "next/router";
const PZDetail = () => {
const router = useRouter();
const { id } = router.query;
const { } = usePZDetailData(id);
return <></>;
};
export default PZDetail;
Does not work on refresh. I found a similar topic, but manually using 'refetch' from react-query in useEffects doesn't seem like a good solution. How to do it then?
Edit
Referring to the comment, I am enclosing the rest of the code, the react-query hook. Together with the one already placed above, it forms a whole.
const fetchPZDetailData = (id) => {
return axiosInstance.get(`documents/pzs/${id}`);
};
export const usePZDetailData = (id) => {
return useQuery(["pzs", id], () => fetchPZDetailData(id), {});
};
Edit 2
I attach PZList page code with <Link> implementation
import Link from "next/link";
import React from "react";
import TableModel from "../../components/TableModel";
import { usePZSData } from "../../hooks/usePZData";
import { createColumnHelper } from "#tanstack/react-table";
type PZProps = {
id: number;
title: string;
entry_into_storage_date: string;
};
const index = () => {
const { data: PZS, isLoading } = usePZSData();
const columnHelper = createColumnHelper<PZProps>();
const columns = [
columnHelper.accessor("title", {
cell: (info) => (
<span>
<Link
href={`/pzs/${info.row.original.id}`}
>{`Dokument ${info.row.original.id}`}</Link>
</span>
),
header: "Tytuł",
}),
columnHelper.accessor("entry_into_storage_date", {
header: "Data wprowadzenia na stan ",
}),
];
return (
<div>
{isLoading ? (
"loading "
) : (
<TableModel data={PZS?.data} columns={columns} />
)}
</div>
);
};
export default index;
What you're experiencing is due to the Next.js' Automatic Static Optimization.
If getServerSideProps or getInitialProps is present in a page, Next.js
will switch to render the page on-demand, per-request (meaning
Server-Side Rendering).
If the above is not the case, Next.js will statically optimize your
page automatically by prerendering the page to static HTML.
During prerendering, the router's query object will be empty since we
do not have query information to provide during this phase. After
hydration, Next.js will trigger an update to your application to
provide the route parameters in the query object.
Since your page doesn't have getServerSideProps or getInitialProps, Next.js statically optimizes it automatically by prerendering it to static HTML. During this process the query string is an empty object, meaning in the first render router.query.id will be undefined. The query string value is only updated after hydration, triggering another render.
In your case, you can work around this by disabling the query if id is undefined. You can do so by passing the enabled option to the useQuery call.
export const usePZDetailData = (id) => {
return useQuery(["pzs", id], () => fetchPZDetailData(id), {
enabled: id
});
};
This will prevent making the request to the API if id is not defined during first render, and will make the request once its value is known after hydration.

How can I call a custom hook which accepts parameter inside useEffect?

In my component I use a function which I want to extract, it uses some hooks for setting url params. I created a custom hook.
function useMyCustomHook() {
const history = useHistory();
const location = useLocation();
const locationParams = new URLSearchParams(location.search);
function myCustomHook(id: string ) {
does something
}
return myCustomHook;
}
So I extracted it like shown above and now I want to use it in my other component and inside a useEffect hook.
const { myCustomHook } = useMyCustomHook(); // It asks me to add parameters,
but I want to pass it inside useEffect
useEffect(() => {
if (something) myCustomHook(myParam);
}, [foo]);
Is this a possible approach? Or is there a better solution where I can extract something with hooks and then reuse it in useEffect with parameters? Thank you!
First you need export your custom Hook, I think if you need return a function with id, that function need be executed each id change.
custom hook
import { useCallback} from "react"
import {useNavigate, useLocation} from "react-router-dom"
export const useMyCustomHook = (id) => {
const navigate = useNavigate() // React-router v6
const location = useLocation()
const locationParams = new URLSearchParams(location.search)
const anyFunction = useCallback(() => {
// does something
}, [id]) // the function execute if "id" change
return anyFunction
}
where you wanna use your custom hook
import {useMyCustomHook} from "your router"
const {anyFunction} = useMyCustomHook(id) // your pass the id
useEffect(() => {
if (something) anyFunction()
}, [foo])
I think this is the better way. useCallback only render the function of the params change.

How to use path in react Router Route for the following format

I need to use the following path in React Router
http://localhost:3000/testgroup/test/callback/?code=2d54b51d
I'm using the following code to declare the path but its not working. Any idea on how to use this? It works on callback/:code but my callback url comes in the above format from the source. Also, how do I extract the code from the url in react router. Please note, I'm using the older version as the current application is in the older version.
<TestRoute
// authenticated={true}
path={`${userRoute}/test/callback/?code=:code`}
component={CallBackHandler}
history={history}
/>
react-router-dom only concerns itself with the path portion of a URL, the queryString isn't used for path matching.
Remove the queryString from the route path.
<TestRoute
// authenticated
path={`${userRoute}/test/callback/`}
component={CallBackHandler}
history={history}
/>
Read the queryString params from the location object in the component once it is mounted. It's useful to create a custom hook to access the location.search and return a memoized URLSearchParams object.
const { useLocation } from 'react-router-dom';
const useSearchParams = () => {
const { search } = useLocation();
return React.useMemo(() => new URLSearchParams(search), [search]);
};
const CallBackHandler = props => {
const searchParams = useSearchParams();
const code = searchParams.get("code");
...
};

You should call navigate() in a React.useEffect(), not when your component is first rendered, useNavigate error

I have a problem with using navigate in one of my context's functions. I have a function called getCurrencyFromPath that is provided to my component via useContext. This function is called in useEffect, only on component's mount, to get a currency from query parameter (example path: /HistoricalRates?currency=USD). If someone would try to pass a currency (into query parameter) that is not valid for my app I'd like to navigate user to a default currency which is USD. So in my function I have an "if" check, if query parameter is ok, if not then navigate. So when user passes this wrong query parameter my page mounts and at once navigate should be called, but it's not and instead I get this error. If i call navigate directly in useEffect it works, there is no error, but as soon as i try to do the same logic with outsourcing this code to context function I get an error.
PS. I want to keep this logic in this context function, putting this directly in useEffect is not what I want to achieve (although it would solve the problem)
// component
const Header: React.FC = () => {
const { getCurrencyFromPath, currency, setCurrency } = React.useContext(HistoricalRatesContext);
const navigate = useNavigate();
React.useEffect(() => {
getCurrencyFromPath();
}, []);
const onPickerValueChange = (currency: Currencies) => {
navigate(`/HistoricalRates?currency=${currency}`);
setCurrency(currency);
};
return (
<div className={classes.chartHeader}>
<PageHeader text={`USD historical rates`}></PageHeader>
<CurrencyPicker className={classes.pickerAdditional} value={currency}
changeValue={onPickerValueChange} blockedCurrencies={[Currencies.PLN]} />
</div>
);
};
export default Header;
// context
import { useNavigate } from 'react-router-dom';
.
.
.
const navigate = useNavigate();
const getCurrencyFromPath = () => {
let currency = query.get('currency') || '';
if (!isValidCurrency(currency)) {
currency = Currencies.USD;
navigate(`/HistoricalRates?currency=${currency}`);
}
setCurrency(currency as Currencies);
};
This part should'nt be in the component of this route /HistoricalRates?currency=${currency}
React.useEffect(() => {
getCurrencyFromPath();
}, []);

How to get data from the route url to put inside the callback function in React router?

I'm working on my search feature. I want to trigger a callback function in the route to fetch all data before it goes into the search component.
Like this:
<Route path="/search/:query" component={QuestionSearchContainer} onChange={()=>store.dispatch(fetchData(query?)) }/>
here is the QuestionSearchContainer:
const mapStateToProps = (state,ownProps) => {
return {
questions: Object.values(state.entities.questions),
currentUser: state.entities.users[state.session.id],
query: ownProps.match.params.query,
url: ownProps.match.url
}}
But how could I get the query keyword in the search url to put inside my fetchData as a parameter? I want to fetch the data and save it to the redux store before going to the QuestionSearchContainer so that I can get all data for questions in the container.
If you don't want to do the data fetching withing your QuestionSearchContainer component, you can make a higher-order component to wrap it with that does your data fetching for you.
You can easily modify this HOC to only return the Wrapped component when the data finishes loading as well. The loading part of this is assuming that fetchData is a redux thunk action creator . useParams is a hook exported from react-router-dom that gives you access to the match params. useDispatch is a hook exported from react-redux that gives you access to your store's dispatch function.
import { useParams } from 'react-router-dom';
import { useDispatch } from 'react-redux';
import { useEffect, useState } from 'react';
const withFetchData = (Component) => ({ children, ...props }) => {
const { query } = useParams();
const [loading, setLoading] = useState(true);
const dispatch = useDispatch();
useEffect(() => {
// Assuming fetchData is a a redux thunk action creator
setLoading(true);
dispatch(fetchData(query)).then(() => {
setLoading(false);
});
}, [query]);
if(loading){
return 'loading...'
}
return <Component {...props} />;
};
const QuestionSearchContainerWithFetchData = withFetchData(
QuestionSearchContainer
);
const Parent = () => {
return (
<Route
path="/search/:query"
component={QuestionSearchContainerWithFetchData}
/>
);
};
Another option is to create a special route that does what you desire. For instance, this OnChangeRoute function would call the callback onChangeParams with the current params every time the params change. In this one, there's a loading prop that you have to pass in as the component itself doesn't care about what you are doing with the params.
import { useEffect, useRef } from "react";
function InnerOnChangeRoute({ loading, onParamsChange, Component, ...rest }) {
const onChangeRef = useRef(onParamsChange);
useEffect(()=>{
onChangeRef.current=onParamsChange;
},[onParamsChange])
useEffect(() => {
onChangeRef.current(rest.match.params);
}, [rest.match.params]);
if(loading){
return 'loading....'
}
return <Component {...rest} />;
}
// A wrapper for <Route> that redirects to the login
// screen if you're not yet authenticated.
function OnChangeRoute({ Component, onParamsChange, loading, ...rest }) {
return (
<Route
{...rest}
render={(data) => (
<InnerOnChangeRoute
Component={Component}
onParamsChange={onParamsChange}
loading={loading}
{...data}
/>
)}
/>
);
}
In general for redux, you have to use dispatch (or mapDispatchToProps in the connector HOC) to run an action that updates the store with your data.
Here are some links that will hopefully help you get redux more under control.
https://redux.js.org/advanced/async-actions
https://redux-toolkit.js.org/usage/usage-guide#asynchronous-logic-and-data-fetching
https://github.com/reduxjs/redux-thunk
Firstly, Route doesn't have an onChange handler. (onEnter was available in previous versions(3 & earlier) of react-router-dom
Since your requirement seems to be specific to a single component(QuestionSearchContainer), using custom hooks or hocs may not be an ideal solution.
You can simply use a useEffect and listen to url changes(query). You can get the query using props.match.params and pass it as an argument to your dispatch callback.
Just make sure to maintain a loading state in redux and render a fallback while data is being fetched.
code snippet
const QuestionSearchContainer = props => {
...
useEffect(() => {
const {query} = props.match.params
console.log(query);
store.dispatch(fetchData(query))
}, [query]);
...
return <div>
{!props.isLoading && <div>My actual question search component with data !</div>}
</div>;
};
export default QuestionSearchContainer;

Resources