@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
6
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