@canlooks/reactive
TypeScript icon, indicating that this package has built-in type declarations

3.6.6 • Public • Published

@canlooks/reactive

This is a very simple and lightweight React tool for responding data and managing state.

Install

npm i @canlooks/reactive

A react-class-component which using @canlooks/reactive will look like this.

import React from 'react'
import {reactive} from '@canlooks/reactive'

@reactive
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 {reactive, useReactive} from '@canlooks/reactive'

const Index = reactive(() => {
    const data = useReactive({
        count: 1
    })

    const increase = () => {
        data.count++
    }

    return (
        <div>
            <div>{data.count}</div>
            <button onClick={increase}>Increase</button>
        </div>
    )
})

Contents

Basic API

automatic allocation

External data & sharing state

Additional functions

Hooks

Advance


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.


In some case, you need to distinguish between ComponentClass and FunctionComponent.


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

const obj = reactive({
    a: 1
})

const disposer = autorun(() => {
    console.log('Now "obj.a" is: ' + obj.a)
})

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++
}

IIFE for action()

act(() => {
    //
})
// is equivalent to
action(() => {
    //
})()

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++
    }
})

You do not have to provide/inject, just use it.

import React from 'react'
import {act, reactive} from '@canlooks/reactive'

const data = reactive({
    a: 1,
    b: 2
})

@reactive
class A extends React.Component {
    render() {
        return (
            <div>{data.a}</div>
        )
    }
}

const B = reactive(() => {
    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.

<Chip/> can take component to pleces for updating only a small part.

import {Chip, reactive, useReactive} from '@canlooks/reactive'

// In this case, component "Index" will never re-render.
const Index = reactive(() => {
    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 alias for <Chip/>

chip(() => <AnyComponent/>)
// is equivalent to
<Chip>{() => <AnyComponent/>}</Chip>

<Model/> has advanced usage like advanced, and common usage like this.

const Index = reactive(() => {
    const data = useReactive({
        // This value always sync with value of <input/>
        value: 'Hello'
    })

    return (
        <div>
            <Model refer={() => data.value}>
                <input/>
            </Model>
        </div>
    )
})
const Index = reactive(() => {
    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() is a syntactic sugar of reactor()

import React from 'react'
import {reactive, watch} from '@canlooks/reactive'

@reactive
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.
    }
}

import React from 'react'
import {loading, reactive} from '@canlooks/reactive'

@reactive
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>
        )
    }
}
const Index = reactive(() => {
    const method = useLoading(async () => {
        // ...
    })
    
    return method.loading
        ? <div>Loading...</div>
        : <button onClick={method.load}>button</button>
})

type Options = {
    mode?: 'localStorage' | 'sessionStorage'
    sync?: boolean
}

declare function storage<T>(
    name: string,
    initialvalues?: T,
    options?: Options
): T

This method create a reactive data which always sync with 'localStorage' or 'sessionStorage'.


It's encapsulated like:

function useReactive<T>(initialValue: T): T {
    return useMemo(() => {
        return reactive(initialValue)
    }, [])
}
function useReactor(refer: () => T, effect: (newValue: T, oldValue: T) => void, options?: ReactorOptions): void {
    useEffect(() => {
        return index(refer, effect, options)
    }, [])
}
function useAutorun(fn: () => void): void {
    useEffect(() => {
        return autorun(fn)
    }, [])
}
function useAction<F extends (...args: any[]) => any>(fn: F): F {
    return useCallback(action(fn), [])
}

Set these option in tsconfig.json

{
  "compilerOptions": {
    "jsx": "react-jsx",
    "jsxImportSource": "@canlooks/reactive/jsx"
  }
}

If you use @emotion, set options like this

{
  "compilerOptions": {
    "jsx": "react-jsx",
    "jsxImportSource": "@canlooks/reactive/jsx/emotion"
  }
}

then, you can use two-way binding like this

const Index = reactive(() => {
    const data = useReactive({
        // This value always sync with value of <input/>
        value: 'hello'
    })
    
    return (
        <input data-model={() => data.value}/>
    )
})

If your <Input/> has particular onChange callback, you can do this

<input
    data-model={() => data.value}
    // return the actual value
    data-model-update={e => e.target.value}
/>

Package Sidebar

Install

npm i @canlooks/reactive

Weekly Downloads

6

Version

3.6.6

License

MIT

Unpacked Size

102 kB

Total Files

67

Last publish

Collaborators

  • canlooks