remos
(RE)act (M)odel (O)riented (S)tore
Installation
NPM
npm i remos --save
YARN
yarn add remos
Features
- [x] Simple setup
- [x] No provider required
- [x] No boilerplate
- [x] Model inheritance supported
- [x] Nested model supported
- [x] Model injection supported
- [x] Fully Typescript supoorted
- [x] Immer supported
- [x] Memoized data supported
- [x] Model family supported
- [ ] Redux Devtools supported
CodeSandbox
Counter App (5 lines of code)import { useModel } from "remos";
const App = () => {
const model = useModel({ count: 1 });
return <h1 onClick={() => model.count++}>{model.count}</h1>;
};
Core Concepts
Imagine the model is where to describe a state and actions of the application. A simple model looks like this:
import { create } from "remos";
const counterModel = create({
// define count prop and set initial value as 0
count: 0,
// define model method
increase() {
// update model prop, the 'this' keyword means the current model
this.count++;
},
});
// even we can update the model prop outside the model's methods
// but describing method to escapsule the model logics and those logics can re-use easily
counterModel.count++;
There are 2 kinds of model, global and local models. A global works like single Redux store, that means we can have multiple stores in an app. The local model works like useState() hook, you can use it to store local states and when you change its props, the component will re-render
import { create, useModel } from "remos";
// the global is created by calling create function with specified props
const globalModel = create({});
const GlobalModelConsumer = () => {
// consume globalModel, the component will re-render when global model props are changed
useModel(globalModel);
// render global model prop
return <h1>{globalModel.prop}</h1>;
};
const LocalModelConsumer = () => {
// create and consume local model
// local model is created once and get from cache for next re-rendering time
const localModel = useModel({ prop1: 1, prop2: 2 });
// if you local model has complex logic and too much properties, you can put those things into the function that returns a model props, that function is called once
const otherLocalModel = useModel(() => ({
prop1: 1,
prop2: 2,
method1() {},
method2() {},
}));
return <h1>{localModel.prop1}</h1>;
};
Magic methods
You can define magic methods to handle some special events of the model
const model = create({
firstName: "",
lastName: "",
fullName: "",
// this method will be called when the model has first access (get, set, listen)
_onInit() {},
// this method will be called whenever the model's props are changed
_onChange() {},
// this method will be called when firstName prop changed
// that means you can define onXXXChange() to handle changing event of other props
_onFirstNameChange() {},
// this method will be called to validate the firstName prop
_valFirstName() {
// returning true to mark the firstName prop is VALID
// returning false to mark the firstName prop is INALID
// throwing an error to mark the firstName prop is INVALID
// calling $invalid('firstName', true/false/Error) to set the invalid status of the firstName prop
},
// this method will be called after onChange() call,
// you can put the validation logic for whole model here
_valAll() {},
// this is custom getter for fullName
_getFullName() {
return `${this.firstName} ${this.lastName}`;
// if you have complex computation, you can use $memo to cache the result
// return this.$memo(
// () => // do complex computation,
// [this.firstName, this.lastName] // this is dependency list, the memo does re-evaluation when the dep values are changed only
// );
},
// if you dont define custom setter for specified prop,
// that prop become readsonly
// _setFullName(value: string) {}
});
Binding with React
Using useModel to bind the model to the host component, when the model changes, the host component re-renders
import { useModel } from "remos";
import myModel from "path/to/model";
const App = () => {
useModel(myModel);
return <div><{myModel.myProp}/div>;
};
Recipes
Creating simple model
import { create } from "remos";
// create a model with specified props
// the created model has binded properties
// when binded property changed, the change listeners will be called
const counter = create({ count: 1 });
// register change listener
counter.$listen(() => console.log("changed"));
counter.count++; // update count prop
Connecting model to React component
import { create, useModel } from "remos";
const counter = create({ count: 1 });
const App = () => {
// bind the model to the component
// when model is changed, the component re-renders
useModel(counter);
return <h1>{counter.count}</h1>;
};
Selecting single model prop
It detects changes with strict-equality (old === new) by default.
import { create, useModel } from "remos";
const counter = create({ count: 1, other: 2 });
const App = () => {
// select only count prop value
// that means the component will re-render when count prop change only
// nothing happens if other prop is changed
const count = useModel(counter, (props) => props.count);
return <h1>{count}</h1>;
};
Selecing multiple model props
For more control over re-rendering, you may provide an alternative equality function on the thrid argument.
import { create, useModel } from "remos";
const user = create({ firstName: "", lastName: "", age: 50 });
const App = () => {
const result = useModel(
user,
(props) => (
{
firstName: props.firstName,
lastName: props.lastName,
},
// use shallow compare or pass custom equality function here
"shallow"
)
);
return (
<h1>
{result.firstName} {result.lastName}
</h1>
);
};
Handling multiple model updates
const model1 = create({});
const model2 = create({});
const model3 = create({});
const App = () => {
// the component will be re-rendered whenever model1, model2, model3 are updated
useModel([model1, model2, model3]);
return <></>;
};
Selecting props from multiple models
const author = create({ name: "Bill" });
const article = create({ title: "React basis" });
const App = () => {
const result = useModel(
{ author, article },
(props) =>
`The article ${props.article.title} is written by ${props.author.name}`
);
const { authorName, articleTitle } = useModel(
{ author, article },
(props) => ({
authorName: props.author.name,
articleTitle: props.article.title,
}),
"shallow" // using shallow compare if the selector returns complex object
);
return <div>{result}</div>;
};
Demos
- Counter App
- Todo App
- Using React Suspense
- Family Model
- Local Model
- Hacker News
- Search Box
- Performance Test