React Router how to update the route twice in one render cycle - reactjs

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?

Related

React Hook not notifying state change to components using the hook

So I have a Hook
export default function useCustomHook() {
const initFrom = localStorage.getItem("startDate") === null? moment().subtract(14, "d"): moment(localStorage.getItem("startDate"));
const initTo = localStorage.getItem("endDate") === null? moment().subtract(1, "d"): moment(localStorage.getItem("endDate"));
const [dates, updateDates] = React.useState({
from: initFrom,
to: initTo
});
const [sessionBreakdown, updateSessionBreakdown] = React.useState(null);
React.useEffect(() => {
api.GET(`/analytics/session-breakdown/${api.getWebsiteGUID()}/${dates.from.format("YYYY-MM-DD")}:${dates.to.format("YYYY-MM-DD")}/0/all/1`).then(res => {
updateSessionBreakdown(res.item);
console.log("Updated session breakdown", res);
})
},[dates])
const setDateRange = React.useCallback((startDate, endDate) => {
const e = moment(endDate);
const s = moment(startDate);
localStorage.setItem("endDate", e._d);
localStorage.setItem("startDate", s._d);
updateDates((prevState) => ({ ...prevState, to:e, from:s}));
}, [])
const getDateRange = () => {
return [dates.from, dates.to];
}
return [sessionBreakdown, getDateRange, setDateRange]
}
Now, this hook appears to be working in the network inspector, if I call the setDateRanger function I can see it makes the call to our API Service, and get the results back.
However, we have several components that are using the sessionBreakdown return result and are not updating when the updateSessionBreakdown is being used.
i can also see the promise from the API call is being fired in the console.
I have created a small version that reproduces the issue I'm having with it at https://codesandbox.io/s/prod-microservice-kq9cck Please note i have changed the code in here so it's not reliant on my API Connector to show the problem,
To update object for useState, recommended way is to use callback and spread operator.
updateDates((prevState) => ({ ...prevState, to:e, from:s}));
Additionally, please use useCallback if you want to use setDateRange function in any other components.
const setDateRange = useCallback((startDate, endDate) => {
const e = moment(endDate);
const s = moment(startDate);
localStorage.setItem("endDate", e._d);
localStorage.setItem("startDate", s._d);
updateDates((prevState) => ({ ...prevState, to:e, from:s}));
}, [])
Found the problem:
You are calling CustomHook in 2 components separately, it means your local state instance created separately for those components. So Even though you update state in one component, it does not effect to another component.
To solve problem, call your hook in parent component and pass the states to Display components as props.
Here is the codesandbox. You need to use this way to update in one child components and use in another one.
If wont's props drilling, use Global state solution.

Doing a router push in useEffect causes the effect to run in an infinite loop

I am using NextJs, and I have a component that uses useDebounceHook.
The component looks like this.
import { useRouter } from 'next/router';
function SearchComponent() {
const router = useRouter();
const [searchResults, setSearchResults] = useState([]);
const [searchTerm, setSearchTerm] = useState<string>('');
const debouncedSearchTerm: string = useDebounce<string>(searchTerm, 250);
const handleSearchChange = (event) => {
setSearchTerm(event.target.value);
};
useEffect(() => {
const newPath = `/search?q=${debouncedSearchTerm}`;
router.push(newPath, undefined, { shallow: true });
}, [debouncedSearchTerm]);
// eslint complains above that router is missing and if we add, the function runs infintely.
useEffect(() => {
fetchQueryResults()
.then((data) => {
setSearchResults(data)l
})
}, [router.query.q]);
return (
<InputField
placeholder="Search"
value={searchTerm}
onChange={handleSearchChange}
/>
{renderSearchResults()}
)
}
// useDebounceHook reference: https://usehooks.com/useDebounce/
The component listens to the search change event and immediately updates the value as it needs to be visible on the screen textbox. However, it debounces the value for fetching search results.
And we want to do the fetch from the URL route as we want it to be bookmarkable. Hence we push the query param to the URL once the debounce value changes rather than directly fetching.
Here the problem is Eslint complains that we are missing router from the dependency array. If we add it, it goes into an infinite loop.
How to solve this issue? Is it ok if we skip adding the router to the dependency array?
One option is to only do the router push if it changes q, i.e.
useEffect(() => {
const newPath = `/search?q=${debouncedSearchTerm}`;
if (debouncedSearchTerm !== router.query.q) {
router.push(newPath, undefined, { shallow: true });
}
}, [router, debouncedSearchTerm]);
However I think it's also fine to omit router as a dependency here (and suppress the lint), since you don't need to re-navigate if the router changes (generally when dependencies change you want to rerun the effect, but here router doesn't seem necessary).
One thing to consider is if the page changes from some other reason (ex. they click a link on the page which takes them to "search?q=blah"), what should happen? Should the effect run taking them back to search?q=searchTerm? This is essentially the case being handled here.
I did this temporary fix until the larger issue is resolved from nextjs:
useEffect(() => {
const newPath = `/search?q=${debouncedSearchTerm}`;
router.push(newPath, undefined, { shallow: true });
// This is a hack to prevent the redirect infinite loop problem
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [undefined]);

useLocation Hook returns wrong path

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*/);

How to run a function when user clicks the back button, in React.js?

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.

React native screens not re-rendering when custom hook state got changed

I am using a custom hook in app purchases.
const useInAppPurchase = () => {
const context = useContext(AuthGlobal)
const [isFullAppPurchased, setIsFullAppPurchased] = useState(false)
useEffect(() => {
console.log(`InAppPurchase useEffect is called`)
getProductsIAP()
return async () => {
try {
await disconnectAsync()
} catch (error) {}
}
}, [])
....
}
When I used this hook at AccountScreen (where I do the purchase) Account screen is getting re-rendered once the payment is done.
i.e. isFullAppPurchased is changing from false -> true
const AccountScreen = (props) => {
const width = useWindowDimensions().width
const {
isFullAppPurchased,
} = useInAppPurchase()
return (
// value is true after the purchase
<Text>{isFullAppPurchased}</Text>
)
}
But I am using the same hook in CategoryList screen and after the payment is done when I navigate to the CategoryList screen, The values (isFullAppPurchased) is not updated (still false).
But when I do the re-rendering manually then I get isFullAppPurchased as true.
const CategoryList = (props) => {
const navigation = useNavigation()
const { isFullAppPurchased } = useInAppPurchase()
return (
// still value is false
<Text>{isFullAppPurchased}</Text>
)
}
What is the reason for this behaviour ? How should I re-render CategoryList screen once the payment is done ?
Thank you.
I see hook make API request only on mount, if whole parent component didn't unmount and rendered a new, value of hook stays same.
E.g. dependencies array is empty - [] so hook doesn't request data again.
Probably better idea is to pass isFullAppPurchased via context or redux from top level.
And put state and function to update that state in same place.

Resources