mobx-react-routing
Version:
MobX react-router-dom integration
155 lines (154 loc) • 5.05 kB
JavaScript
import { LinkedAbortController } from 'linked-abort-controller';
import { action, computed, makeObservable, observable, reaction, runInAction, } from 'mobx';
import { createBrowserRouter, createHashRouter, createMemoryRouter, matchPath, } from 'react-router-dom';
import { generateId } from 'yummies/id';
import { createRoute } from '../create-route.js';
import { QueryParamsBase } from '../query-params/index.js';
export class RouterStoreBase {
abortController;
queryParams;
matches;
location;
blockers = new Set();
router;
fallbackComponent;
errorBoundaryComponent;
get lastMatch() {
return this.matches.at(-1) ?? null;
}
createRoute(declaration, index, parentPath) {
return createRoute({
declaration,
router: this,
index,
parentPath,
factory: (config, declaration) => {
const VM = config.VM;
return new VM(declaration, this, config);
},
});
}
constructor({ routes, fallbackComponent, errorBoundaryComponent, abortSignal, type = 'browser', ...libRouterParams }) {
this.abortController = new LinkedAbortController(abortSignal);
this.fallbackComponent = fallbackComponent;
this.errorBoundaryComponent = errorBoundaryComponent;
let createRouterFn;
switch (type) {
case 'browser': {
createRouterFn = createBrowserRouter;
break;
}
case 'hash': {
createRouterFn = createHashRouter;
break;
}
case 'memory': {
createRouterFn = createMemoryRouter;
break;
}
}
this.router = createRouterFn(routes.map((route, index) => this.createRoute(route, index, [])), libRouterParams);
this.queryParams = new QueryParamsBase(this.router);
this.matches = this.collectRouteMatches(this.router.state.matches);
this.location = { ...this.router.state.location };
observable(this, 'matches');
observable(this, 'location');
observable(this, 'blockers');
computed(this, 'blocked');
computed(this, 'lastMatch');
action.bound(this, 'navigate');
action.bound(this, 'blockRouting');
action.bound(this, 'unblockRouting');
action.bound(this, 'back');
makeObservable(this);
const subscription = this.router.subscribe((state) => {
runInAction(() => {
this.matches = this.collectRouteMatches(state.matches);
this.location = { ...state.location };
});
});
this.abortController.signal.addEventListener('abort', () => {
subscription();
});
}
collectRouteMatches(matches) {
return matches.map((match) => {
return {
id: match.route.id,
params: match.params,
path: match.route.path,
pathname: match.pathname,
};
});
}
createPath(to) {
if (typeof to === 'string') {
const [pathname, search] = to.split('?', 2);
return {
pathname,
search: search ? `?${search}` : '',
hash: '',
};
}
else {
return {
pathname: to.pathname,
search: this.queryParams.buildSearchString(to.search || {}),
hash: '',
};
}
}
get currentUrl() {
return (this.location.pathname +
(this.location.search ? `?${this.location.search}` : ''));
}
createUrl(to) {
const path = this.createPath(to);
return `${path.pathname}${path.search}${path.hash}`;
}
async navigate(to, options) {
await this.router.navigate(this.createUrl(to), options);
}
get blocked() {
return this.blockers.size > 0;
}
blockRoutingIf(expression, id = generateId()) {
const disposeFunction = reaction(expression, (blocks) => {
if (blocks) {
this.blockRouting(id);
}
else {
this.unblockRouting(id);
}
}, { fireImmediately: true });
return () => {
this.unblockRouting(id);
disposeFunction();
};
}
blockRouting(id = generateId()) {
if (!this.blockers.has(id)) {
this.blockers.add(id);
}
return id;
}
unblockRouting(id) {
this.blockers.delete(id);
}
getInstance() {
return this.router;
}
async back() {
await this.router.navigate(-1);
}
findMatch = (pathOrPattern) => {
return matchPath(pathOrPattern, this.location.pathname);
};
isMatched = (pathOrPattern) => {
return !!this.findMatch(pathOrPattern);
};
clean() {
this.blockers.clear();
this.abortController.abort();
}
}