English | 简体中文
The next-generation state manager for React.
- Just one API, simple and efficient. Almost no learning cost.
- Define model with custom Hooks.
- Perfect typescript support.
- Supports multiple data sources.
yarn add hox
# Or
npm install --save hox
In hox, you can process a custom Hook with createModel
to make it persistent and globally shared.
import { useState } from "react";
import { createModel } from "hox";
function useCounter() {
const [count, setCount] = useState(0);
const decrement = () => setCount(count - 1);
const increment = () => setCount(count + 1);
return {
count,
decrement,
increment
};
}
export default createModel(useCounter);
By calling
createModel
, hox will return a new custom Hook, which is used for retrieving model data. Furthermore, we can pass values to custom hooks to the second parameter of createModels for custom hooks.
In order to retrieve the data of counter model, you need to import and call useCounterModel
in your components.
import useCounterModel from "../models/counter";
function App(props) {
const counter = useCounterModel();
return (
<div>
<p>{counter.count}</p>
<button onClick={counter.increment}>Increment</button>
</div>
);
}
useCounterModel
is a real Hook. By calling it, you can subscribe to the updates of data. So if you click the "Increment" button, the update of counter model will be triggered, and finally, hox will notify all components or Hooks using useCounterModel
.
In some scenarios, we might want to pass values to custom hooks.
Just like the example below, we could pass a value to the second parameter of createModel for the custom hook. This is the best time to set the initial value for useState
, etc.
import { useState } from "react";
import { createModel } from "hox";
function useCounter(initialValue) {
const [count, setCount] = useState(initialValue ?? 0);
const decrement = () => setCount(count - 1);
const increment = () => setCount(count + 1);
return {
count,
decrement,
increment
};
}
const useCounterModel = createModel(useCounter);
const useCounterModelWithInitialValue = createModel(useCounter, 20);
Although you can still design your model according to the traditional single data source pattern, we recommend splitting the big model into small parts. Therefore inevitably, we need to handle dependencies between multiple models. For example, the order
model depends on the account
model.
In hox, handling these depdencies is actually quite simple and straightforward: You can call useXxxModel
to retrieve another model and subscribe its updates. Just like what you can do in components.
Caution: Be careful with circular dependencies!
import { useCounterModel } from "./counter";
export function useCounterDouble() {
const counter = useCounterModel();
return {
...counter,
count: counter.count * 2
};
}
In some scenarios, we only want to read the current value of a model, without subscribing to its updates.
Just like the example below, we can read the current value of counter model from useCounterModel.data
.
useCounterModel.data
is not Hook, you can use it anywhere.
import { useState } from "react";
import { useCounterModel } from "./counter";
export function logger() {
const [log, setLog] = useState([]);
const logCount = () => {
const counter = useCounterModel.data;
setLog(log.concat(counter));
};
return {
log,
logCount
};
}
Of course, we use Hooks to define our models, but you can still retrieve and subscribe to models in class components:
class App extends Component {
render() {
const { counter } = this.props;
return (
<div>
<p>{counter.count}</p>
<button onClick={counter.increment}>Increment</button>
</div>
);
}
}
export default withModel(useCounterModel, counter => ({
counter
}))(App);
In order to control the data you want to subscribe precisely, you can pass an odditional depsFn
function to useXxxModel
.
const counter = useCounterModel(model => [model.count, model.x.y]);
This is very similiar to the deps
parameter of useMemo
or useEffect
. But remember, the depsFn
of useModel
is a function .
In addition, we recommend splitting a large model into small parts, so that not only is the code easier to maintain, but performance can also get improved.
declare function createModel(hook: ModelHook, hookArg?): UseModel;
Create a model.
The parameter is a custom Hook, used for defining the logic of model.
You can call it multiple times to create multiple models:
const useCounterModelA = createModel(useCounter);
const useCounterModelB = createModel(useCounter);
const useTimerModel = createModel(useTimer);
You can also pass values to custom hooks to the second parameter of createModels for custom hooks.
Calling
createModel(useCounter)
two times will create two instances which are isolated from each other.
useModel
UseModel
is the return type of createModel
. It is used for retrieving model and subscribing to its updates.
export interface UseModel<T> {
(depsFn?: (model: T) => unknown[]): T;
data?: T;
}
The parameter depsFn
can be omitted. And it is used for performance optimization.
useModel
is a React Hook, so please follow React's rules of hooks.
What's more, there is data
field on useModel
, which is used for read the current value of model. You'll find it quite useful when you try to just read value without subscribing to its updates, or try to use model in none-react environments.
declare function withModel(
useModel,
mapModelToProps: (model, ownProps) => object
): (C: ComponentType) => ComponentType;
type ModelMap = {
[key: string]: unknown;
};
withModel
is the bridge between models and class components. If you have ever used react-redux's connect
before, you'll find it very familiar.
The first parameter useModel
describes which models need to be obtained. You can just pass one useModel
, or multiple in the form of array.
The second parameter mapModelToProps
is used to define the mapping rule from model to component props
.
For example:
// subscibe to a single model
export default withModel(useCounterModel, counter => ({
count: counter.count
}))(App);
// subscribe to multiple models
export default withModel(
[useCounterModel, useTimerModel],
([counter, timer]) => ({
count: counter.count,
timer
})
)(App);