React Native webview routing - javascript

I'm building a React-Native app with some native screens, and some screens where I load the website with a WebView. Instead of the classical website navigation, I have a native drawer to switch pages.
My issue is that the website is using react-router, so it handles the URL change smoothly on browsers by loading only the necessary code. Still, when I change the URL in my WebView, it does as if I was refreshing the website, and it reloads everything, leading up to very slow navigation.
The only 'hack' I thought would be exposing a function on the website window variable to trigger a react-router 'go to'.
Any ideas?

So, after a few days of struggling, I have settled with the following solution, which I admit is slighlty obscure, but works perfectly (incl. back button, gestures):
In the React Web Application, I keep the ReactRouter history object available through the global Window object.
import { useHistory } from 'react-router'
// ...
declare global {
interface Window {
ReactRouterHistory: ReturnType<typeof useHistory>
}
}
const AppContextProvider = props => {
const history = useHistory()
// provide history object globally for use in Mobile Application
window.ReactRouterHistory = history
// ...
}
In the React Native Mobile Application, I have custom code injected to the WebView, that makes use of the history object for navigation and the application communicates with this code using messages:
webViewScript.ts
// ...
export default `
const handleMessage = (event) => {
var message = JSON.parse(event.data)
switch (message.type) {
// ...
case '${MessageTypes.NAVIGATE}':
if (message.params.uri && message.params.uri.match('${APP_URL}') && window.ReactRouterHistory) {
const url = message.params.uri.replace('${APP_URL}', '')
window.ReactRouterHistory.push(url)
}
break
}
};
!(() => {
function sendMessage(type, params) {
var message = JSON.stringify({type: type, params: params})
window.ReactNativeWebView.postMessage(message)
}
if (!window.appListenersAdded) {
window.appListenersAdded = true;
window.addEventListener('message', handleMessage)
var originalPushState = window.history.pushState
window.history.pushState = function () {
sendMessage('${MessageTypes.PUSH_STATE}', {state: arguments[0], title: arguments[1], url: arguments[2]})
originalPushState.apply(this, arguments)
}
}
})()
true
`
intercom.ts (no routing specifics here, just for generic communication)
import WebView, {WebViewMessageEvent} from 'react-native-webview'
import MessageTypes from './messageTypes'
export const sendMessage = (webview: WebView | null, type: MessageTypes, params: Record<string, unknown> = {}) => {
const src = `
window.postMessage('${JSON.stringify({type, params})}', '*')
true // Might fail silently per documentation
`
if (webview) webview.injectJavaScript(src)
}
export type Message = {
type?: MessageTypes,
params?: Record<string, unknown>
}
export type MessageHandler = (message: Message) => void
export const handleMessage = (handlers: Partial<Record<MessageTypes, MessageHandler>>) => (
(event: WebViewMessageEvent) => {
const message = JSON.parse(event.nativeEvent.data) as Message
const messageType = message.type
if (!messageType) return
const handler = handlers[messageType]
if (handler) handler(message)
}
)
export {default as script} from './webViewScript'
WebViewScreen.tsx
import {handleMessage, Message, script, sendMessage} from '../intercom'
// ...
const WebViewScreen = ({navigation, navigationStack}: WebViewScreenProps) => {
const messageHandlers = {
[MessageTypes.PUSH_STATE]: ({params}: Message) => {
if (!params) return
const {url} = params
const fullUrl = `${APP_URL}${url}`
navigationStack.push(fullUrl)
},
// ...
}
const uri = navigationStack.currentPath
// navigation solution using history object propagated through window object
useEffect(() => {
if (uri) {
sendMessage(webViewRef.current, MessageTypes.NAVIGATE, {uri})
}
}, [uri])
// this is correct! Source is never going to be updated, navigation is done using native history object, see useEffect above
// eslint-disable-next-line react-hooks/exhaustive-deps
const source = useMemo(() => ({uri}), [])
return (
<View
// ...
refreshControl={
<RefreshControl
// ...
/>
}
>
<WebView
source={source}
injectedJavaScript={script}
onMessage={handleMessage(messageHandlers)}
// ...
/>
</View>
)
}

