Detect when a user leaves page in Next JS - next.js

I would like to detect when the user leaves the page Next JS. I count 3 ways of leaving a page:
by clicking on a link
by doing an action that triggers router.back, router.push, etc...
by closing the tab (i.e. when beforeunload event is fired
Being able to detect when a page is leaved is very helpful for example, alerting the user some changes have not been saved yet.
I would like something like:
router.beforeLeavingPage(() => {
// my callback
})

I use 'next/router' like NextJs Page for disconnect a socket
import { useEffect } from 'react'
import { useRouter } from 'next/router'
export default function MyPage() {
const router = useRouter()
useEffect(() => {
const exitingFunction = () => {
console.log('exiting...');
};
router.events.on('routeChangeStart', exitingFunction );
return () => {
console.log('unmounting component...');
router.events.off('routeChangeStart', exitingFunction);
};
}, []);
return <>My Page</>
}

router.beforePopState is great for browser back button but not for <Link>s on the page.
Solution found here: https://github.com/vercel/next.js/issues/2694#issuecomment-732990201
... Here is a version with this approach, for anyone who gets to this page
looking for another solution. Note, I have adapted it a bit further
for my requirements.
// prompt the user if they try and leave with unsaved changes
useEffect(() => {
const warningText =
'You have unsaved changes - are you sure you wish to leave this page?';
const handleWindowClose = (e: BeforeUnloadEvent) => {
if (!unsavedChanges) return;
e.preventDefault();
return (e.returnValue = warningText);
};
const handleBrowseAway = () => {
if (!unsavedChanges) return;
if (window.confirm(warningText)) return;
router.events.emit('routeChangeError');
throw 'routeChange aborted.';
};
window.addEventListener('beforeunload', handleWindowClose);
router.events.on('routeChangeStart', handleBrowseAway);
return () => {
window.removeEventListener('beforeunload', handleWindowClose);
router.events.off('routeChangeStart', handleBrowseAway);
};
}, [unsavedChanges]);
So far, it seems to work pretty reliably.
Alternatively you can add an onClick to all the <Link>s yourself.

You can use router.beforePopState check here for examples

I saw two things when coding it :
Knowing when nextjs router would be activated
Knowing when specific browser event would happen
I did a hook that way. It triggers if next router is used, or if there is a classic browser event (closing tab, refreshing)
import SingletonRouter, { Router } from 'next/router';
export function usePreventUserFromErasingContent(shouldPreventLeaving) {
const stringToDisplay = 'Do you want to save before leaving the page ?';
useEffect(() => {
// Prevents tab quit / tab refresh
if (shouldPreventLeaving) {
// Adding window alert if the shop quits without saving
window.onbeforeunload = function () {
return stringToDisplay;
};
} else {
window.onbeforeunload = () => {};
}
if (shouldPreventLeaving) {
// Prevents next routing
SingletonRouter.router.change = (...args) => {
if (confirm(stringToDisplay)) {
return Router.prototype.change.apply(SingletonRouter.router, args);
} else {
return new Promise((resolve, reject) => resolve(false));
}
};
}
return () => {
delete SingletonRouter.router.change;
};
}, [shouldPreventLeaving]);
}
You just have to call your hook in the component you want to cover :
usePreventUserFromErasingContent(isThereModificationNotSaved);
This a boolean I created with useState and edit when needed. This way, it only triggers when needed.

You can use default web api's eventhandler in your react page or component.
if (process.browser) {
window.onbeforeunload = () => {
// your callback
}
}

Browsers heavily restrict permissions and features but this works:
window.confirm: for next.js router event
beforeunload: for broswer reload, closing tab or navigating away
import { useRouter } from 'next/router'
const MyComponent = () => {
const router = useRouter()
const unsavedChanges = true
const warningText =
'You have unsaved changes - are you sure you wish to leave this page?'
useEffect(() => {
const handleWindowClose = (e) => {
if (!unsavedChanges) return
e.preventDefault()
return (e.returnValue = warningText)
}
const handleBrowseAway = () => {
if (!unsavedChanges) return
if (window.confirm(warningText)) return
router.events.emit('routeChangeError')
throw 'routeChange aborted.'
}
window.addEventListener('beforeunload', handleWindowClose)
router.events.on('routeChangeStart', handleBrowseAway)
return () => {
window.removeEventListener('beforeunload', handleWindowClose)
router.events.off('routeChangeStart', handleBrowseAway)
}
}, [unsavedChanges])
}
export default MyComponent
Credit to this article

this worked for me in next-router / react-FC
add router event handler
add onBeforeUnload event handler
unload them when component unmounted
https://github.com/vercel/next.js/issues/2476#issuecomment-563190607

You can use the react-use npm package
import { useEffect } from "react";
import Router from "next/router";
import { useBeforeUnload } from "react-use";
export const useLeavePageConfirm = (
isConfirm = true,
message = "Are you sure want to leave this page?"
) => {
useBeforeUnload(isConfirm, message);
useEffect(() => {
const handler = () => {
if (isConfirm && !window.confirm(message)) {
throw "Route Canceled";
}
};
Router.events.on("routeChangeStart", handler);
return () => {
Router.events.off("routeChangeStart", handler);
};
}, [isConfirm, message]);
};

Related

Next.js: Correct way to get dynamic route param when change triggered by Next.Link

I have a kanban app build with Next.js. I currently have two boards:
{"name": "New Board", "id": "6db0ceec-d371-4b53-8065-2eeebac4694a"}
{"name": "tired": "cc41d33e-43a1-49bd-8b76-18e46417b27a"}
I have a menu which maps over next Link, rendering links like so:
<Link href={`/board/${board.id}`}>{board.name}</Link>
I then have the following:
src/pages/board/[boardId].js (page)
src/pages/api/board/[boardId].js (API end point)
In the page, I've defined an async function which sends a GET request to the end point that retrieves the data. For SSR, it's called in getServerSideProps() (this would be called when a user navigates to a specific board page from another part of the app). For client-side, I call this in an effect. (This is called when the user is already on the board page but they select a different board from the menu).
The issue I am having is figuring out the correct Next.js idiomatic way to get the new id from the route when it is changed. I've tried using router.query and router.asPath. However, it often gives me the old value (before the route changed). The only way I am reliably able to get the correct param when the route changes is to use window.location.pathname.split('/')[2].
I will include the source code for the page as well as some console.log() output which will show how the three methods of getting the id from the route are inconsistent (window is always correct) as I switch back and forth between the two boards by clicking the Links in the menu:
// src/pages/board/[boardId].js
import React, { useEffect, useState } from 'react'
import { useDispatch } from 'react-redux'
import supabase from 'Utilities/SupabaseClient'
import Board from 'Components/Screens/Board/Board'
import { useRouter } from 'next/router'
import axios from 'axios'
import { getBaseUrl } from 'Utilities'
import { hydrateTasks } from 'Redux/Reducers/TaskSlice'
const BoardPage = (props) => {
const router = useRouter()
const dispatch = useDispatch()
async function handleRouteChange() {
const { asPath } = router
const { boardId } = router.query // sometimes this does not update!
const idFromWindow = window.location.pathname.split('/')[2]
const { board, tasks } = await handleFetchData({boardId: idFromWindow})
console.log(`hello from handleRouteChange:\n\nFrom window: ${idFromWindow}\n\nFrom router.query: ${boardId}\n\nFrom router.asPath: ${asPath}`)
dispatch(hydrateTasks({board, tasks}))
}
useEffect(() => {
//subscribe
router.events.on('routeChangeComplete', handleRouteChange);
//unsubscribe
return () => router.events.off('routeChangeComplete', handleRouteChange);
}, [ router.events]);
return (
<Board {...props}/>
)
}
const handleFetchData = async ({boardId, req}) => {
const baseUrl = getBaseUrl(req)
return axios.get(`${baseUrl}/api/board/${boardId}`)
.then(({data}) => data)
.catch(err => { console.log(err)})
}
export async function getServerSideProps ({ query, req }) {
const { user } = await supabase.auth.api.getUserByCookie(req)
if (!user) {
return { props: {}, redirect: { destination: '/signin' } }
}
const { boardId } = query
const { board, tasks} = await handleFetchData({boardId, req})
return { props: { user, board, tasks } }
}
export default BoardPage
Starting from the "tired" board, I click back and forth between "New Board" and "tired". Observe the console output. The window is always correct. The router is frequently wrong:
// click 1
[boardId].js?0a51:19 hello from handleRouteChange:
From window: 6db0ceec-d371-4b53-8065-2eeebac4694a
From router.query: cc41d33e-43a1-49bd-8b76-18e46417b27a
From router.asPath: /board/cc41d33e-43a1-49bd-8b76-18e46417b27a
// click 2
[boardId].js?0a51:19 hello from handleRouteChange:
From window: cc41d33e-43a1-49bd-8b76-18e46417b27a
From router.query: cc41d33e-43a1-49bd-8b76-18e46417b27a
From router.asPath: /board/cc41d33e-43a1-49bd-8b76-18e46417b27a
// click 3
[boardId].js?0a51:19 hello from handleRouteChange:
From window: 6db0ceec-d371-4b53-8065-2eeebac4694a
From router.query: cc41d33e-43a1-49bd-8b76-18e46417b27a
From router.asPath: /board/cc41d33e-43a1-49bd-8b76-18e46417b27a
// click 4
[boardId].js?0a51:19 hello from handleRouteChange:
From window: cc41d33e-43a1-49bd-8b76-18e46417b27a
From router.query: cc41d33e-43a1-49bd-8b76-18e46417b27a
From router.asPath: /board/cc41d33e-43a1-49bd-8b76-18e46417b27a
I'm new to Next.js, so it's possible I am going about this the wrong way...
How I have done this is -
Suppose I have a page called localhost:3000/board
I have done this with state, and not with [boardId] (lets called this state as boardId and initialvalue be null)
Suppose a user from anywhere in the app visit this page, using the Link
<Link href="/board">
Go To Board
</Link>
on the page mount I try to read the value of boardId from url such as -
useEffect(() => {
if (router.query && router.query.boardId )
{
setBoardId(router.query.boardId);
}
}, []);
and if fount I set the state of boardId, also I do this to get the data from API
useEffect(() => {
if (boardId) getBoardIdDataFromApi();
}, [boardId] );
In the above Case the board Id will be null as I'm not passing any Id as params to the url. (In my case I create a new board here)
Case 2 - suppose a User visit this board page with something like this, from anywhere in the page -
<Link
href={{
pathname: "/board",
query: { boardId: boardId },
}}
>
this time url will be like
localhost:3000?boardId=AnyBoardId
and this will load the Id and get actual data from the api, or change the layout accodringingly.
useEffect(() => {
if (router.query && router.query.boardId )
{
setBoardId(router.query.boardId);
}
}, []);
Case - 3
Now when a user change the boaardId fromt being on the page itself, you can do -
const onChangeBoard = (v) => {
router.push('/board?boardId=${v}', undefined, { shallow: true })
setboardId(v);
}
This will upadte the state of boardId and fetch the data once the user chooses a different board and update the url.
I'm experimenting with {shallow:true}, and I have all the data fetching mechanisms on the client side.
For you -
you can block getServerSideProps for Case 1
Use getServerSideProps for case 2
For case 3, if you remove shallow, you can again use getServerSideProps but please verify.
This may not be the exact answer. but can help you to understand the logic
Okay I got this working by checking the effect to:
useEffect(() => {
async function handleRouteChange() {
const { boardId } = router.query
const { board, tasks } = await handleFetchData({ boardId })
dispatch(hydrateTasks({ board, tasks }))
}
handleRouteChange()
}, [router])
Here is the complete code for the page now:
// src/pages/board/[boardId].js
import React, { useEffect } from 'react'
import { useDispatch } from 'react-redux'
import supabase from 'Utilities/SupabaseClient'
import Board from 'Components/Screens/Board/Board'
import { useRouter } from 'next/router'
import axios from 'axios'
import { getBaseUrl } from 'Utilities'
import { hydrateTasks } from 'Redux/Reducers/TaskSlice'
const BoardPage = (props) => {
const router = useRouter()
const dispatch = useDispatch()
useEffect(() => {
async function handleRouteChange() {
const { boardId } = router.query
const { board, tasks } = await handleFetchData({ boardId })
dispatch(hydrateTasks({ board, tasks }))
}
handleRouteChange()
}, [router])
return (
<Board {...props}/>
)
}
const handleFetchData = async ({boardId, req}) => {
const baseUrl = getBaseUrl(req)
return axios.get(`${baseUrl}/api/board/${boardId}`)
.then(({data}) => data)
.catch(err => { console.log(err)})
}
export async function getServerSideProps ({ query, req }) {
const { user } = await supabase.auth.api.getUserByCookie(req)
if (!user) {
return { props: {}, redirect: { destination: '/signin' } }
}
const { boardId } = query
const { board, tasks} = await handleFetchData({boardId, req})
return { props: { user, board, tasks } }
}
export default BoardPage

React useState() hook returns initial value [duplicate]

I am trying to learn hooks and the useState method has made me confused. I am assigning an initial value to a state in the form of an array. The set method in useState is not working for me, both with and without the spread syntax.
I have made an API on another PC that I am calling and fetching the data which I want to set into the state.
Here is my code:
<div id="root"></div>
<script type="text/babel" defer>
// import React, { useState, useEffect } from "react";
// import ReactDOM from "react-dom";
const { useState, useEffect } = React; // web-browser variant
const StateSelector = () => {
const initialValue = [
{
category: "",
photo: "",
description: "",
id: 0,
name: "",
rating: 0
}
];
const [movies, setMovies] = useState(initialValue);
useEffect(() => {
(async function() {
try {
// const response = await fetch("http://192.168.1.164:5000/movies/display");
// const json = await response.json();
// const result = json.data.result;
const result = [
{
category: "cat1",
description: "desc1",
id: "1546514491119",
name: "randomname2",
photo: null,
rating: "3"
},
{
category: "cat2",
description: "desc1",
id: "1546837819818",
name: "randomname1",
rating: "5"
}
];
console.log("result =", result);
setMovies(result);
console.log("movies =", movies);
} catch (e) {
console.error(e);
}
})();
}, []);
return <p>hello</p>;
};
const rootElement = document.getElementById("root");
ReactDOM.render(<StateSelector />, rootElement);
</script>
<script src="https://unpkg.com/#babel/standalone#7/babel.min.js"></script>
<script src="https://unpkg.com/react#17/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom#17/umd/react-dom.production.min.js"></script>
Neither setMovies(result) nor setMovies(...result) works.
I expect the result variable to be pushed into the movies array.
Much like .setState() in class components created by extending React.Component or React.PureComponent, the state update using the updater provided by useState hook is also asynchronous, and will not be reflected immediately.
Also, the main issue here is not just the asynchronous nature but the fact that state values are used by functions based on their current closures, and state updates will reflect in the next re-render by which the existing closures are not affected, but new ones are created. Now in the current state, the values within hooks are obtained by existing closures, and when a re-render happens, the closures are updated based on whether the function is recreated again or not.
Even if you add a setTimeout the function, though the timeout will run after some time by which the re-render would have happened, the setTimeout will still use the value from its previous closure and not the updated one.
setMovies(result);
console.log(movies) // movies here will not be updated
If you want to perform an action on state update, you need to use the useEffect hook, much like using componentDidUpdate in class components since the setter returned by useState doesn't have a callback pattern
useEffect(() => {
// action on update of movies
}, [movies]);
As far as the syntax to update state is concerned, setMovies(result) will replace the previous movies value in the state with those available from the async request.
However, if you want to merge the response with the previously existing values, you must use the callback syntax of state updation along with the correct use of spread syntax like
setMovies(prevMovies => ([...prevMovies, ...result]));
Additional details to the previous answer:
While React's setState is asynchronous (both classes and hooks), and it's tempting to use that fact to explain the observed behavior, it is not the reason why it happens.
TLDR: The reason is a closure scope around an immutable const value.
Solutions:
read the value in render function (not inside nested functions):
useEffect(() => { setMovies(result) }, [])
console.log(movies)
add the variable into dependencies (and use the react-hooks/exhaustive-deps eslint rule):
useEffect(() => { setMovies(result) }, [])
useEffect(() => { console.log(movies) }, [movies])
use a temporary variable:
useEffect(() => {
const newMovies = result
console.log(newMovies)
setMovies(newMovies)
}, [])
use a mutable reference (if we don't need a state and only want to remember the value - updating a ref doesn't trigger re-render):
const moviesRef = useRef(initialValue)
useEffect(() => {
moviesRef.current = result
console.log(moviesRef.current)
}, [])
Explanation why it happens:
If async was the only reason, it would be possible to await setState().
However, both props and state are assumed to be unchanging during 1 render.
Treat this.state as if it were immutable.
With hooks, this assumption is enhanced by using constant values with the const keyword:
const [state, setState] = useState('initial')
The value might be different between 2 renders, but remains a constant inside the render itself and inside any closures (functions that live longer even after render is finished, e.g. useEffect, event handlers, inside any Promise or setTimeout).
Consider following fake, but synchronous, React-like implementation:
// sync implementation:
let internalState
let renderAgain
const setState = (updateFn) => {
internalState = updateFn(internalState)
renderAgain()
}
const useState = (defaultState) => {
if (!internalState) {
internalState = defaultState
}
return [internalState, setState]
}
const render = (component, node) => {
const {html, handleClick} = component()
node.innerHTML = html
renderAgain = () => render(component, node)
return handleClick
}
// test:
const MyComponent = () => {
const [x, setX] = useState(1)
console.log('in render:', x) // ✅
const handleClick = () => {
setX(current => current + 1)
console.log('in handler/effect/Promise/setTimeout:', x) // ❌ NOT updated
}
return {
html: `<button>${x}</button>`,
handleClick
}
}
const triggerClick = render(MyComponent, document.getElementById('root'))
triggerClick()
triggerClick()
triggerClick()
<div id="root"></div>
I know that there are already very good answers. But I want to give another idea how to solve the same issue, and access the latest 'movie' state, using my module react-useStateRef.
As you understand by using React state you can render the page every time the state change. But by using React ref, you can always get the latest values.
So the module react-useStateRef let you use state's and ref's together. It's backward compatible with React.useState, so you can just replace the import statement
const { useEffect } = React
import { useState } from 'react-usestateref'
const [movies, setMovies] = useState(initialValue);
useEffect(() => {
(async function() {
try {
const result = [
{
id: "1546514491119",
},
];
console.log("result =", result);
setMovies(result);
console.log("movies =", movies.current); // will give you the latest results
} catch (e) {
console.error(e);
}
})();
}, []);
More information:
react-usestsateref
I just finished a rewrite with useReducer, following #kentcdobs article (ref below) which really gave me a solid result that suffers not one bit from these closure problems.
See: https://kentcdodds.com/blog/how-to-use-react-context-effectively
I condensed his readable boilerplate to my preferred level of DRYness -- reading his sandbox implementation will show you how it actually works.
import React from 'react'
// ref: https://kentcdodds.com/blog/how-to-use-react-context-effectively
const ApplicationDispatch = React.createContext()
const ApplicationContext = React.createContext()
function stateReducer(state, action) {
if (state.hasOwnProperty(action.type)) {
return { ...state, [action.type]: state[action.type] = action.newValue };
}
throw new Error(`Unhandled action type: ${action.type}`);
}
const initialState = {
keyCode: '',
testCode: '',
testMode: false,
phoneNumber: '',
resultCode: null,
mobileInfo: '',
configName: '',
appConfig: {},
};
function DispatchProvider({ children }) {
const [state, dispatch] = React.useReducer(stateReducer, initialState);
return (
<ApplicationDispatch.Provider value={dispatch}>
<ApplicationContext.Provider value={state}>
{children}
</ApplicationContext.Provider>
</ApplicationDispatch.Provider>
)
}
function useDispatchable(stateName) {
const context = React.useContext(ApplicationContext);
const dispatch = React.useContext(ApplicationDispatch);
return [context[stateName], newValue => dispatch({ type: stateName, newValue })];
}
function useKeyCode() { return useDispatchable('keyCode'); }
function useTestCode() { return useDispatchable('testCode'); }
function useTestMode() { return useDispatchable('testMode'); }
function usePhoneNumber() { return useDispatchable('phoneNumber'); }
function useResultCode() { return useDispatchable('resultCode'); }
function useMobileInfo() { return useDispatchable('mobileInfo'); }
function useConfigName() { return useDispatchable('configName'); }
function useAppConfig() { return useDispatchable('appConfig'); }
export {
DispatchProvider,
useKeyCode,
useTestCode,
useTestMode,
usePhoneNumber,
useResultCode,
useMobileInfo,
useConfigName,
useAppConfig,
}
With a usage similar to this:
import { useHistory } from "react-router-dom";
// https://react-bootstrap.github.io/components/alerts
import { Container, Row } from 'react-bootstrap';
import { useAppConfig, useKeyCode, usePhoneNumber } from '../../ApplicationDispatchProvider';
import { ControlSet } from '../../components/control-set';
import { keypadClass } from '../../utils/style-utils';
import { MaskedEntry } from '../../components/masked-entry';
import { Messaging } from '../../components/messaging';
import { SimpleKeypad, HandleKeyPress, ALT_ID } from '../../components/simple-keypad';
export const AltIdPage = () => {
const history = useHistory();
const [keyCode, setKeyCode] = useKeyCode();
const [phoneNumber, setPhoneNumber] = usePhoneNumber();
const [appConfig, setAppConfig] = useAppConfig();
const keyPressed = btn => {
const maxLen = appConfig.phoneNumberEntry.entryLen;
const newValue = HandleKeyPress(btn, phoneNumber).slice(0, maxLen);
setPhoneNumber(newValue);
}
const doSubmit = () => {
history.push('s');
}
const disableBtns = phoneNumber.length < appConfig.phoneNumberEntry.entryLen;
return (
<Container fluid className="text-center">
<Row>
<Messaging {...{ msgColors: appConfig.pageColors, msgLines: appConfig.entryMsgs.altIdMsgs }} />
</Row>
<Row>
<MaskedEntry {...{ ...appConfig.phoneNumberEntry, entryColors: appConfig.pageColors, entryLine: phoneNumber }} />
</Row>
<Row>
<SimpleKeypad {...{ keyboardName: ALT_ID, themeName: appConfig.keyTheme, keyPressed, styleClass: keypadClass }} />
</Row>
<Row>
<ControlSet {...{ btnColors: appConfig.buttonColors, disabled: disableBtns, btns: [{ text: 'Submit', click: doSubmit }] }} />
</Row>
</Container>
);
};
AltIdPage.propTypes = {};
Now everything persists smoothly everywhere across all my pages
React's useEffect has its own state/lifecycle. It's related to mutation of state, and it will not update the state until the effect is destroyed.
Just pass a single argument in parameters state or leave it a black array and it will work perfectly.
React.useEffect(() => {
console.log("effect");
(async () => {
try {
let result = await fetch("/query/countries");
const res = await result.json();
let result1 = await fetch("/query/projects");
const res1 = await result1.json();
let result11 = await fetch("/query/regions");
const res11 = await result11.json();
setData({
countries: res,
projects: res1,
regions: res11
});
} catch {}
})(data)
}, [setData])
# or use this
useEffect(() => {
(async () => {
try {
await Promise.all([
fetch("/query/countries").then((response) => response.json()),
fetch("/query/projects").then((response) => response.json()),
fetch("/query/regions").then((response) => response.json())
]).then(([country, project, region]) => {
// console.log(country, project, region);
setData({
countries: country,
projects: project,
regions: region
});
})
} catch {
console.log("data fetch error")
}
})()
}, [setData]);
Alternatively, you can try React.useRef() for instant change in the React hook.
const movies = React.useRef(null);
useEffect(() => {
movies.current='values';
console.log(movies.current)
}, [])
The closure is not the only reason.
Based on the source code of useState (simplified below). Seems to me the value is never assigned right away.
What happens is that an update action is queued when you invoke setValue. And after the schedule kicks in and only when you get to the next render, these update action then is applied to that state.
Which means even we don't have closure issue, react version of useState is not going to give you the new value right away. The new value doesn't even exist until next render.
function useState(initialState) {
let hook;
...
let baseState = hook.memoizedState;
if (hook.queue.pending) {
let firstUpdate = hook.queue.pending.next;
do {
const action = firstUpdate.action;
baseState = action(baseState); // setValue HERE
firstUpdate = firstUpdate.next;
} while (firstUpdate !== hook.queue.pending);
hook.queue.pending = null;
}
hook.memoizedState = baseState;
return [baseState, dispatchAction.bind(null, hook.queue)];
}
function dispatchAction(queue, action) {
const update = {
action,
next: null
};
if (queue.pending === null) {
update.next = update;
} else {
update.next = queue.pending.next;
queue.pending.next = update;
}
queue.pending = update;
isMount = false;
workInProgressHook = fiber.memoizedState;
schedule();
}
There's also an article explaining the above in the similar way, https://dev.to/adamklein/we-don-t-know-how-react-state-hook-works-1lp8
I too was stuck with the same problem. As other answers above have clarified the error here, which is that useState is asynchronous and you are trying to use the value just after setState. It is not updating on the console.log() part because of the asynchronous nature of setState, it lets your further code to execute, while the value updating happens on the background. Thus you are getting the previous value. When the setState is completed on the background it will update the value and you will have access to that value on the next render.
If anyone is interested to understand this in detail. Here is a really good Conference talk on the topic.
https://www.youtube.com/watch?v=8aGhZQkoFbQ
I found this to be good. Instead of defining state (approach 1) as, example,
const initialValue = 1;
const [state,setState] = useState(initialValue)
Try this approach (approach 2),
const [state = initialValue,setState] = useState()
This resolved the rerender issue without using useEffect since we are not concerned with its internal closure approach with this case.
P.S.: If you are concerned with using old state for any use case then useState with useEffect needs to be used since it will need to have that state, so approach 1 shall be used in this situation.
If we have to update state only, then a better way can be if we use the push method to do so.
Here is my code. I want to store URLs from Firebase in state.
const [imageUrl, setImageUrl] = useState([]);
const [reload, setReload] = useState(0);
useEffect(() => {
if (reload === 4) {
downloadUrl1();
}
}, [reload]);
const downloadUrl = async () => {
setImages([]);
try {
for (let i = 0; i < images.length; i++) {
let url = await storage().ref(urls[i].path).getDownloadURL();
imageUrl.push(url);
setImageUrl([...imageUrl]);
console.log(url, 'check', urls.length, 'length', imageUrl.length);
}
}
catch (e) {
console.log(e);
}
};
const handleSubmit = async () => {
setReload(4);
await downloadUrl();
console.log(imageUrl);
console.log('post submitted');
};
This code works to put URLs in state as an array. This might also work for you.
With custom hooks from my library, you can wait for the state values to update:
useAsyncWatcher(...values):watcherFn(peekPrevValue: boolean)=>Promise - is a promise wrapper around useEffect that can wait for updates and return a new value and possibly a previous one if the optional peekPrevValue argument is set to true.
(Live Demo)
import React, { useState, useEffect, useCallback } from "react";
import { useAsyncWatcher } from "use-async-effect2";
function TestComponent(props) {
const [counter, setCounter] = useState(0);
const [text, setText] = useState("");
const textWatcher = useAsyncWatcher(text);
useEffect(() => {
setText(`Counter: ${counter}`);
}, [counter]);
const inc = useCallback(() => {
(async () => {
await new Promise((resolve) => setTimeout(resolve, 1000));
setCounter((counter) => counter + 1);
const updatedText = await textWatcher();
console.log(updatedText);
})();
}, []);
return (
<div className="component">
<div className="caption">useAsyncEffect demo</div>
<div>{counter}</div>
<button onClick={inc}>Inc counter</button>
</div>
);
}
export default TestComponent;
useAsyncDeepState is a deep state implementation (similar to this.setState (patchObject)) whose setter can return a promise synchronized with the internal effect. If the setter is called with no arguments, it does not change the state values, but simply subscribes to state updates. In this case, you can get the state value from anywhere inside your component, since function closures are no longer a hindrance.
(Live Demo)
import React, { useCallback, useEffect } from "react";
import { useAsyncDeepState } from "use-async-effect2";
function TestComponent(props) {
const [state, setState] = useAsyncDeepState({
counter: 0,
computedCounter: 0
});
useEffect(() => {
setState(({ counter }) => ({
computedCounter: counter * 2
}));
}, [state.counter]);
const inc = useCallback(() => {
(async () => {
await new Promise((resolve) => setTimeout(resolve, 1000));
await setState(({ counter }) => ({ counter: counter + 1 }));
console.log("computedCounter=", state.computedCounter);
})();
});
return (
<div className="component">
<div className="caption">useAsyncDeepState demo</div>
<div>state.counter : {state.counter}</div>
<div>state.computedCounter : {state.computedCounter}</div>
<button onClick={() => inc()}>Inc counter</button>
</div>
);
}
var [state,setState]=useState(defaultValue)
useEffect(()=>{
var updatedState
setState(currentState=>{ // Do not change the state by get the updated state
updateState=currentState
return currentState
})
alert(updateState) // the current state.
})
Without any addtional NPM package
//...
const BackendPageListing = () => {
const [ myData, setMyData] = useState( {
id: 1,
content: "abc"
})
const myFunction = ( x ) => {
setPagenateInfo({
...myData,
content: x
})
console.log(myData) // not reflecting change immediately
let myDataNew = {...myData, content: x };
console.log(myDataNew) // Reflecting change immediately
}
return (
<>
<button onClick={()=>{ myFunction("New Content")} }>Update MyData</button>
</>
)
Not saying to do this, but it isn't hard to do what the OP asked without useEffect.
Use a promise to resolve the new state in the body of the setter function:
const getState = <T>(
setState: React.Dispatch<React.SetStateAction<T>>
): Promise<T> => {
return new Promise((resolve) => {
setState((currentState: T) => {
resolve(currentState);
return currentState;
});
});
};
And this is how you use it (example shows the comparison between count and outOfSyncCount/syncCount in the UI rendering):
const App: React.FC = () => {
const [count, setCount] = useState(0);
const [outOfSyncCount, setOutOfSyncCount] = useState(0);
const [syncCount, setSyncCount] = useState(0);
const handleOnClick = async () => {
setCount(count + 1);
// Doesn't work
setOutOfSyncCount(count);
// Works
const newCount = await getState(setCount);
setSyncCount(newCount);
};
return (
<>
<h2>Count = {count}</h2>
<h2>Synced count = {syncCount}</h2>
<h2>Out of sync count = {outOfSyncCount}</h2>
<button onClick={handleOnClick}>Increment</button>
</>
);
};
Use the Background Timer library. It solved my problem.
const timeoutId = BackgroundTimer.setTimeout(() => {
// This will be executed once after 1 seconds
// even when the application is the background
console.log('tac');
}, 1000);
// replace
return <p>hello</p>;
// with
return <p>{JSON.stringify(movies)}</p>;
Now you should see, that your code actually does work. What does not work is the console.log(movies). This is because movies points to the old state. If you move your console.log(movies) outside of useEffect, right above the return, you will see the updated movies object.

Next/Router emit events

I'm writing a custom hook to prevent a user from navigating away if there's unsaved information (in a form) on a page.
To do this, I want to emit a router event when a route change has been cancelled. However, router.events.emit('routeChangeStart') causes a a type error TypeError: Cannot read properties of undefined (reading 'shallow')
import { useRouter } from 'next/router'
import { useEffect, useCallback } from 'react'
export default function usePreventWindowUnload(preventDefault: boolean) {
const confirmMessage = 'Are you sure?'
const router = useRouter()
const onRouterChangeStart = useCallback(() => {
if (preventDefault) {
if (window.confirm(confirmMessage)) {
return true
}
}
// line causing error
router.events.emit('routeChangeError')
throw 'cancelled route change'
,} [preventDefault])
useEffect(() => {
router.events.on('routeChangeStart', onRouteChangeStart)
return () => {
router.events.off('routeChangeStart', onRouteChangeStart)
}
}, [preventDefault])
}
How do I fix this?

Next JS: Warn User for Unsaved Form before Route Change

In Next How can i stop Router Navigation in Next JS.
I am trying to use routerChangeStart event to stop navigation.
useEffect(() => {
const handleRouteChange = (url: string): boolean => {
if (dirty) {
return false;
}
return true;
};
Router.events.on('routeChangeStart', handleRouteChange);
return () => {
Router.events.off('routeChangeStart', handleRouteChange);
};
}, []);
It seems there is no perfect way to this but I handle it with this little trick:
React.useEffect(() => {
const confirmationMessage = 'Changes you made may not be saved.';
const beforeUnloadHandler = (e: BeforeUnloadEvent) => {
(e || window.event).returnValue = confirmationMessage;
return confirmationMessage; // Gecko + Webkit, Safari, Chrome etc.
};
const beforeRouteHandler = (url: string) => {
if (Router.pathname !== url && !confirm(confirmationMessage)) {
// to inform NProgress or something ...
Router.events.emit('routeChangeError');
// tslint:disable-next-line: no-string-throw
throw `Route change to "${url}" was aborted (this error can be safely ignored). See https://github.com/zeit/next.js/issues/2476.`;
}
};
if (notSaved) {
window.addEventListener('beforeunload', beforeUnloadHandler);
Router.events.on('routeChangeStart', beforeRouteHandler);
} else {
window.removeEventListener('beforeunload', beforeUnloadHandler);
Router.events.off('routeChangeStart', beforeRouteHandler);
}
return () => {
window.removeEventListener('beforeunload', beforeUnloadHandler);
Router.events.off('routeChangeStart', beforeRouteHandler);
};
}, [notSaved]);
This code will interrupt changing route (with nextJs Route and also browser refresh / close tab action)
Here's my custom hook solution that seems to cut it, written in TypeScript.
import Router from "next/router"
import { useEffect } from "react"
const useWarnIfUnsavedChanges = (unsavedChanges: boolean, callback: () => boolean) => {
useEffect(() => {
if (unsavedChanges) {
const routeChangeStart = () => {
const ok = callback()
if (!ok) {
Router.events.emit("routeChangeError")
throw "Abort route change. Please ignore this error."
}
}
Router.events.on("routeChangeStart", routeChangeStart)
return () => {
Router.events.off("routeChangeStart", routeChangeStart)
}
}
}, [unsavedChanges])
}
You can use it in your component as follows:
useWarnIfUnsavedChanges(changed, () => {
return confirm("Warning! You have unsaved changes.")
})
You can write a custom hook.
import Router from 'next/router';
import { useEffect } from 'react';
const useWarnIfUnsavedChanges = (unsavedChanges, callback) => {
useEffect(() => {
const routeChangeStart = url => {
if (unsavedChanges) {
Router.events.emit('routeChangeError');
Router.replace(Router, Router.asPath, { shallow: true });
throw 'Abort route change. Please ignore this error.';
}
};
Router.events.on('routeChangeStart', routeChangeStart);
return () => {
Router.events.off('routeChangeStart', routeChangeStart);
};
}, [unsavedChanges]);
};
export default useWarnIfUnsavedChanges;
Take inspiration from: https://github.com/vercel/next.js/discussions/12348#discussioncomment-8089
Thanks #raimohanska for good solution. I did a small update to include confirmation for page reload as well:
/**
* Asks for confirmation to leave/reload if there are unsaved changes.
*/
import Router from 'next/router';
import { useEffect } from 'react';
export const useOnLeavePageConfirmation = (unsavedChanges: boolean) => {
useEffect(() => {
// For reloading.
window.onbeforeunload = () => {
if (unsavedChanges) {
return 'You have unsaved changes. Do you really want to leave?';
}
};
// For changing in-app route.
if (unsavedChanges) {
const routeChangeStart = () => {
const ok = confirm('You have unsaved changes. Do you really want to leave?');
if (!ok) {
Router.events.emit('routeChangeError');
throw 'Abort route change. Please ignore this error.';
}
};
Router.events.on('routeChangeStart', routeChangeStart);
return () => {
Router.events.off('routeChangeStart', routeChangeStart);
};
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [unsavedChanges]);
};
Usage:
useOnLeavePageConfirmation(changesUnsaved);
You need to make a hook that will prevent the router from changing. But for it to work correctly, you should know if your form is pristine or not. To do that with react-final-form they have a FormSpy component that can subscribe to that:
import { Form, FormSpy } from 'react-final-form'
import { useWarnIfUnsaved } from '#hooks/useWarnIfUnsaved'
const [isPristine, setPristine] = useState(true)
useWarnIfUnsaved(!isPristine, () => {
return confirm('Warning! You have unsaved changes.')
})
return (
<Form
render={({ handleSubmit, submitting, submitError }) => {
return (
<>
<FormSpy subscription={{ pristine: true }}>
{(props) => {
setPristine(props.pristine)
return undefined
}}
</FormSpy>
...
And the suggested hook for Typescript from #raimohanska worked for me:
import Router from "next/router"
import { useEffect } from "react"
export const useWarnIfUnsaved = (unsavedChanges: boolean, callback: () => boolean) => {
useEffect(() => {
if (unsavedChanges) {
const routeChangeStart = () => {
const ok = callback()
if (!ok) {
Router.events.emit("routeChangeError")
throw "Abort route change. Please ignore this error."
}
}
Router.events.on("routeChangeStart", routeChangeStart)
return () => {
Router.events.off("routeChangeStart", routeChangeStart)
}
}
}, [unsavedChanges])
}

Access Redux state in custom hook?

I need a custom hook that uses Redux's state. If you were to pass the state from a React component to the function it would look something like:
Custom hook:
function useMyCustomHook(state) {
const { message } = state;
const handleClick = () => {
if(environment_variable) {
// do something with message
} else {
// do something else with message
}
}
return handleClick;
}
My component:
const MyComponent = ({ state }) => {
return <button onClick={()=> useMyCustomHook(state) }>Go</button>
}
It's a bit of a pain to have to pass Redux's state from the React component every time. Is it possible to access the state directly in the custom hook?
With the latest versions of react-redux you could use useSelector hook.
Also note that a hook is not supposed to be called on an handler
import { useSelector } from 'react-redux';
function useMyCustomHook() {
const message = useSelector(state => state.message);
const handleClick = () => {
if(environment_variable) {
// do something with message
} else {
// do something else with message
}
}
return handleClick;
}
and it will be used like
const MyComponent = ({ state }) => {
const handleClick = useMyCustomHook();
return <button onClick={handleClick}>Go</button>
}

Resources