English | 简体中文
Table of Contents
- Reactive data flow.
- Selector hook allows extracting state from store, and high-performance optimized.
- Perfectly TypeScript support.
import { store, useSelector } from 'hodux';
// create an observable object
const counter = store({
num: 0,
other: '',
inc() { counter.num += 1; }
});
// select state from store
export default function Counter(props) {
const num = useSelector(() => counter.num);
// or you can do some compute in component
// const total = useSelector(() => counter.num + props.step);
return <div onClick={counter.inc}>The num:{num}</div>;
}
npm install --save hodux
# or
$ yarn add hodux
- Signature:
function store<M extends object>(model: M): M
- Description: pass in a pureModel or viewModel and returns a proxied-based observable object, and the observable object behave like origin object(just normal js object), it's just a wrapper of ES6 Proxy binding.
Create store with viewModel
// stores/counter.js
const counter = store({
count: 0,
inc() {
counter.count++;
},
// Async operations can be expressed with the standard async/await syntax
async incx() {
await wait(1000);
counter.count += 1;
}
});
export default counter;
Create store with pureModel
// stores/counter.js
export default store({ count: 0 });
// src/Counter.js
import counter from './stores/counter';
// changing data anywhere outside the store, and the components can perceive this changes
const incx = async () => {
await wait(1000);
counter.count += 1;
};
export function Counter() {
const count = useSelector(() => counter.count);
return <div onClick={incx}>{count}</div>;
}
Lazy creates
// stores/counter.js
export default (initalCount = 0) => {
const state = store({ count: initalCount });
function inc() {
state += n;
}
async function incx() {
await wait(1000);
state.count += 1;
}
return { state, inc, incx }
}
Create complex or large store(any valid JS structure)
// stores can include nested data, arrays, Maps, Sets, getters, setters, inheritance, ...
const person = store({
// nested object
profile: {
firstName: 'Bob',
lastName: 'Smith',
// getters
get name() {
return `${person.firstName} ${person.lastName}`
},
age: 25
},
// array
hobbies: [ 'programming', 'sports' ],
// collections
familyMembers: new Map(),
});
// changing stores as normal js objects
person.profile.firstName = 'Daid';
delete person.profile.lastName;
person.hobbies.push('reading');
person.familyMembers.set('father', father);
person.familyMembers.set('mother', mother);
- Signature:
function useSelector<V>(selector: Selector<V>, config?: Config<V>): V
- Description: extracts state from store as needed, the components will not re-render unless any selected state changes.
Maybe it's the main difference with react-redux's useSelector(), because react-redux call selector whenever store state changes even not selected at all(react-redux internal decides if makes re-render), so you do't need to use any cache selector library(such as reselect) with useSelector.
useSelector
accepts two parameters:
-
the first parameter is a
selector
function which works as observer API in reactivity system. It subscribes the selected state and diff the previous returned value with the next one to decide if or not re-render. Maybe you can do some compute with state inuseSelector
and takes result as the return value. -
the second is an optional config object
-
equals
: the compare function between previous return value and the next return value, the defalut is equality -
debugger
: the debugger function passed to@nx-js/observer-util
-
Returns basic type(is recommended)
function Counter() {
const num = useSelector(() => counter.num);
return <div>{num}</div>;
}
Computed(calculation cache)
function App() {
const computed = useSelector(() => {
const items = store.items; // select items from store
return items.reduce((acc, item) => acc + item.value, 0);
});
return <div>{computed}</div>;
}
Select state from multiple stores
function CompWithMutlStore() {
// whenever the `count` from store1 or the `step` from store1 changes the compoent will re-render,
// so the `result` is always be the newest value
const result = useSelector(() => store1.count + store2.step);
}
You should pass in equals function when returns complex types
function TodoView() {
const [isEmpty, hasCompleted, allCompleted, active, filter] = useSelector(
() => [
todoStore.isEmpty,
todoStore.hasCompleted,
todoStore.allCompleted,
todoStore.activeType,
todoStore.filterType
],
{ equals: _.equals } // use lodash/isEqual
);
...
}
🚨Attention please, selector
should not returns non-serializable value such as function, Symbol or ES6 collection, because they are incomparable, you should select out plain serializable objects, arrays, and primitives. This issues is similar to react-redux hooks, check the document or this issue, but the target model pass to store()
has no this limitations, you should convert non-serializable to serializable before returning.
🚨You should returns serializable value
function Component() {
// DON'T DO THIS
const familyMemebers = useSelector(() => person.familyMemebers);
// DO THIS
const [father, mother] = useSelector(() => [
person.familyMemebers.get('father'),
person.familyMemebers.get('mother')
]);
...
}
function connect<V extends {}, OwnProps = {}>(
selector: Selector<V, OwnProps>,
config?: Config<V>
): (classComponent: C) => ConnectedComponent<V, OwnProps>
An HOC wrapper of useSelector
to connect store state to the class components, and is only for class components.
connect
accepts two parameters:
-
selectorWithProps(ownProps?)
: familiar to selector, but the difference is selectorWithProps must return object type(such asmapStateToProps
in react-redux), selectorWithProps accepts the connected component's props as parameter. -
config
: same as useSelector's config parameter
Class components
const counter = store({
n: 0,
inc() { counter.n += 1; }
});
const selectToProps = () => ({ n: counter.n });
class Counter extends Component {
render() {
return <div onClick={counter.inc}>{n}</div>;
}
}
export default const ReactivedCounter = connect(selectToProps)(Counter);
ownProps
const selectToProps = (props) => ({
step: props.step,
n: testStore.n
});
class Counter extends React.Component {
state = { n: this.props.n }
inc() {
const n = this.state.n + this.props.step;
this.setState({ n });
}
render() {
return <div onClick={() => this.inc()}>{this.state.n}</div>;
}
}
const Connected = connect(selectToProps)(Counter);
render(<Connected step={10} />);
- Type:
React.FunctionComponent<React.PropsWithChildren<Config<any>>>
- Description: The global config Provider.
function consoleLogger(e) {
if (e.type !== 'get') {
console.log(`[${e.type}]`, e.key, e.value);
}
}
ReactDOM.render(
<HoduxConfig debugger={consoleLogger}>
<App />
</HoduxConfig>,
document.getElementById('root')
);
- Signature:
function batch(fn: Function) => void
- Description: a wrapper of
unstable_batchedUpdates
, to prevent multiples render caused by multiple store mutations in asynchronous handler such assetTimeout
andPromise
, etc. If you experience performance issues you can batch changes manually withbatch
.
The React team plans to improve render batching in the future. The
batch
API may be removed in the future in favor of React's own batching.
const listStore = store({
loading: false,
list: []
});
listStore.load = async () => {
testStore.loading = true;
const list = await fetchData();
batch(() => {
testStore.loading = false;
testStore.list = list;
});
}
The examples folder contains working examples. You can run one of them with
$ cd examples/[folder] && npm start
then open http://localhost:3000 in your web browser.
- @nx-js/observer-util Transparent reactivity with 100% language coverage. Made with ❤️ and ES6 Proxies.
- react-easy-state Simple React state management
Hodux is Inspired by react-easy-state but considered for React Hooks.