Have you tried a combination of
UseEffect and route navigation?

Related

How to abstract multiple similar function components (providers) in React?

I have several providers / contexts in a React app that do the same, that is, CRUD operations calling a Nestjs app:
export const CompaniesProvider = ({children}: {children: any}) => {
const [companies, setCompanies] = useState([])
const fetchCompany = async () => {
// etc.
try {
// etc.
setCompanies(responseData)
} catch (error) {}
}
const updateCompany = async () => {
// etc.
try {
// etc.
} catch (error) {}
}
// same for delete, findOne etc..
return (
<CompaniesContext.Provider value={{
companies,
saveSCompany,
}}>
{children}
</CompaniesContext.Provider>
)
}
export const useCompanies = () => useContext(CompaniesContext)
Another provider, for instance, the Technology model would look exactly the same, it just changes the api url:
export const TechnologiesProvider = ({children}: {children: any}) => {
const [technologies, setTechnologies] = useState([])
const fetchTechnology = async () => {
// etc.
try {
// etc.
setTechnologies(responseData)
} catch (error) {}
}
const updateTechnology = async () => {
// etc.
try {
// etc.
} catch (error) {}
}
// same for delete, findOne etc..
return (
<TechnologiesContext.Provider value={{
technologies,
savesTechnology,
}}>
{children}
</TechnologiesContext.Provider>
)
}
export const useTechnologies = () => useContext(TechnologiesContext)
What is the best way to refactor? I would like to have an abstract class that implements all the methods and the different model providers inherit the methods, and the abstract class just needs the api url in the constructor..
But React prefers function components so that we can use hooks like useState.
Should I change function components to class components to be able to refactor? But then I lose the hooks capabilities and it's not the react way nowadays.
Another idea would be to inject the abstract class into the function components, and the providers only call for the methods.
Any ideas?
One way to achieve it is to create a factory function that gets a url (and other parameters if needed) and returns a provider & consumer
This is an example for such function:
export const contextFactory = (url: string) => {
const Context = React.createContext([]); // you can also get the default value from the fn parameters
const Provider = ({ children }: { children: any }) => {
const [data, setData] = useState([]);
const fetch = async () => {
// here you can use the url to fetch the data
try {
// etc.
setData(responseData);
} catch (error) {}
};
const update = async () => {
// etc.
try {
// etc.
} catch (error) {}
};
// same for delete, findOne etc..
return (
<Context.Provider
value={{
data,
save
}}
>
{children}
</Context.Provider>
);
};
const hook = () => useContext(Context)
return [Provider, hook]
};
And this is how you can create new providers & consumers
const [CompaniesProvider, useCompanies] = contextFactory('http://...')
const [TechnologiesProvider, useTechnologies] = contextFactory('http://...')
I ended up creating a class representing the CRUD operations:
export class CrudModel {
private api = externalUrls.api;
constructor(private modelUrl: string) {}
async fetchRecords() {
const url = `${this.api}/${this.modelUrl}`
try {
const response = await fetch(url, {
method: 'GET',
headers: {
'Content-type': 'application/json'
},
})
return await response.json()
} catch (error) {}
}
// removeRecord
// updateRecord
// saveRecord
}
Then for every provider I reduced the code, since I just call an instance of the CrudModel, which has the method implementations.
type Technology = {
id: number;
name: string;
}
type Context = {
technologies: Technology[];
saveTechnology: any;
removeTechnology: any;
updateTechnology: any;
}
const TechnologiesContext = createContext<Context>({
technologies: [],
saveTechnology: null,
removeTechnology: null,
updateTechnology: null,
})
export const TechnologiesProvider = ({children}: {children: any}) => {
const [technologies, setTechnologies] = useState([])
const router = useRouter()
const crudModel = useMemo(() => {
return new CrudModel('technologies')
}, [])
const saveTechnology = async (createForm: any): Promise<void> => {
await crudModel.saveRecord(createForm)
router.reload()
}
// fetchTechnologies
// removeTechnology
// updateTechnology
useEffect(() => {
async function fetchData() {
const fetchedTechnologies = await crudModel.fetchRecords()
setTechnologies(fetchedTechnologies)
}
fetchData()
}, [crudModel])
return (
<TechnologiesContext.Provider value={{
technologies,
saveTechnology,
removeTechnology ,
updateTechnology,
}}>
{children}
</TechnologiesContext.Provider>
)
}
This way I can have types for every file, and it's easy to debug / maintain. Having just one factory function like previous answer it feels cumbersome to follow data flow. The downside is that there is some repetition of code among the provider files. Not sure if I can refactor further my answer

