@canlooks/reactive
v4.2.1
Published
A react tool for responding data and managing state.
Downloads
300
Maintainers
Readme
@canlooks/reactive
This is a simple and lightweight React
tool for responding data, auto re-render and managing state.
Installation
npm i @canlooks/reactive
A quick example
A react-class-component
which using @canlooks/reactive
will look like this.
import {RC} from '@canlooks/reactive/react'
@RC
export default class Index extends React.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>
)
}
}
and Function component
will look like this:
import {RC, useReactive} from '@canlooks/reactive/react'
const Index = RC(() => {
const data = useReactive({
count: 1
})
const increase = () => {
data.count++
}
return (
<div>
<div>{data.count}</div>
<button onClick={increase}>Increase</button>
</div>
)
})
Contents
Basic API
reactive() & reactive.deep()
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 // Both instance and static properties are reactive.
})
// using decorator
@reactive
class Data {
a = 1
}
You can also create a reactive React Component
like quick example.
reactor()
import {act, reactive, reactor} from '@canlooks/reactive'
const obj = reactive({
a: 1,
b: 2
})
const disposer = reactor(() => obj.a, (to, from) => {
console.log(`"obj.a" was changed from ${from} to ${to}`)
})
act(() => obj.a++) // log: "obj.a" was changed from 1 to 2
act(() => obj.b++) // nothing happen
disposer() // Remember to dispose if you don't use it anymore.
declare type ReactorOptions = {
immediate?: boolean
once?: boolean
}
declare function reactor<T>(refer: () => T, effect: (newValue: T, oldValue: T) => void, options?: ReactorOptions): () => void
autorun()
const obj = reactive({
a: 1
})
const disposer = autorun(() => {
console.log('Now "obj.a" is: ' + obj.a)
})
action()
Every methods for modifying reactive data
are strongly suggest wrapping in "action".
const obj = reactive({
a: 1,
b: 2
})
reactor(() => [obj.a, obj.b], () => {
// ...
})
// Good, effect will trigger only once.
const increase = action(() => {
obj.a++
obj.b++
})
// Bad, effect will trigger twice.
const decrease = () => {
obj.a++
obj.b++
}
act()
IIFE for action()
act(() => {
//
})
// is equivalent to
action(() => {
//
})()
Automatic allocation
Each property in reactive object
or class
will allocate automatically.
const data = reactive({
// Become reactive property
count: 1,
// Become computed property
get double() {
// This function will not execute repeatedly until "count" change.
return this.count * 2
},
// Become action
increase() {
this.count++
}
})
External data & sharing state
No need to provide/inject
, just use it.
import {act, reactive} from '@canlooks/reactive'
import {RC} from '@canlooks/reactive/react'
const data = reactive({
a: 1,
b: 2
})
@RC
class A extends React.Component {
render() {
return (
<div>{data.a}</div>
)
}
}
const B = RC(() => {
return (
<div>
<div>{data.a}</div>
<div>{data.b}</div>
</div>
)
})
// Modify data everywhere.
act(() => data.a++) // Both component "A" and "B" will update.
act(() => data.b++) // Only component "B" will update.
Additional functions
<Chip/>
<Chip/>
can take component to pleces for updating only a small part.
import {Chip, RC, useReactive} from '@canlooks/reactive/react'
// In this case, component "Index" will never re-render.
const Index = RC(() => {
const data = useReactive({
a: 1,
b: 2
})
return (
<div>
<Chip>
{/*Re-render when only "a" is modified*/}
{() => <ChildA count={data.a}/>}
</Chip>
<Chip>
{/*Re-render when only "b" is modified*/}
{() => <ChildB data={data.b}/>}
</Chip>
</div>
)
})
chip()
is an function way for <Chip/>
chip(() => <AnyComponent/>)
// is equivalent to
<Chip>{() => <AnyComponent/>}</Chip>
<Model/>
<Model/>
has advanced usage like advanced, and common usage like this.
const Index = RC(() => {
const data = useReactive({
// This value always sync with value of <input/>
value: 'Hello'
})
return (
<div>
<Model refer={() => data.value}>
<input/>
</Model>
</div>
)
})
useModel()
const Index = RC(() => {
const data = useModel('Hello Reactive')
// "data" has "value" and "onChange" props.
return (
<div>
<p>Input value is: {data.value}</p>
<input {...data}/>
{/*or use like this*/}
<input value={data.value} onChange={data.onChange}/>
</div>
)
})
@watch()
@watch()
is a syntactic sugar of reactor()
import {watch} from '@canlooks/reactive'
import {RC} from '@canlooks/reactive/react'
@RC
class Index extends React.Component {
@watch(() => someExternal.data)
effect1() {
// This effect will trigger when "someExternal.data" modified.
}
a = 1
@watch(t => t.a) // t === this
effect2() {
// This effect will trigger when "this.a" modified.
}
}
@loading()
import {loading} from '@canlooks/reactive'
import {RC} from '@canlooks/reactive/react'
@RC
class Index extends React.Component {
busy = false
@loading(t => t.busy) // t === this
async myAsyncMethod() {
// It changes "busy" to true,
// and changes 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>
)
}
}
useLoading()
const Index = RC(() => {
const method = useLoading(async () => {
// ...
})
return method.loading
? <div>Loading...</div>
: <button onClick={method.load}>button</button>
})
autoload()
To define a common business data automatically.
abstract class Autoload<D, A> {
loading: boolean
data: D
setData(v: D | undefined): void
abstract loadData(...args: A[]): D | Promise<D>
update(...args: A[]): Promise<D>
}
function autoload<D, A>(loadData: (...args: A[]) => D | Promise<D>, options?: ReactiveOptions): Autoload<D, A>
example:
@reactive
class MyData extends Autoload {
async loadData() {
const res = await fetch('fetch/my/data')
return await res.json()
}
}
const myData = new MyData()
// Automatic load data on first use.
console.log(myData.data)
// Load data again.
myData.update()
defineStorage()
This method create a reactive data
which always sync with 'localStorage' or 'sessionStorage'.
type Options = {
mode?: 'localStorage' | 'sessionStorage'
async?: boolean
debounce?: number
}
declare function defineStorage<T>(
name: string,
initialvalues?: T,
options?: Options
): T
example:
const userStorage = defineStorage('user', {
name: 'canlooks',
age: 18
})
// then modifying "userStorage" will sync with 'localStorage'
userStorage.age++
defineForage()
It's similar to defineStorage()
, but it use localforage
to store data.
type Options = {
instance?: typeof localforage
deep?: boolean
}
declare function defineForage<T>(
name: string,
initialvalues?: T,
options?: Options
): T
or using Forage
class which extends Autoload
class Forage extends Autoload {
dispose(): void
}
Hooks
useReactive()
It's encapsulated like:
function useReactive<T>(initialValue: T): T {
return useMemo(() => reactive(initialValue), [])
}
useReactor()
function useReactor(refer: () => T, effect: (newValue: T, oldValue: T) => void, options?: ReactorOptions): void {
useEffect(() => {
return index(refer, effect, options)
}, [])
}
useAutorun()
function useAutorun(fn: () => void): void {
useEffect(() => {
return autorun(fn)
}, [])
}
useAction()
function useAction<F extends (...args: any[]) => any>(fn: F): F {
return useCallback(action(fn), [])
}
useAutoload()
function useAutoload<D, A>(loadData: (...args: A[]) => D | Promise<D>, options?: ReactiveOptions): Autoload<D, A> {
return useMemo(() => autoload(), [])
}