@shadowvzs/react-view-router
v0.8.16
Published
Router for ReactJS with MobX+Typescript, decoupled component and logic with introducing the viewStore layer between routes and component which can instantiate outside as well and can inject data for all instantiated viewStore
Downloads
43
Maintainers
Readme
react-view-router
Router for ReactJS with MobX, TypeScript
Why?
- Current routers doesn't fitted to my needs, they allow to mount an JSX.Element but doesn't help to separate the logic/components, doesn't help too much in testing and always need write our own route guard if needed (which should be enough often ex.: auth),
Goal
- more testable component, if we split the logic the components then we can test just the logic or component, not just together
- more cleaner components (separated business logic & dummy component)
- support both common element based route and the ViewStore way, dynamic, normal, nested paths
- can expose/instantiate outside the router/history instance and automatically inject into the ViewStore the history
- easy injection for ViewStore (RouterProvider), which make easier to inject auth/services/configs into the viewStore and not need hooks into components
- support mounting multiple component/viewstore into the app with same router if needed
- very easy way to guard the component (lifecycles should be used for that)
ViewStores
LifeCycles:
methods which help to move some logic from component into ViewStore- beforeMount - run 1x, can please here an initial data fetch
- beforeUpdate - called everytime if route data was changed (example: you are in detail view and id in url was changed)
- beforeUnmount - called before the unmounting the component
Guards:
method args: url data, returns Pomise, if returns false then route will be not changed)- canMount - can be mounted - good place for auth check/guard
- canUpdate - can be updated - good place for discard modals for edit views
- canUnmount - can be unmounted - good place for discard modals for edit views when leave the component
Injectable data:
// this based on project where it is use, so just an example
export type ViewStoreInjectedData = {
globalConfig: { baseApiUrl: string; };
serviceMap: Record<string, unknown>;
notifyService: (type: 'error' | 'success', message: string) => void;
};
const data = {
globalConfig: { baseApiUrl: '' },
serviceMap: {
bookService: {
getBooks: () => {}
}
},
notifyService: (type, message) => { console.info(type, message); },
};
Provider:
<RouterProvider<ViewStoreInjectedData> history={new BrowserHistory()} injectedData={data}>
{/* children will be here */}
</RouterProvider>
Routes:
<Route path='/login' ViewStore={LoginView} Cmp={LoginCmp} />
<Route path='/signup' ViewStore={SignUpView} Cmp={SignUpCmp} />
<Route path='/books/:genre/:category' ViewStore={BookListView} Cmp={BookListCmp} />
<Route path='/books/drama' exact={false} element={<div>show this if route starts with "/books/drama" (exact is false)</div>} />
Links:
<ul>
<li><Link to='/'>/</Link></li>
<li><Link to='/login'>/login</Link></li>
<li><Link to='/signup'>/signup</Link></li>
<li><Link to='/books/drama/bestseller?top=12#2'>/pista/222</Link></li>
</ul>
Examples
Simple view
class SignUpView extends ViewStore implements ISignUpView {
public onSignUp = (ev: React.FormEvent<HTMLFormElement>) => {
ev.preventDefault();
alert('onSubmit');
return false;
};
}
Simple component
const SignUpCmp = (props: { store: ISignUpView }): JSX.Element => {
const { store } = props;
return (
<div>
<h4>Sign Up</h4>
<form onSubmit={store.onSignUp}>
<div>
<input placeholder='username' type='text' />
</div>
<div>
<input placeholder='email' type='text' />
</div>
<div>
<input placeholder='password' type='password' />
</div>
<div>
<input type='checkbox' /> Agree
</div>
<button>Submit</button>
</form>
</div>
);
};
More advanced ViewStore
class BookListView extends ViewStore<ViewStoreInjectedData> implements IBookListView {
@observable
public books: Book[] = [];
@action.bound
public setBooks(books: Book[]) { this.books = books; }
@observable
public loading = false;
@action.bound
public setLoading(loading: boolean) { this.loading = loading; }
constructor() {
super();
this.update = this.update.bind(this);
makeObservable(this);
}
// called before the mount
public beforeMount() { this.loadFromDatabase().catch(console.error); }
// called at every update
public beforeUpdate() { this.loadFromDatabase().catch(console.error); }
// lets override the default update
public update(urlData: IUrlData) {
const oldStateWithoutHash = { ...this.props, hash: undefined };
const newStateWithoutHash = { ...urlData, hash: undefined };
// if there more difference then the hash, then we update normally
if (JSON.stringify(oldStateWithoutHash) !== JSON.stringify(newStateWithoutHash)) {
super.update(urlData);
} else {
// but only if the hash changed then we not reload the list
this.setProps(urlData);
this.render();
}
}
private loadFromDatabase = async () => {
this.setLoading(true);
this.injectedData.notifyService('success', 'loading started');
try {
// wait for some random time
await delayPromise(Math.random() * 1000 + 500);
// generate some dummy datam, normally the injectData should contain the services for data loading
const books = new Array(Math.round(Math.random() * 7) + 3).fill(1).map((_, idx) => ({
id: String(idx + 1),
title: `Book Nr #${idx + 1}`
}));
this.setBooks(books);
} catch (error: unknown) {
console.error(error);
} finally {
this.injectedData.notifyService('success', 'loading ended');
this.setLoading(false);
}
return false;
};
}
and component for the view
interface BookListCmpProps {
store: IBookListView;
params: { genre: string; category: string; top: string; };
hash?: string;
}
const BookListCmp = observer((props: BookListCmpProps): JSX.Element => {
const { store, params, hash } = props;
const { genre, category, top } = params;
return (
<div>
<h4>Book List {top && `- top(${top})`}</h4>
<p>Genre: {genre}</p>
<p>Category: {category}</p>
<ul>
{store.loading && <h4>Loading....</h4>}
{!store.loading && store.books.map(book => (
<li key={book.id} style={book.id === hash ? { backgroundColor: 'cyan' } : {}}>
<Link to={`/books/${genre}/${category}?top=${top || 10}#${book.id}`}>{book.title}</Link>
</li>
))}
</ul>
<nav>
History
<ul>
<li><Link to={`/books/history/best-seller`}>Best Seller</Link></li>
<li><Link to={`/books/history/recommended`}>Recommended</Link></li>
<li><Link to={`/books/history/cheap`}>Cheap</Link></li>
<li><Link to={`/books/history/ForEvents`}>For Events</Link></li>
</ul>
</nav>
</div >
);
});
Credit to Igor Gaponovfor the npm guide https://betterprogramming.pub/how-to-create-and-publish-react-typescript-npm-package-with-demo-and-automated-build-80c40ec28aca#b9e9