Is there anyway to pass state to getServerSideProps

I am new to next.js. I want to pass page state to getServerSideProps. Is it possible to do this?
const Discover = (props) => {
const [page, setPage] = useState(1);
const [discoverResults, setDiscoverResults] = useState(props.data.results);
// console.log(discoverResults, page);
return (
<div>
<Card items={discoverResults} render={(discoverResults) => <DiscoverCard results={discoverResults} />} />
</div>
);
};
export default Discover;
export async function getServerSideProps() {
const movieData = await axios.get(`https://api.themoviedb.org/3/discover/movie?api_key=${process.env.NEXT_PUBLIC_MOVIE_DB_KEY}&language=en-US&sort_by=popularity.desc&include_adult=false&include_video=false&page=${page}&with_watch_monetization_types=flatrate`);
return {
props: {
data: movieData.data,
},
};
}
the only way is changing your route with params and recive it in server side :
import { useRouter } from "next/router";
const Discover = (props) => {
const { page } = props;
const router = useRouter();
const goToNextPage = () => {
router.replace(`/your-page-pathname?page=${+page + 1}`);
}
return (
<div>
page is : {page}
<button onClick={goToNextPage}>
next page
</button>
</div>
);
};
export default Discover;
export async function getServerSideProps(context) {
const { page } = context.query;
return {
props: {
page: page || 0,
},
};
}
To read more on the topic, recommend reading: Refreshing Server-Side Props
I recommend to use SWR for handling this kind of api calls
an example of this here:
https://swr.vercel.app/examples/ssr
In this example, it can be seen that the api calls happens in the Server side and it is being cached in the Client side.
For handling the query from the urls. This can be done using the same methods as well following the examples from their documentation of SWR https://swr.vercel.app/docs/pagination#pagination
SWR will help alot of stuffs in the api state management. I really recommend to start learning it as soon as possible..

Preserve state value on client side navigation - NextJs - Next-Redux-Wrapper

