mobx-react-viewmodel
v0.2.0
Published
Tiny package (~0.9 KB) which can make a huge difference for your React/Mobx app architecture.
Downloads
1,262
Maintainers
Readme
mobx-react-viewmodel
Tiny package (~0.9 KB) which can make a huge difference for your React/Mobx app architecture.
Simple hooks useViewModel
and useViewModelFactory
allow you to move all component's state and logic from the render function to a separate stateful view-model class. Which unleashes the full power of Mobx and helps you escape React Hooks Hell.
Written in TypeScript and 100% type-safe 🎯
Installation
npm install mobx-react-viewmodel --save
or
yarn add mobx-react-viewmodel
Simple Example
import { observer } from 'mobx-react'
import { useViewModel } from 'mobx-react-viewmodel'
class MyViewModel {
@observable
title: string = '';
constructor() {
makeObservable(this);
}
@action
setTitle(title: string) {
this.title = title;
}
}
const MyComponent = observer(() => {
const viewModel = useViewModel(MyViewModel); // MyViewModel instance will be created only once on first render
return (
<div>
<input
type="text"
value={viewModel.title}
onChange={e => viewModel.setTitle(e.target.value)}
placeholder="Type title..."
/>
<p>Title: {viewModel.title}</p>
</div>
);
});
Benefits
- View-layer (component's render function) stays clean and contains only rendering code
- Taking full advantage of Mobx using
@observable / @computed / @action / reaction / when
primitives for powerful reactive programming - No need to use
useState / useEffects / useCallback / useMemo
anymore (goodbye spaghetti code,stale closure
bugs and dependency list for every hook) - At some point, if you need to move some component's state to a global Mobx-store, you don't have to rewrite old hooks-code with Mobx. You can just cut and paste what you need from view-model class to the store class
Advanced Example
// MyPageViewModel.ts
import { action, comparer, computed, makeObservable, observable, reaction } from 'mobx';
import { User, UsersStore } from 'our/users/module'
interface MyPageViewModelProps {
userId: string;
}
class MyPageViewModel extends ViewModel<MyPageViewModelProps> {
@observable
user?: User;
constructor(props: MyPageViewModelProps, private usersStore: UsersStore) {
super(props);
makeObservable(this);
}
// is invoked immediately after a component is mounted
init(){
this.disposers.push(
reaction(
() => this.props.userId,
userId => this.getUserFromStore(userId),
{ fireImmediately: true }
)
);
}
// is invoked immediately before a component is unmounted and destroyed
dispose() {
super.dispose();
this.user = undefined;
}
@action
updateField = (field: keyof User, value: string) => {
if (!this.user) return;
this.user[field] = value;
}
@action
save = () => {
if (!this.user) return;
this.usersStore.updateUser(this.user);
}
@computed
isPristine() {
const storeUser = this.usersStore.getUser(this.props.userId);
return this.user && storeUser && comparer.shallow(this.user, storeUser);
}
@action
private getUserFromStore = (userId: string) => {
const user = this.usersStore.getUser(userId);
if (user) {
this.user = { ...user };
} else {
console.error(`Couldn't find user: ${userId}`);
}
}
}
// MyPage.tsx
import { observer } from 'mobx-react'
import { useViewModel } from 'mobx-react-viewmodel'
import { useParams } from 'react-router-dom';
import { useStores } from 'our/stores/path';
const MyPage = observer(() => {
// Get the userId param from the URL.
const { userId } = useParams();
// Get our custom store from hook or context
const { userStore } = useStores();
const viewModel = useViewModelFactory(
props => new MyPageViewModel(props, userStore), // Factory-function will be called only once on first render
{ userId } // These `props` are reactive. It will be passed and updated in view-model every time it changes without creating new instance of view-model
);
//** or
// const viewModel = useViewModel(
// MyPageViewModel,
// { userId },
// [userStore] // Extra dependencies for the view-model constructor
// );
return (
<div>
<input
type="text"
value={viewModel.user.first_name}
onChange={e => viewModel.updateField('first_name', e.target.value)}
placeholder="First Name"
/>
<input
type="text"
value={viewModel.user.last_name}
onChange={e => viewModel.updateField('last_name', e.target.value)}
placeholder="Last Name"
/>
<button type="button" disabled={viewModel.isPristine} onClick={viewModel.save}>Save</button>
</div>
);
});
You can find more examples here.
API documentation
useViewModel(ViewModelClass, props?, args?)
Basic hook that creates an instance of the ViewModelClass
on the first render and keeps it alive during all further renders.
If props
is passed, it will be set to the ViewModelClass instance props
property every time it changes.
If args
is passed, it will be passed to the ViewModelClass constructor on the first render. You should use only permanent references to the object there, for example, a reference to singleton objects.
If ViewModelClass
implements init()
or dispose()
methods, they will be called on the component's mount
and unmount
events.
useViewModelFactory(factoryFn, props?)
Hook for more advanced view-model instantiation. It allows to inject extra dependencies to the view-model constructor inside the custom factoryFn
. Life-cycle and props
update logic is the same as with useViewModel
ViewModel
Base class with observable props
property which is automatically updated by useViewModel
or useViewModelFactory
. Also, it has a built-in disposers
array for storing all disposer-functions from reaction
, when
, or any other custom ones that you need to call on unmount
. You can extend your custom view-model classes from this class for cutting down boilerplate code.