README
@canlooks/reactive
This was my private tool originally. Many of my workmates thought it was useful. Let me share.
This is a very simple and lightweight tool for responding data and managing state.
It's similar to mobx
a little, but more simple and freewheeling.
Install
npm i @canlooks/reactive
A quick example
The code which using @canlooks/reactive
looks like this.
import React from 'react'
import {Component, reactive} from '@canlooks/reactive'
@reactive
export default class Index extends Component {
count = 1
test = 2
onClick() {
this.count++ // This component will update.
}
myTest() {
this.test++
// This component will NOT update,
// because "test" have never referred in "render()"
}
render() {
return (
<div>
<div>{this.count}</div>
<button onClick={this.onClick}>Increase</button>
<button onClick={this.myTest}>Test</button>
</div>
)
}
}
Contents
Basic functions
Optimization & additional functions
reactive()
There are 3 ways to create a reactive data
.
import {reactive} from '@canlooks/reactive'
// create by object
const data = reactive({
a: 1,
b: 2
})
// create by class
const DataClass = reactive(class {
a = 1
static b = 2 // It doesn't work.
})
// Only instance properties are listened.
const dataInstance = new DataClass()
// using decorator
@reactive
class Data {
a = 1
}
You can even create a reactive React Class Component like quick example, or functional component like this
const Index = reactive(() => {
return (
<div>Hello Reactive</div>
)
})
@reactive.pure()
It's a reactive way to create React.PureComponent
.
import {Component, reactive} from '@canlooks/reactive'
@reactive.pure
class Index extends Component {
}
useReactive()
To create an internal reactive state in functional component.
import React, {useCallback} from 'react'
import {reactive, useReactive} from '@canlooks/reactive'
const Index = reactive(() => {
const data = useReactive({
count: 1
})
const onClick = useCallback(() => {
data.count++
// This component will update.
// Even in "useCallback", data is up to date.
}, [])
return (
<div>
<div>{data.count}</div>
<button onClick={onClick}>increase</button>
</div>
)
})
External data & sharing state
You do not have to provide/inject
, just use it.
import {Component, reactive} from '@canlooks/reactive'
const data = reactive({
a: 1,
b: 2
})
@reactive
class A extends Component {
render() {
return (
<div>{data.a}</div>
)
}
}
const B = reactive(() => {
return (
<div>
<div>{data.a}</div>
<div>{data.b}</div>
</div>
)
})
// Change data everywhere.
data.a++ // Both component "A" and "B" will update.
data.b++ // Only component "B" will update.
props & $props
Because of React
's limitation. props
in Class Component is not proxiable, so there is a clone property
named $props
.
import {Component, reactive, watch} from '@canlooks/reactive'
@reactive
class Index extends Component<{
a: any
b: any
}> {
@watch(t => t.props)
effect1() {
// Any of properties in "props" change,
// this effect will execute regardless.
}
@watch(t => t.$props.a) // use "$props"
effect2() {
// this effect will execute when only prop "a" change.
}
}
I recommend using $props
instead props
in every Class Components.
And extending Component
in "@canlooks/reactive" instead "React", if you use typescript
.
Specially, props in functional component is proxiable, so there is no $props in functional component.
<Fragment/>
<Fragment/>
can take component to pleces for updating only a small part, not the entire component.
import {Fragment, reactive} from '@canlooks/reactive'
@reactive
class Index extends Component {
count = 1
test = 2
onClick() {
this.count++
// Component "Index" and "ChildB" will not re-render,
// only component "ChildA" will update.
}
render() {
return (
<div>
<Fragment>
{() => <ChildA count={this.count}/>}
</Fragment>
<Fragment>
{() => <ChildB data={this.test}/>}
</Fragment>
<button onClick={this.onClick}>Increase</button>
</div>
)
}
}
<Model/>
<Model/>
can create two-way data bindings on form elements.
import {Model, reactive} from '@canlooks/reactive'
@reactive
class Index extends Component {
inputValue = 'Hello Reactive' // This value always sync with value of <input/>
render() {
return (
<div>
<Model bindState={() => this.inputValue}>
{() => <input/>}
</Model>
</div>
)
}
}
props | type | description |
---|---|---|
bindState | Function | Reference function, In this function, referred property which made by reactive() will bind on child element. |
children | ReactElement ()=>ReactElement |
I recommend using function type. Otherwise, children will NOT update except value changed. |
onChange | Function | To define your own onChange callback. |
@watch()
@watch()
can respond to reactive data
's change. If it is used in React Component, it will dispose automatically
when componentWillUnmount
.
import {Component, reactive, watch} from '@canlooks/reactive'
@reactive
class Index extends Component {
@watch(() => someExternal.data)
effect1() {
// This effect will execute when "someExternal.data" change.
}
a = 1
b = 2
@watch<Index>(t => [t.a, t.b]) // t === this
effect2() {
// This effect will execute when "this.a" or "this.b" change.
}
render() {
return (
<div>
</div>
)
}
}
type Options = { immediate?: boolean, once?: boolean }
function Watch(refer: Refer, options: Options): MethodDecorator
Type of Refer | description |
---|---|
<T>(this: T, context: T) => any | This is a reference function. In that function, every referred properties which made by reactive() will bind on effect.You just need to refer them, return value is not a must. |
@loading()
In some cases, many async functions take a long time to return. @loading()
could be a good helper.
import {loading, reactive} from '@canlooks/reactive'
@reactive
class Index extends Component {
busy = false
@loading(t => t.busy) // t === this
async myAsyncMethod() {
// It change "busy" to true,
// and change false back until this function return.
}
stack = 0
@loading(t => t.stack)
async concurrent() {
// It make "stack" +1,
// and -1 until this function return.
}
render() {
return (
<div>
{(this.busy || this.stack !== 0) &&
<div>I'm busy</div>
}
</div>
)
}
}
As mentioned above, the parameter in @loading()
is reference function.
@noOverlap()
import {noOverlap, reactive} from '@canlooks/reactive'
@reactive
class Data {
@noOverlap
async mayConcurrent() {
// This function can not execute concurrently.
}
}
const data = new Data()
data.mayConcurrent()
.then(console.log)
data.mayConcurrent() // This method may not execute,
.then(console.log) // but you can still get the return value.
@cache()
@cache()
extends @noOverlap()
, the decorated function will not execute concurrently.
In addition, @cache()
create a memoized function which execute only once if you don't force update.
It may be helpful for managing public data.
import {cache, reactive} from '@canlooks/reactive'
@reactive
class Data {
@cache cachedMethod() {
}
}
const data = new Data()
data.cachedMethod()
data.cachedMethod() // This method may not execute, but you can still get the return value.
There are several ways to force execute a cached function.
import {cache, CacheParams, reactive} from '@canlooks/reactive'
@reactive
class Data {
@cache
// Define parameter type to "CacheParams" if you use typescript.
cachedMethod(params: CacheParams<{}>) {
}
@cache((t, params) => params.times === 2)
myCachedMethod(params: {
times: number
}) {
}
}
const data = new Data()
data.cachedMethod({forceUpdate: true}) // Default way.
data.myCachedMethod({times: 2}) // Custom way.
cache.update(data.cachedMethod)() // High-order function
function Cache(refer: Refer): MethodDecorator
Type of Refer | description |
---|---|
(this: T, context: T, ...args: any[]) => boolean | This is a function whose return value determines whether cached needs to be updated. |
nextTick()
import React, {createRef} from 'react'
import {Component, nextTick, reactive} from '@canlooks/reactive'
@reactive
class Index extends Component {
showInput = false
inputElement = createRef()
async onClick() {
this.showInput = true
console.log(this.inputElement.current) // null
await nextTick()
console.log(this.inputElement.current) // <input/>
}
render() {
return (
<div>
<button onClick={this.onClick}>button</button>
{this.showInput &&
<input ref={this.inputElement}/>
}
</div>
)
}
}
function nextTick<T>(callback?: (...args: T[]) => void, ...args: T[]): Promise<T>
reactor()
reactor()
is similar to @watch()
, but you should dispose it by yourself.
import {reactive, reactor} from '@canlooks/reactive'
const obj = reactive({
a: 1,
b: 2
})
const disposer = reactor(() => obj.a, () => {
console.log('"obj.a" was changed to ' + obj.a)
})
obj.a++ // log: "obj.a" was changed to 2
obj.b++ // nothing happen
disposer() // To dispose if you don't use it anymore.
function reactor(refer: Refer, effect: Function): Disposer
As mentioned above, the first parameter is reference function.
autorun()
autorun()
is a syntactic sugar of reactor()
.
import {reactive, autorun} from '@canlooks/reactive'
const obj = reactive({
a: 1
})
const disposer = autorun(() => {
console.log('Now "obj.a" is: ' + obj.a)
})
Notes
Shallow proxy
reactive()
method create a shallow-proxy object, it means only top-level properties are responsive. For example
import {Component, reactive} from '@canlooks/reactive'
@reactive
class Index extends Component {
obj = {
a: 1
}
method() {
this.obj.a++ // This component doesn't update yet.
this.obj = {...this.obj} // It works.
}
render() {
// ...
}
}
You can "reactive" deeper if you need it. For example
import {Component, reactive} from '@canlooks/reactive'
@reactive
class Index extends Component {
obj = reactive({
a: 1
})
method() {
this.obj.a++ // It works
}
render() {
// ...
}
}
Auto-cache getter
"getter" property will auto-cache.
import {reactive} from '@canlooks/reactive'
@reactive
class Data {
count = 1
get double() {
this.count * 2
// This function will not execute repeatedly until "count" changes.
}
}
AutoBind
import {Component, reactive} from '@canlooks/reactive'
@reactive
class Index extends Component {
onClick() {
console.log(this) // Always get the correct "this".
}
render() {
return (
<button onClick={this.onClick}>button</button>
)
}
}
Automatic batching
Before React@18.0.0
, batching isn't automatic. Setting states in async-function will probably make component update
many times unnecessarily.
But in @canlooks/reactive
, components updating are batched.
import {Component, nextTick, reactive} from '@canlooks/reactive'
@reactive
class Index extends Component {
a = 1
b = 2
method1() {
setTimeout(() => {
this.a++
this.b++
// This component update only once.
}, 1000)
}
async method2() {
this.a++
await nextTick()
this.b++
// This component will update twice.
}
render() {
// ...
}
}
The reference function
Like @watch()
<Model/>
@loading()
reactor()
, they all have a similar parameter called reference-function. It
only works in sync-function. For example
import {reactive, watch} from '@canlooks/reactive'
@reactive
class Data {
@watch(() => {
setTimeout(() => {
someExternal.data // It doesn't work.
}, 0)
})
method1() {
}
@watch(async () => someExternal.data) // It doesn't work.
method2() {
}
@watch(() => someExternal.data) // It works.
method3() {
}
}