So I am trying to fix the hydrating issue I am facing when using wrapper.getServerSideProps. When I reroute with the current setup the store is cleared out and then the new data is added, which results in a white page since a lot of important data is no longer there (i.e, translations and cms data).
Screenshot from redux-dev-tools Hydrate action diff:
Screenshot is taken after routing from the homepage to a productpage, so that there was an existing store. Everything is reset to the initial app state.
What I am trying to do
In the store.js I create the store and foresee a reducer to handle the Hydrate call. The downside of this approach is that the payload will always be a new store object since it is called on the server. I was thinking to check the difference between the 2 json's and then only apply the difference instead of the whole initial store.
Get the difference between the client and server state.
Make the next state, overwrite clientstate with patched serverstate so this includes, updated state from hydrate and the existing client state.
Currently results in a white page.
You can see the reducer code below in the store.js
//store.js
import combinedReducer from './reducer';
const bindMiddleware = (middleware) => {
if (process.env.NODE_ENV !== 'production') {
return composeWithDevTools(applyMiddleware(...middleware));
}
return applyMiddleware(...middleware);
};
const reducer = (state, action) => {
if (action.type === HYDRATE) {
const clientState = { ...state };
const serverState = { ...action.payload };
if (state) {
// preserve state value on client side navigation
// Get the difference between the client and server state.
const diff = jsondiffpatch.diff(clientState, serverState);
if (diff !== undefined) {
// If there is a diff patch the serverState, with the existing diff
jsondiffpatch.patch(serverState, diff);
}
}
// Make next state, overwrite clientstate with patched serverstate
const nextState = {
...clientState,
...serverState,
};
// Result, blank page.
return nextState;
}
return combinedReducer(state, action);
};
export const makeStore = () => {
const cookies = new Cookies();
const client = new ApiClient(null, cookies);
const middleware = [
createMiddleware(client),
thunkMiddleware.withExtraArgument(cookies),
];
return createStore(reducer, bindMiddleware(middleware));
};
const wrapper = createWrapper(makeStore);
export default wrapper;
//_app.jsx
const App = (props) => {
const { Component, pageProps, router } = props;
return (
<AppComponent cookies={cookies} locale={router.locale} location={router}>
<Component {...pageProps} />
</AppComponent>
);
};
App.getInitialProps = async ({ Component, ctx }) => {
return {
pageProps: {
...(Component.getInitialProps ? await Component.getInitialProps(ctx) : {}),
},
};
};
App.propTypes = {
Component: PropTypes.objectOf(PropTypes.any).isRequired,
pageProps: PropTypes.func,
router: PropTypes.objectOf(PropTypes.any).isRequired,
};
App.defaultProps = {
pageProps: () => null,
};
export default wrapper.withRedux(withRouter(App));
// Product page
export const getServerSideProps = wrapper.getServerSideProps(
async ({ query, store: { dispatch } }) => {
const productCode = query.id?.split('-', 1).toString();
await dispatch(getProductByCode(productCode, true));
});
const PDP = () => {
const { product } = useSelector((state) => state.product);
return (
<PageLayout>
<main>
<h1>{product?.name}</h1>
<div
className="description"
dangerouslySetInnerHTML={{ __html: product?.description }}
/>
</main>
</PageLayout>
);
};
export default PDP;
Oke, so I solved my issue through not overthinking the concept. Went back to the drawing board and made a simple solution.
Came to the conclusion that there are only a few state objects that need to persist during client navigation.
I only had to make a change to my i18n, to make it dynamic since we fetch translations on page basis.
This is the final reducer for anyone that might, in the future run into a similar problem.
const reducer = (state, action) => {
if (action.type === HYDRATE) {
const clientState = { ...state };
const serverState = { ...action.payload };
const nextState = { ...clientState, ...serverState };
const locale = nextState.i18n.defaultLocale || config.i18n.defaultLocale;
const nextI18n = {
...state.i18n,
locale,
messages: {
[locale]: {
...state.i18n.messages[locale],
...nextState.i18n.messages[locale],
},
},
loadedGroups: {
...state.i18n.loadedGroups,
...nextState.i18n.loadedGroups,
},
};
if (state) {
nextState.i18n = nextI18n;
nextState.configuration.webConfig = state.configuration.webConfig;
nextState.category.navigation = state.category.navigation;
}
return nextState;
}
return combinedReducer(state, action);
};

How to dynamically import SVG and render it inline

