How to prevent router change in Micro Frontends

49 阅读2分钟

background

there is a demand that we need to prevent router change before no saving, to pop up a confirm modal, if you click the ok button, then go on prev action, or stay at the current page.

Find ways to reach this demand

I searched the Qian Kun documents, but could not find ways to prevent router change functions or plugins, I realized maybe I should learn the theory of Router change first.

here is the theory of Router render's theory.

// src/index.tsx
// this case use BrowserRouter(most popular way)
import { BrowserRouter as Router, Switch, Route, Link} from "react-router-dom";

<Router>
	<Link to="/">Home</Link>
	{/* A <Switch> looks through its children <Route>s and
	renders the first one that matches the current URL. */}
	<Switch>
		<Route path="/about">
			<About />
		</Route>
		<Route path="/users">
			<Users />
		</Route>
		<Route path="/">
			<Home />
		</Route>
	</Switch>
</Router>
// BrowserRouter source code
import { createBrowserHistory as createHistory } from "history";
/**
* The public API for a <Router> that uses HTML5 history.
*/
class BrowserRouter extends React.Component {

	history = createHistory(this.props);
	render() {
		return <Router history={this.history} children={this.props.children} />;
	}

}

history is a closure function in a library, it contains the history state and record list, and many actions related to history, such as push, replace so on, it is a ==very important library==. limit to the content words, you can reach the history;

// source code
// Router component
<RouterContext.Provider
	value={{
		history: this.props.history,
		location: this.state.location,
		match: Router.computeRootMatch(this.state.location.pathname),
		staticContext: this.props.staticContext
		}}
>
	<HistoryContext.Provider
		children={this.props.children || null}
		value={this.props.history}
	/>

</RouterContext.Provider>

we can see the router's state is saved in context, it used context to post data to other children components, so the Route component should not be used outside of the Router component or cannot get the related data from context, and the closure history will also saved the same data.

If you can get the history, you will have a way to prevent history.push() function.

now show you the hook, the main project should define a 'father history' on a window global variable, then the sub project could use the father's history to prevent router change

import _ from 'lodash';
import { Modal } from 'antd';
import { useEffect, useMemo, useRef, useState } from 'react';
import { Link, Prompt, history } from 'umi';
const { confirm } = Modal;
interface PromptType {
	when: boolean;
	title?: string; // words about the prompt to leave
	onCallback?: () => void;
}
const fatherHistory = 'main_history'; // it detemines the definition of window?.main_history in the main project
// there is a secene that could be not support, that is when you are in a subproject, you pushstate and in a subproject new page, then you come back, that could cause a problem

function usePrompt({ when = false, title, onCallback }: PromptType) {
	const unblockOutRef = useRef<any>(null);
	const unblockInnerRef = useRef<any>(null);
	useEffect(() => {
		const win: any = window;
		let flag = true;
		let timer: any = null;
		const _location = location;
		const func = (__history, location) => {
			if (flag) {
				flag = false;
				clearTimeout(timer);
				timer = setTimeout(() => {
					flag = true;
					confirm({
						title: title || 'Are you sure you want to leave?',
						okText: 'yes',
						cancelText: 'no',
						onOk: () => {
							unblockOutRef.current?.();
							unblockInnerRef.current?.();
							if (location.pathname === _location.pathname) {
								__history.go(-1);
							} else {
								__history.push(location.pathname);
							}
							onCallback?.();
						},
					});
				}, 50);
			}
		};
	try {
		function runValidateBeforeLeaveOut(__history: any) {
			try {
				unblockOutRef.current = __history.block((location, action,retry) => {
				if (when) {
					func(__history, location);
							}
				if (!when) return true;
					return false;
				});
				} catch (error) {
					console.log(error, 'promptError');
				}
	
	}
	runValidateBeforeLeaveOut(win?.[fatherHistory]);
	function runValidateBeforeLeaveInner(__history: any) {
	try {
		unblockInnerRef.current = __history.block((location, action, retry) => {
		if (when) {
			func(__history, location);
		}
		if (!when) return true;
			return false;
		});
		} catch (error) {
			console.log(error, 'promptError');
		}
	}
		runValidateBeforeLeaveInner(history);
	} catch (error) {
		console.log(error, 'error');
	}
	return () => {
		unblockOutRef.current?.();
		unblockInnerRef.current?.();
	};
	}, [when]);
}
  
export default usePrompt;