Next JS: Warn User for Unsaved Form before Route Change - next.js

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])
}

Related

Why filter method in my reducer returns an array of proxy? -Redux Toolkit

so i want to delete an item from array, onClick but when i log the filtered data in the console i get an array of Proxy.
i tried Changing my code but nothing worked
whats wrong here in itemRemoved?
import { createSlice, createAction } from "#reduxjs/toolkit";
// Action Creater
const slice = createSlice({
name: "shoppingCart",
initialState: [],
reducers: {
itemAdded: some code // ,
itemRemoved: (cart, { payload }) => {
cart.filter((item) => {
if (item.id === payload.id) {
if (item.count === 1) {
return cart.filter((item) => item.id !== payload.id);
}
else {
const itemIndex = cart.indexOf(item);
cart[itemIndex].count = cart[itemIndex].count - 1;
return cart;
}
}
});
},
},
});
export const { itemAdded, itemRemoved } = slice.actions;
export default slice.reducer;
Assuming you want to remove the element with the id you are passing through the dispatch function
itemRemoved: (state, { payload }) => {
const newCart = state.cart.filter(item => item.id !== payload.id)
const state.cart = newCart
return state
}),

Detect when a user leaves page in 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]);
};

React Native Firebase update information for all users in real time based on other users actions

Hi I'm currently trying to add users to a page using react native, redux, and firebase. When User 1 clicks join, they get added to the feed and likewise for other users. However, a problem I'm facing is when user 2 clicks join, they get added to the feed but don't get displayed on user 1's page unless the user 1 refocuses on the page after going away.
Here is my code for the page itself in react native
import React, { Component } from 'react';
import { Text, View, Button, TouchableOpacity, SafeAreaView, ScrollView, Image } from 'react-native';
import styles from '../styles.js'
import { connect } from 'react-redux'
import { FlatList } from 'react-native-gesture-handler';
import { FontAwesome5 } from '#expo/vector-icons';
import { Octicons } from '#expo/vector-icons';
import { FontAwesome } from '#expo/vector-icons';
import { addUser, removeUser, getLivingRoomUsers } from '../actions/livingRoomUser.js'
import { bindActionCreators } from 'redux'
class LivingRoom extends React.Component {
constructor(props) {
super(props);
this.state = {
inRoom: false,
isMuted: false
};
}
componentDidMount(){
this._unsubscribe = this.props.navigation.addListener('focus', () => {
this.props.getLivingRoomUsers()
});
}
joinRoom = () => {
this.props.addUser()
this.setState({ inRoom: true });
}
leaveRoom = () => {
this.props.removeUser(this.props.livingRoomUser)
this.setState({ inRoom: false });
}
...
render(){
return (
<View>
<SafeAreaView style={styles.livingRoomUserContainer}>
<FlatList
data={this.props.livingRoomUser.feed}
Here is my actions code for the redux portion:
export const addUser = () => {
return async (dispatch, getState) => {
try {
const { user } = getState()
const id = uuid.v4()
const livingRoomUser = {
id: id,
avatar: user.avatar,
username: user.username,
isMuted: false,
date: new Date().getTime(),
}
db.collection('livingroom').doc(id).set(livingRoomUser)
dispatch({
type: 'ADD_USER', payload: livingRoomUser
})
dispatch(getLivingRoomUsers())
} catch (e) {
alert(e)
}
}
}
export const removeUser = (livingRoomUser) => {
return async (dispatch, getState) => {
try {
db.collection('livingroom').doc(livingRoomUser.id).delete();
dispatch(getLivingRoomUsers())
//get living room users
} catch (e) {
alert(e)
}
}
}
export const getLivingRoomUsers = () => {
return async (dispatch, getState) => {
try {
const livingRoomUsers = await db.collection('livingroom').get()
let array = []
livingRoomUsers.forEach((livingRoomUser) => {
array.push(livingRoomUser.data())
})
dispatch({
type: 'GET_LIVING_ROOM_USERS', payload: orderBy(array, 'date', 'asc')
})
} catch (e) {
alert(e)
}
}
}
To summarize. I want the getUsers to be updated anytime someone adds/removes themself from the page. However, from my implementation currently actions only get updated for the current user and the feed only gets updated when the page is focused. How do I go about this?
use onSnapshot listener on the firestore then you can get the latest updates as the store change
export const getLivingRoomUsers = () => {
return async (dispatch, getState) => {
try {
db.collection('livingroom').onSnapshot(snapshot => {
let array = snapshot.docs.map(d => d.data());
dispatch({
type: 'GET_LIVING_ROOM_USERS',
payload: orderBy(array, 'date', 'asc'),
});
});
} catch (e) {
alert(e);
}
};
};

Why, while using useEffect() and .then() in Redux, I get an Error: Actions must be plain objects. Use custom middleware for async actions

using Redux and am now straggling with a signin and signout button while using oauth.
When I press on the button to logIn, the popup window appears and I can choose an account. But in the meantime the webpage throws an error.
I got the following error as stated in the title:
Error: Actions must be plain objects. Use custom middleware for async actions.
I am using hooks, in this case useEffect().then() to fetch the data.
1) Why?
2) Also do not know, why I am getting a warning: The 'onAuthChange' function makes the dependencies of useEffect Hook (at line 35) change on every render. Move it inside the useEffect callback. Alternatively, wrap the 'onAuthChange' definition into its own useCallback() Hook react-hooks/exhaustive-deps
Here is my code:
GoogleAuth.js
import React, { useEffect } from "react";
import { useSelector, useDispatch } from "react-redux";
import { signIn, signOut } from "../actions";
const API_KEY = process.env.REACT_APP_API_KEY;
const GoogleAuth = () => {
const isSignedIn = useSelector((state) => state.auth.isSignedIn);
console.log("IsSignedIn useSelector: " + isSignedIn);
const dispatch = useDispatch();
const onAuthChange = () => {
if (isSignedIn) {
dispatch(signIn());
} else {
dispatch(signOut());
}
};
useEffect(
() => {
window.gapi.load("client:auth2", () => {
window.gapi.client
.init({
clientId: API_KEY,
scope: "email"
})
.then(() => {
onAuthChange(window.gapi.auth2.getAuthInstance().isSignedIn.get());
console.log("isSignedIn.get(): " + window.gapi.auth2.getAuthInstance().isSignedIn.get());
window.gapi.auth2.getAuthInstance().isSignedIn.listen(onAuthChange);
});
});
},
[ onAuthChange ]
);
const onSignInOnClick = () => {
dispatch(window.gapi.auth2.getAuthInstance().signIn());
};
const onSignOutOnClick = () => {
dispatch(window.gapi.auth2.getAuthInstance().signOut());
};
const renderAuthButton = () => {
if (isSignedIn === null) {
return null;
} else if (isSignedIn) {
return (
<button onClick={onSignOutOnClick} className="ui red google button">
<i className="google icon" />
Sign Out
</button>
);
} else {
return (
<button onClick={onSignInOnClick} className="ui red google button">
<i className="google icon" />
Sign In with Google
</button>
);
}
};
return <div>{renderAuthButton()}</div>;
};
export default GoogleAuth;
reducer/index.js
import { combineReducers } from "redux";
import authReducer from "./authReducer";
export default combineReducers({
auth: authReducer
});
reducers/authReducer.js
import { SIGN_IN, SIGN_OUT } from "../actions/types";
const INITIAL_STATE = {
isSignedIn: null
};
export default (state = INITIAL_STATE, action) => {
switch (action.type) {
case SIGN_IN:
return { ...state, isSignedIn: true };
case SIGN_OUT:
return { ...state, isSignedIn: false };
default:
return state;
}
};
actions/index.js
import { SIGN_IN, SIGN_OUT } from "./types";
export const signIn = () => {
return {
type: SIGN_IN
};
};
export const signOut = () => {
return {
type: SIGN_OUT
};
};
types.js
export const SIGN_IN = "SIGN_IN";
export const SIGN_OUT = "SIGN_OUT";
The reason of the first error is that, inside both onSignInOnClick and onSignInOnClick, dispatch() receives a Promise (since window.gapi.auth2.getAuthInstance().signIn() returns a Promise).
There are different solution to handle effects in redux, the simplest are redux promise or redux thunk.
Otherwise you can dispatch the { type: SIGN_IN } action, and write a custom middleware to handle it.
The reason of the second error, is that the onAuthChange is redefined on every render, as you can see here:
const f = () => () => 42
f() === f() // output: false
Here's a possible solution to fix the warning:
useEffect(() => {
const onAuthChange = () => {
if (isSignedIn) {
dispatch(signIn())
} else {
dispatch(signOut())
}
}
window.gapi.load('client:auth2', () => {
window.gapi.client
.init({
clientId: API_KEY,
scope: 'email',
})
.then(() => {
onAuthChange(window.gapi.auth2.getAuthInstance().isSignedIn.get())
console.log(
'isSignedIn.get(): ' +
window.gapi.auth2.getAuthInstance().isSignedIn.get(),
)
window.gapi.auth2.getAuthInstance().isSignedIn.listen(onAuthChange)
})
})
}, [isSignedIn])

Redux actions without return or dispatch

I am implementing Oauth from google with redux, and I wanted to have all google API calls handled from my redux and ended up writing helper functions in my actions file that doesn't return anything or call dispatch. I ended up with code where I only dispatch once from my JSX file and wondering if this is okay or there is another better way to do it?
The code is as follows:
authActions.js
const clientId = process.env.REACT_APP_GOOGLE_OAUTH_KEY;
let auth;
export const authInit = () => (dispatch) => {
window.gapi.load('client:auth2', () =>
window.gapi.client.init({ clientId, scope: 'email' }).then(() => {
auth = window.gapi.auth2.getAuthInstance();
dispatch(changeSignedIn(auth.isSignedIn.get()));
auth.isSignedIn.listen((signedIn) => dispatch(changeSignedIn(signedIn)));
})
);
};
export const signIn = () => {
auth.signIn();
};
export const signOut = () => {
auth.signOut();
};
export const changeSignedIn = (signedIn) => {
const userId = signedIn ? auth.currentUser.get().getId() : null;
return {
type: SIGN_CHANGE,
payload: { signedIn, userId },
};
};
GoogleAuth.jsx
import { useSelector, useDispatch } from 'react-redux';
import classNames from 'classnames';
import { authInit, signIn, signOut } from '../../actions/authActions';
function GoogleAuth() {
const { signedIn } = useSelector((state) => state.auth);
const dispatch = useDispatch();
useEffect(() => {
dispatch(authInit());
}, [dispatch]);
const onClick = () => {
if (signedIn) {
signOut();
} else {
signIn();
}
};
let content;
if (signedIn === null) {
return null;
} else if (signedIn) {
content = 'Sign Out';
} else {
content = 'Sign In';
}
return (
<div className="item">
<button
className={classNames('ui google button', {
green: !signedIn,
red: signedIn,
})}
onClick={onClick}
>
<i className="ui icon google" />
{content}
</button>
</div>
);
}
export default GoogleAuth;
The code works fine, but it feels like it might be misleading having action calls in JSX but not dispatching it, is it okay?

Resources