How peristent is router state in react? - reactjs

I have an app where I set the store state as follow :
history.replace(location.pathname, {myValue: 1});
I know, one way to clear it is doing history.replace(location.pathname, {});
but I was wondering, what other way this state is replaced ?
It looks like this happen when I click on a <Link to={"/new/url"}/> but are there other situation ? how persistend is that state ?

The easiest way is to use connected-react-router if you are using Redux.
Otherwise, you have to build it by yourself.
Basically, you need to have a listener to catch the event "popstate".
A popstate event is dispatched to the window each time the active
history entry changes between two history entries for the same
document
(Mozila MDN Web docs)
Here is a very basic example:
function useLocation () {
const [location, setLocation] = useState(window.location)
useEffect(() => {
window.addEventListener('popstate', handleChange)
return () => window.removeEventListener('popstate', handleChange)
}, [])
const handleChange = ()=> setLocation(window.location);
}

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.

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 Router how to update the route twice in one render cycle

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?

How to detect route changes using React Router in React?

I have a search component that is global to my application, and displays search results right at the top. Once the user does any sort of navigation, e.g., clicking a search result or using the back button on the browser, I want to reset the search: clear the results and the search input field.
Currently, I am handling this with a Context; I have a SearchContext.Provider that broadcasts the resetSearch function, and wherever I am handling navigation, I have consumers that invoke resetSearch before processing navigation (which I do programmatically with the useHistory hook).
This doesn't work for back button presses on the browser's controls (since that is something out of my control).
Is there an intermediate step before my Routes are rendered (or any browser navigation happens) that I can hook into, to make sure my resetSearch function is invoked?
As requested, here is the code:
// App.js
const [query, setQuery] = useState("");
const [results, setResults] = useState([]);
const resetSearch = () => {
setResults([]);
setQuery("");
};
<SearchContext.Provider value={{ resetSearch }}>
// all of my <Route /> components in this block
</SearchContext.Provider>
// BusRoute.js
const { resetSearch } = useContext(SearchContext);
const handleClick = stop => {
resetSearch();
history.push(`/stops/${stop}`);
};
return (
// some other JSX
<button onClick={() => handleClick(stop.code)}>{stop.name}</button>
);
You can listen to history changes:
useEffect(() => {
const unlisten = history.listen((location) => {
console.log('new location: ', location)
// do your magic things here
// reset the search: clear the results and the search input field
})
return function cleanup() {
unlisten()
}
}, [])
You can use this effect in your parent component which controls your global search's value.
You can use componentWillUnmount feature from class components with useEffect in hooks with functional component

How to addEventListener with React Hooks when the function needs to read state?

I use React with function components and hooks - and I am trying to add a scroll listener which modified the state basing on the current value of the state.
My current code looks like this:
const [isAboveTheFoldVisible, setAboveTheFoldVisible] = useState(true);
const scrollHandler = useCallback(
() => {
if (window.pageYOffset >= window.innerHeight) {
if (isAboveTheFoldVisible) {
setAboveTheFoldVisible(false);
}
} else if (!isAboveTheFoldVisible) {
setAboveTheFoldVisible(true);
}
},
[isAboveTheFoldVisible]
);
useEffect(() => {
window.addEventListener('scroll', scrollHandler);
return () => {
window.removeEventListener('scroll', scrollHandler);
};
}, [scrollHandler]);
However, because of the [scrollHandler] in useEffect, the listeners are removed and re-added when isAboveTheFoldVisible is being set... which is not necessary.
Possible solutions that I see:
Have two useEffects, one with adding scroll listener and setting a state like scroll position without reading anything
const [scrollPosition, setScrollPosition] = useState(0)
and then second having the scrollPosition as a [scrollPosition].
I do not really like this solution because it's not really explicit why we're setting the scrollPosition when you see the listener.
Using custom hook like useEventListener https://usehooks.com/useEventListener/
Using ref to reference the handler.
Do not read the value of isAboveTheFoldVisible and set the new state anyway. React compares the old state with the new one and if it's same it won't re-render.

Resources