A fast, intuitive, access path based reactive react state management
ES5
andES6
supported. use traps functions to collect accessed property path of React component.- When base data updates, relink the changed proxy object and leave others untouched.
PathNodeTree
is the data structure to perform diff action. In Relinx, Only accessed property paths are used to buildpathNodeTree
which help to optimize comparison performance.- In order to embrace
React-Redux
community, middleware is re-write base onRedux middleware
. - Fine-grained render controls. According to
pathNodeTree
, we can know which component should update when base value changes. - Functional component only, and React
Hooks
supported
Conceptually, Relinx is inspired by lots of open source library likes React-Redux
, dva
etc. It includes action
,reducers
, dispatch
and effects
as well.
On handle side effect, In order to simply learning curve and leverage usage complexity, Relinx do not apply any other library like redux-saga
and use a variant of redux-thunk
.
The Relinx design principle is very simple
Collect accessed properties paths of component, then re-render component if bind pathNode detect value change.
- A observed component will create a tracker and patcher. Tracker is used to collect access paths. Patcher is used to re-render component when access path value change.
- Patcher will be added to top level application. According to patcher's paths value, application will create a PathNodeTree
- Dispatch an action to reducer will create changed value group. In order to find which paths value has been updated, changed value will be passed to application.
- In application, Based on created PathNodeTree, perform diff algorithm. If a pathNode is detected with value change. Its patcher will be pushed to pendingPatchers.
- Once comparison and patcher clear up is finished. pendingPatchers will be trigger running. The related component will be re-rendered.
- Component's re-render will cause the re-creation of tracker paths. component will patcher will be add to application again to update PathNodeTree.
$ npm install relinx --save
The following is a simple Relinx usage example.
// index.js
import React from "react"
import ReactDOM from "react-dom"
import {logger, Provider, createStore, applyMiddleware, thunk} from "relinx"
import Models from "./models"
import App from "./views"
const store = createStore(
{
models: new Models()
},
applyMiddleware(thunk, logger)
)
const Simple = () => (
<Provider store={store}>
<App />
</Provider>
)
ReactDOM.render(<Simple />, document.getElementById("app"))
// models.js
import appModel from "./appModel"
export default () => ({
app: new appModel()
})
// appModel.js
export default () => ({
state: {count: 0},
reducers: {
increment: state => ({count: state.count + 1})
}
})
// app.js
import React, {useCallback} from "react"
import {useRelinx, observe} from "relinx"
export default observe(() => {
const [state, dispatch] = useRelinx("app")
const {count} = state
const handleClick = useCallback(() => dispatch({type: "increment"}), [])
return (
<div>
<span>{count}</span>
<button onclick={handleClick}>+</button>
</div>
)
})
In observe
wrapped component function, calls useRelinx
will return an array value. The first item is Proxy State
which is decorated with getter trap. Accessed path will be collect when its property is used.
Action is composite of type
and payload
. Its usage is the same as in Redux
. It is the only way to trigger state update, which should be called with dispatch
function.
Process action in sync way, and result in changed value.
Backend with a redux-thunk
variant, Process action in async way. all the ajax
should be placed here.
wrap a functional component and make it access path sensitive
It's a prerequisite when you want useRelinx
in component. The usage of observe
is to create a Tracker
scope. Scope is made to achieve fine-grained
re-render control.
In order to make DEBUG
info easy to read, it'd better to wrap a named
function component, like as follows.
const A = () => <span>hello world</span>
const ObservedA = observe(A)
Theoretically, any React component is recommended to wrapped with observe function. It will create an individual update context for wrapped component. However, it only meaningful when the component has proxy state
passing from parent or use useRelinx
method.
const [state: Proxy, dispatch: Function] = useRelinx((modelName: String))
useRelinx
will return a two length array value.
The first item is state
. state
is an proxy
object, and its value is derived from store base value and configuration in exported model
file. Refer to usage example listed above.
const A = observe(() => {
const [state] = useRelinx("app") // state value: { count: 1 }
return <span>{state.count}</span>
})
The second item is dispatch
, The same usage as in React-Redux
useRelinx
could only be used in functional component- When use
useRelinx
, component or its ancestor component is required to be wrapped withobserve
function - It'd better to wrap component with
observe
function when useuseRelinx
const [dispatch: Function] = useDispatch()
A straightforward hooks method to return dispatch
if you do not need state.
By default, its value is true
. It makes backend
Tracker to use Proxy
or defineProperty
. However, Tracker
will detect current context whether Proxy
is supported first. If it's false, useProxy
value will be ignored.
By default, its value is false. Normally, it is used on dev condition. It helps to log the undeclared properties used by state
. It's meaningful when ES5
should be supported. The below section have made a detail explanation
By default, Relinx will display the message why a component update in development mode. But it could be set with false
to disable.
It is optional, In order to support multiple Provider
in app
, Relinx will create a random namespaceKey
if it is omitted.
Do not attempt to call consecutive dispatch
, the intermediate action will omitted. You could find the answer from Dan's tweet
In Relinx, you can dispatch an action array to fix.
dispatch([action, action, action])
- For primitive type value, the literal value should be equal.
- For Array or object, the comparison will break if their reference are equal. or comparison will continue until
pathNode
is exhausted.
Recently, Relinx only support Primitive Type
, Array
and object
types.
For ES5
, Tracker
use defineProperty
to re-define getter
trap. However, it is not possible to define getter
trap on undeclared
properties, which means Tracker
can not collect paths of these kinds of property access correctly. By the way, component re-render is derived by paths, which may cause non-update even though base value update.
// appModel.js
export default () => ({
state: {location: {}}
})
// view.js
const A = observe(() => {
const [state] = useRelinx("app")
return <span>{state.location.city}</span>
})
appModel.js
should be the following format.
// appModel.js
export default () => ({
state: {
location: {city: null}
}
})
About empty value, it should be defined as null
instead of undefined
. Because Relinx will check property value's value, if it is undefined
, it will be regarded as undeclared
property. which may make confuse when strictMode
is set true
Set true value to Provider
's strictMode
prop. Then you can check warning in console log.
<Provider strictMode>
<App />
</Provider>
- For ES6,
array.length
could be trapped bygetter
trap automatically, so it has no problem when callsmap
,filter
etc. - For ES5, the descriptor of Array's proto functions like
map
,filter
etc could be override.Tracker
will collectlength
path when calls these functions. However, there is an exception.array.length
's descriptor could not be override, which meansfor
statement should be take care. You will lostlength
path if you writearr.length
as condition statement.
For memory leaks
, it always talk about how to remove unused subscriptions. In Relinx, subscription happens when add a patcher
to pathNode
. When pathNode
find property value changes will do the following actions:
- If it is primitive type value, compare its literal value. if not equal,
patchers
will be pushed topendingDispatchers
, and removepatchers
from otherpathNodes
- If it is Array type value, and has new element added. add
patchers
topendingDispatchers
, and removepatchers
from otherpathNodes
- If it is Array type value, and has element deleted. add
patchers
topendingDispatchers
, and these removed elements will not perform comparison action. After finish comparingPathNodeTree
, destroy theseremove
PathNode. - If it is object type value, has key removed and this key has related
pathNode
. addpatchers
topendingDispatchers
, at the mean while, continue the comparison of remove keys. After finish comparingPathNodeTree
, destroy theseremove
PathNode. - When component is un-mount, its related patcher will be destroyed.
- When component is re-rendered, the paths of patcher will be updated, and re-bind
patcher
topathNode
For array's re-render, basically it caused by two factors: length
change or item[key]
change.
In general, the change of length value will cause items
's component to re-render. On the base of React update mechanism, parent component will trigger child components' re-render. For this situation, React.memo
could help to isolate update if it is unnecessary to child component
It is recommend that parent and child component should be wrapped with observe
function. If item[key]
changes, it will only trigger corresponding child component to re-render.
Comparing with Redux
, it use shallowEqual
to verify value's update. mapStateToProps
will be called on every time state
changes which cause unnecessary keys' comparison or make performance issue. React-Redux
official suggest to use reselect to avoid this kind of issue.
However, Relinx create pathNode
for accessed property,and only property with pathNode
will perform diff algorithm. So unnecessary keys' comparison could not happen by default.