I have a function that takes some arguments and renders an SVG. I want to dynamically import that svg based on the name passed to the function. It looks like this:
import React from 'react';
export default async ({name, size = 16, color = '#000'}) => {
const Icon = await import(/* webpackMode: "eager" */ `./icons/${name}.svg`);
return <Icon width={size} height={size} fill={color} />;
};
According to the webpack documentation for dynamic imports and the magic comment "eager":
"Generates no extra chunk. All modules are included in the current
chunk and no additional network requests are made. A Promise is still
returned but is already resolved. In contrast to a static import, the
module isn't executed until the call to import() is made."
This is what my Icon is resolved to:
> Module
default: "static/media/antenna.11b95602.svg"
__esModule: true
Symbol(Symbol.toStringTag): "Module"
Trying to render it the way my function is trying to gives me this error:
Objects are not valid as a React child (found: [object Promise]). If you meant to render a collection of children, use an array instead.
I don't understand how to use this imported Module to render it as a component, or is it even possible this way?
You can make use of ref and ReactComponent named export when importing SVG file. Note that it has to be ref in order for it to work.
The following examples make use of React hooks which require version v16.8 and above.
Sample Dynamic SVG Import hook:
function useDynamicSVGImport(name, options = {}) {
const ImportedIconRef = useRef();
const [loading, setLoading] = useState(false);
const [error, setError] = useState();
const { onCompleted, onError } = options;
useEffect(() => {
setLoading(true);
const importIcon = async () => {
try {
ImportedIconRef.current = (
await import(`./${name}.svg`)
).ReactComponent;
if (onCompleted) {
onCompleted(name, ImportedIconRef.current);
}
} catch (err) {
if (onError) {
onError(err);
}
setError(err);
} finally {
setLoading(false);
}
};
importIcon();
}, [name, onCompleted, onError]);
return { error, loading, SvgIcon: ImportedIconRef.current };
}
Sample Dynamic SVG Import hook in typescript:
interface UseDynamicSVGImportOptions {
onCompleted?: (
name: string,
SvgIcon: React.FC<React.SVGProps<SVGSVGElement>> | undefined
) => void;
onError?: (err: Error) => void;
}
function useDynamicSVGImport(
name: string,
options: UseDynamicSVGImportOptions = {}
) {
const ImportedIconRef = useRef<React.FC<React.SVGProps<SVGSVGElement>>>();
const [loading, setLoading] = useState(false);
const [error, setError] = useState<Error>();
const { onCompleted, onError } = options;
useEffect(() => {
setLoading(true);
const importIcon = async (): Promise<void> => {
try {
ImportedIconRef.current = (
await import(`./${name}.svg`)
).ReactComponent;
onCompleted?.(name, ImportedIconRef.current);
} catch (err) {
onError?.(err);
setError(err);
} finally {
setLoading(false);
}
};
importIcon();
}, [name, onCompleted, onError]);
return { error, loading, SvgIcon: ImportedIconRef.current };
}
For those who are getting undefined for ReactComponent when the SVG is dynamically imported, it is due to a bug where the Webpack plugin that adds the ReactComponent to each SVG that is imported somehow does not trigger on dynamic imports.
Based on this solution, we can temporary resolve it by enforcing the same loader on your dynamic SVG import.
The only difference is that the ReactComponent is now the default output.
ImportedIconRef.current = (await import(`!!#svgr/webpack?-svgo,+titleProp,+ref!./${name}.svg`)).default;
Also note that there’s limitation when using dynamic imports with variable parts. This SO answer explained the issue in detail.
To workaround with this, you can make the dynamic import path to be more explicit.
E.g, Instead of
// App.js
<Icon path="../../icons/icon.svg" />
// Icon.jsx
...
import(path);
...
You can change it to
// App.js
<Icon name="icon" />
// Icon.jsx
...
import(`../../icons/${name}.svg`);
...
Your rendering functions (for class components) and function components should not be async (because they must return DOMNode or null - in your case, they return a Promise). Instead, you could render them in the regular way, after that import the icon and use it in the next render. Try the following:
const Test = () => {
let [icon, setIcon] = useState('');
useEffect(async () => {
let importedIcon = await import('your_path');
setIcon(importedIcon.default);
}, []);
return <img alt='' src={ icon }/>;
};
I made a change based on answer https://github.com/facebook/create-react-app/issues/5276#issuecomment-665628393
export const Icon: FC<IconProps> = ({ name, ...rest }): JSX.Element | null => {
const ImportedIconRef = useRef<FC<SVGProps<SVGSVGElement>> | any>();
const [loading, setLoading] = React.useState(false);
useEffect((): void => {
setLoading(true);
const importIcon = async (): Promise<void> => {
try {
// Changing this line works fine to me
ImportedIconRef.current = (await import(`!!#svgr/webpack?-svgo,+titleProp,+ref!./${name}.svg`)).default;
} catch (err) {
throw err;
} finally {
setLoading(false);
}
};
importIcon();
}, [name]);
if (!loading && ImportedIconRef.current) {
const { current: ImportedIcon } = ImportedIconRef;
return <ImportedIcon {...rest} />;
}
return null;
};
One solution to load the svg dynamically could be to load it inside an img using require, example:
<img src={require(`../assets/${logoNameVariable}`)?.default} />
i changed my code to this and work:
import { ReactComponent as Dog } from './Dog.svg';
use like this:
<Dog />
or if it is dynamic:
import * as icons from '../../assets/categoryIcons';
const IconComponent = icons[componentName??'Dog'];
<IconComponent fill='red' />
I dynamically fetched the SVG file as text and then put the SVG within a div dangerouslySetInnerHTML.
const Icon = ({ className, name, size = 16 }: IconProps) => {
const [Icon, setIcon] = React.useState("");
React.useEffect(() => {
fetch(`/icons/${name}.svg`)
.then((res) => res.text())
.then((res) => {
if (res.startsWith("<svg")) return setIcon(res);
console.error(
`Icon: "${name}.svg" not found in ${process.env.PUBLIC_URL}/icons`
);
return setIcon("");
});
}, [name]);
if (!Icon) return null;
return (
<div
className={classNames("icon", className)}
style={{ width: !size ? "100%" : size + "px", height: "100%" }}
dangerouslySetInnerHTML={{ __html: Icon }}
/>
);
};
Preview on Codesandbox
You can automatically change the color of your svg by giving it a fill value of "currentColor".

