I built an Ionic-React App that can scan a QR-Code and connect to a Device based on that. I use the useLocation() and useHistory() Hooks to route through my App.
I pass some Data like that:
const Home: React.FC = () => {
let history = useHistory()
const startScan = async () => {
//Some Barcode Logic
history.replace("/gatherData", {scan: result.content})
}};
and recieve it using:
const GatherData: React.FC = () => {
let history = useHistory();
let location = useLocation();
useIonViewWillEnter(() => {
console.log(location);
});
I did this, as usual and it seemed to be working quite nicely. But now, somehow, the useLocation() Hook does not recognize the history changing anymore. Although I get routed to the next page, the useLocation() returns an old path:
{pathname: '/home', ... state: undefined, key: 'bdwus9'}
I did try to recode the thing but that didn't solve the issue. Also trying to catch bugs in the HomeFC and in the GatherDataFC didn't help. It would be great if anyone had a solution to the problem...
Your useIonViewWillEnter currently gets an obsolete state because it's cached, just like many other hooks. To fix this, you need to explicitly tell it that its dependency location was changed.
useIonViewWillEnter(() => {
console.log(location);
}, [location]/*depdendency array*/);
Related
I'm using react-router v5 and I want to upgrade to v6. there is a problem: I have many components and use useHistory inside them but I don't want to change them one by one to useNavigate instead of that I want to write a helper function for navigate(history) in react-router v6 and then use that helper(wrapper) function.
Question is: How to write a helper function for useNavigate in react router v6?
thank you for your help!
If you are wanting to provide a custom navigation utility then you'll likely want to create a custom React hook that "wraps" one of the hooks provided from react-router and return the functions you need. Unfortunately react-router#6 doesn't directly expose out the history object, and there isn't a 1-to-1 relationship between the history object and the navigate function. You can cover most common use cases though. I suggest adhering to a "common API" that fits your needs.
RRDv5 Example
import { useHistory } from 'react-router';
const useMyNavigation = () => {
const history = useHistory();
const push = React.useCallback((to, state) => history.push(to, state), []);
const replace = React.useCallback((to, state) => history.replace(to, state), []);
const go = React.useCallback((delta) => history.go(delta), []);
const back = React.useCallback(() => history.back(-1), []);
const forward = React.useCallback(() => history.go(1), []);
return {
back
forward,
go,
push,
replace
};
};
RRDv6 Example
import { useNavigate } from 'react-router';
const useMyNavigation = () => {
const navigate = useNavigate();
const push = React.useCallback((to, state) => navigate(to, { state }), []);
const replace = React.useCallback((to, state) => navigate(to, { replace: true, state }), []);
const go = React.useCallback((delta) => navigate(delta), []);
const back = React.useCallback(() => navigate(-1), []);
const forward = React.useCallback(() => navigate(1), []);
return {
back
forward,
go,
push,
replace
};
};
Alternative RRDv6 Example
Now, when I said that RRDv6 doesn't directly expose out the history object, this isn't entirely true. It does not directly export the history object, but it does export the React context holding it via the UNSAFE_NavigationContext context. With this you can get a reference to the navigator function which is a history#5 history object (as opposed to the history#4 that RRDv5 uses). Note that this likely requires history#5 (the same version used by react-router#6) to be listed as a project dependency. See history#5 Navigation.
import { useContext } from 'react';
import { useNavigate } from 'react-router';
const useMyNavigation = () => {
const navigator = useContext(UNSAFE_NavigationContext).navigator;
const push = React.useCallback((to, state) => navigator.push(to, state), []);
const replace = React.useCallback((to, state) => navigator.replace(to, state), []);
const go = React.useCallback((delta) => navigator.go(delta), []);
const back = React.useCallback(() => navigator.back(-1), []);
const forward = React.useCallback(() => navigator.go(1), []);
return {
back
forward,
go,
push,
replace
};
};
I looked around and tried to find a solution with React router.
With V5 you can use <Promt />.
I tried also to find a vanilla JavaScript solution, but nothing worked for me.
I use React router v6 and histroy is replaced with const navigate = useNavigation() which doesn't have a .listen attribute.
Further v6 doesn't have a <Promt /> component.
Nevertheless, at the end I used useEffect clear function. But this works for all changes of component. Also when going forward.
According to the react.js docs, "React performs the cleanup when the component unmounts."
useEffect(() => {
// If user clicks the back button run function
return resetValues();;
})
Currently the Prompt component (and usePrompt and useBlocker) isn't supported in react-router-dom#6 but the maintainers appear to have every intention reintroducing it in the future.
If you are simply wanting to run a function when a back navigation (POP action) occurs then a possible solution is to create a custom hook for it using the exported NavigationContext.
Example:
import { UNSAFE_NavigationContext } from "react-router-dom";
const useBackListener = (callback) => {
const navigator = useContext(UNSAFE_NavigationContext).navigator;
useEffect(() => {
const listener = ({ location, action }) => {
console.log("listener", { location, action });
if (action === "POP") {
callback({ location, action });
}
};
const unlisten = navigator.listen(listener);
return unlisten;
}, [callback, navigator]);
};
Usage:
useBackListener(({ location }) =>
console.log("Navigated Back", { location })
);
If using the UNSAFE_NavigationContext context is something you'd prefer to avoid then the alternative is to create a custom route that can use a custom history object (i.e. from createBrowserHistory) and use the normal history.listen. See my answer here for details.
I think it is an obvious limitation in React & React Router, but I'm asking maybe someone will have another idea, or someone else that will come here and will understand it impossible.
I have two custom hooks to do some logic related to the navigation. In the code below I tried to remove all the irrelevant code, but you can see the original code here.
Custom Hook to control the page:
const usePageNavigate = () => {
const history = useHistory()
const navigate = useCallback((newPage) => history.push(newPage), [history])
return navigate
}
Custom Hook to constol the QueryString:
const useMoreInfo = () => {
const { pathname } = useLocation()
const history = useHistory()
const setQuery = useCallback((value) => history.replace(pathname + `?myData=${value}`), [history, pathname])
return setQuery
}
Now you can see the *QueryString hook is used the location to concatenate the query string to the current path.
The problem is that running these two in the same render cycle (for example, in useEffect or in act in the test) causes that only the latest one will be the end result of the router.
const NavigateTo = ({ page, user }) => {
const toPage = usePageNavigate()
const withDate = useMoreInfo()
useEffect(() => {
toPage(page)
withDate(user)
}, [])
return <div></div>
}
What can I do (except creating another hook to do both changes as one)?
Should/Can I implement a kind of "events bus" to collect all the history changes and commit them at the end of the loop?
Any other ideas?
I am trying to make a useEffect fire every time the query params of my URL changes, this is the format of the URL:
https://localhost:3000/main/persons?results=1
In my code I have the following:
const location = useLocation();
useEffect(() => {
console.log('Location update')
}, [location]);
However, my problem is that the useEffect is only run when the location.pathname changes, and not when the query parameters of the URL changes (?results=1). I have also tried the following logic: [location.pathname, location.search] but with no luck.
Do anyone know how I can solve this?
Try doing this:
const { pathName } = useLocation();
useEffect(() => {
console.log('Location update')
}, [pathName]);
Similarly for location.search
For URL params use useParams in your hook
let { results} = useParams();
useEffect(() => {
console.log('Params updated',results)
}, [results]);
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