ReactRouter v4 Prompt - override default alert

The React Router v4 <Prompt></Prompt> component is perfect for the use case of protecting navigation away from a partially filled out form.
But what if we want to supply our own logic in place of the default browser alert() that this component uses? React is intended for creating UIs, so it seems like a pretty reasonable use case. Digging through the issues on Prompt in the github I did not find anyone asking about this.
Does anyone know of a solution for providing custom behavior for the alert?
Although you can make use of a custom Modal component while preventing navigating between pages through Links, you can't show a custom modal while trying to close browser or reload it.
However if thats fine with you, you can make use of history.listen to and block navigation. I wrote a generic HOC for it which solves this use case.
In the below code whitelisted pathnames are the pathnames that you would want the other person to navigate to without showing the prompt
import React from 'react';
import { withRouter } from 'react-router';
import _ from 'lodash';
const navigationPromptFactory = ({ Prompt }) => {
const initialState = {
currentLocation: null,
targetLocation: null,
isOpen: false
};
class NavigationPrompt extends React.Component {
static defaultProps = {
when: true
};
state = initialState;
componentDidMount() {
this.block(this.props);
window.addEventListener('beforeunload', this.onBeforeUnload);
}
componentWillReceiveProps(nextProps) {
const {
when: nextWhen,
history: nextHistory,
whiteListedPathnames: nextWhiteListedPaths
} = nextProps;
const { when, history, whiteListedPathnames } = this.props;
if (
when !== nextWhen ||
!_.isEqual(nextHistory.location, history.location) ||
!_.isEqual(whiteListedPathnames, nextWhiteListedPaths)
) {
this.unblock();
this.block(nextProps);
}
}
componentWillUnmount() {
this.unblock();
window.removeEventListener('beforeunload', this.onBeforeUnload);
}
onBeforeUnload = e => {
const { when } = this.props;
// we can't override an onBeforeUnload dialog
// eslint-disable-next-line
// https://stackoverflow.com/questions/276660/how-can-i-override-the-onbeforeunload-dialog-and-replace-it-with-my-own
if (when) {
// support for custom message is no longer there
// https://www.chromestatus.com/feature/5349061406228480
// eslint-disable-next-line
// https://stackoverflow.com/questions/38879742/is-it-possible-to-display-a-custom-message-in-the-beforeunload-popup
// setting e.returnValue = "false" to show prompt, reference below
//https://github.com/electron/electron/issues/2481
e.returnValue = 'false';
}
};
block = props => {
const {
history,
when,
whiteListedPathnames = [],
searchQueryCheck = false
} = props;
this.unblock = history.block(targetLocation => {
const hasPathnameChanged =
history.location.pathname !== targetLocation.pathname;
const hasSearchQueryChanged =
history.location.search !== targetLocation.search;
const hasUrlChanged = searchQueryCheck
? hasPathnameChanged || hasSearchQueryChanged
: hasPathnameChanged;
const isTargetWhiteListed = whiteListedPathnames.includes(
targetLocation.pathname
);
const hasChanged =
when && hasUrlChanged && !isTargetWhiteListed;
if (hasChanged) {
this.setState({
currentLocation: history.location,
targetLocation,
isOpen: true
});
}
return !hasChanged;
});
};
onConfirm = () => {
const { history } = this.props;
const { currentLocation, targetLocation } = this.state;
this.unblock();
// replacing current location and then pushing navigates to the target otherwise not
// this is needed when the user tries to change the url manually
history.replace(currentLocation);
history.push(targetLocation);
this.setState(initialState);
};
onCancel = () => {
const { currentLocation } = this.state;
this.setState(initialState);
// Replacing the current location in case the user tried to change the url manually
this.unblock();
this.props.history.replace(currentLocation);
this.block(this.props);
};
render() {
return (
<Prompt
{...this.props}
isOpen={this.state.isOpen}
onCancel={this.onCancel}
onConfirm={this.onConfirm}
/>
);
}
}
return withRouter(NavigationPrompt);
};
export { navigationPromptFactory };
In order to use the above, you can simply provide your custom Prompt Modal like
const NavigationPrompt = navigationPromptFactory({
Prompt: AlertDialog
});
const whiteListedPathnames = [`${match.url}/abc`, match.url];
<NavigationPrompt
when={isEditingPlan}
cancelLabel={'Stay'}
confirmLabel={'Leave'}
whiteListedPathnames={whiteListedPathnames}
title={'Leave This Page'}
>
<span>
Unsaved Changes may not be saved
</span>
</NavigationPrompt>
The prompt component by default doesn't allow overriding the use of window.alert().
Here's a link to a conversation that matches your needs fairly similarly:
https://github.com/ReactTraining/react-router/issues/4635
There's a few key points in there that you can refer to, mostly just that instead of using prompt you can just make your own modal to be triggered on specific user actions. :)
Hope this helps
Here's a component using hooks to achieve block functionality, the <Prompt.../> component didn't work for me because I wanted to ignore the search on the location.
import { useEffect, useRef } from 'react';
import { useHistory } from 'react-router-dom';
interface IProps {
when: boolean;
message: string;
}
export default function RouteLeavingGuard({ when, message }: IProps) {
const history = useHistory();
const lastPathName = useRef(history.location.pathname);
useEffect(() => {
const unlisten = history.listen(({ pathname }) => lastPathName.current = pathname);
const unblock = history.block(({ pathname }) => {
if (lastPathName.current !== pathname && when) {
return message;
}
});
return () => {
unlisten();
unblock();
}
}, [history, when, message]);
return null;
}

Categories

Resources