velo-hooks
v1.0.1
Published
Velo Hooks ---
Downloads
2
Readme
Velo Hooks
The Velo Hooks provide state management for Velo based on the concepts of hooks from solid.js.
Velo Hooks are based on the Jay Reactive and some underlying API are exposed.
Quick Start Example
In a nutshell, Velo Hooks are
import {bind, createState} from 'velo-hooks';
$w.onReady(() => {
bind($w, refs => {
let [text, setText] = createState('world');
refs.text1.text = () => `hello ${text()}`;
})
})
In the above code
text
andsetText
are state getter and setters, which ensure on the state update, any user of the state is also updated.refs.text1.text
is a setter property, which accepts a function. Anytime any dependency of the function changes, thetext1.text
value will be updated. In the above example, when thetext()
state changes, therefs.text1.text
will be updated.
Some important differences from React
- Unlike React,
text
is a function and reading the state value is a function calltext()
refs.text1.text
is updated automatically on the text state change- No need to declare hook dependencies like react - dependencies are tracked automatically
Automatic Batching
Velo-Hooks use automatic batching of reactions and updates, such that all the reactions of any state update are computed
in a single async batch. velo-hooks supports forcing sync calculation using the reactive batchReactions
or flush
APIs.
Example
Let's dig into another example - a counter
import {bind, createState, createEffect, createMemo, bindShowHide} from 'velo-hooks';
$w.onReady(() => {
bind($w, refs => {
let [counter, setCounter] = createState(30);
let formattedCounter = createMemo(() => `${counter()}`);
let tens = createMemo(() => `${Math.floor(counter()/10)}`);
let step = createMemo(() => Math.abs(counter()) >= 10 ? 5 : 1)
createEffect(() => {
console.log(tens())
})
refs.counter.text = formattedCounter;
refs.increment.onClick(() => setCounter(counter() + step()))
refs.decrement.onClick(() => setCounter(_ => _ - step()))
refs.counterExtraView.text = formattedCounter
refs.box1.backgroundColor = () => counter() % 2 === 0 ? `blue` : 'red'
bindShowHide(refs.counterExtraView, () => counter() > 10, {
hideAnimation: {effectName: "fade", effectOptions: {duration: 2000, delay: 1000}},
showAnimation: {effectName: "spin", effectOptions: {duration: 1000, delay: 200, direction: 'ccw'}}
})
})
})
In the above example we see the use of multiple hooks and binds
createState
is used to create the counter statecreateMemo
are used to create derived (or computed state). note that unlike React useEffect, we do not need to specify the dependenciescreateEffect
is used to print to the console any time thetens
derives state changes.onClick
events are bound to functions who update thecounter
statebindShowHide
is used to bind thehidden
property,show
andhide
functions to a boolean state and to animations. Alternatively, we could have usedcreateEffect
for the same result, if a bit more verbose code.bindbindCollapseExpand
is used to bind thecollapsed
property,expand
andcollapse
functions to a boolean state.bindEnabled
is used to bind theenabled
property,enable
anddisable
functions to a boolean state.bindRepeater
is used to bind a repeaterdata
property,onItemReady
andonItemRemoved
to state management per item
Reference
- bind
- Hooks
- Repeaters
- Special Bindings
- Advanced Computation Control
bind
The bind function is the entry point for initiating velo hooks. Hooks can only be used within callbacks of bind.
The common usage of bind
is
import {bind, createState} from 'velo-hooks';
$w.onReady(() => {
bind($w, refs => {
// ... your hooks logic here
})
})
formally
declare function bind<T>(
$w: $W<T>,
fn: (refs: Refs<T>) => void
): Reactive
$w
- the page$w
to build state management onfn
- state management constructorrefs
- the the equivalent of$w
for hooks, at which all properties are replaced from values to getter functions
- returns - an instance of Reactive - see below. Reactive is used for fine-grained computation control - in most cases the usage of Reactive directly is not needed
createState
Create state is inspired from solid.js and S.js, which is similar and different from React in the sense of using a getter instead of a value.
Examples of those APIs are
let initialValue = 'some initial value';
const [state, setState] = createState(initialValue);
// read value
state();
// set value
let nextValue = 'some next value';
setState(nextValue);
// set value with a function setter
let next = ' and more';
setState((prev) => prev + next);
// set an element property to track a state
refs.elementId.prop = state;
We can also bind the state to a computation, such as change in another state or memo value by using a function as the
createState
parameter
// assuming name is a getter
const [getState, setState] = createState(() => name());
// or even
const [getState2, setState2] = createState(name);
this method removes the need to use createEffect
just in order to update state
formally
type Next<T> = (t: T) => T
type Setter<T> = (t: T | Next<T>) => T
type Getter<T> = () => T
declare function createState<T>(
value: T | Getter<T>
): [get: Getter<T>, set: Setter<T>];
value
- an initial value or a getter of another state to track- returns -
get
- state getterset
- state setter
createEffect
createEffect
is inspired by React useEffect in the sense that it is
run any time any of the dependencies change and can return a cleanup function. Unlike React, the dependencies
are tracked automatically like in Solid.js.
createEffect
can be used for computations, for instance as a timer that ticks every props.delay()
milisecs.
let [time, setTime] = createState(0)
createEffect(() => {
let timer = setInterval(() => setTime(time => time + props.delay()), props.delay())
return () => {
clearInterval(timer);
}
})
formally
type EffectCleanup = () => void
declare function createEffect(
effect: () => void | EffectCleanup
);
effect
- computation to run anytime any of the states it depends on changes.EffectCleanup
- theeffect
function can return aEffectCleanup
function to run before any re-run of the effect
createMemo
createMemo is inspired by Solid.js createMemo. It creates a computation that is cached until dependencies change and return a single getter. For Jay Components memos are super important as they can be used directly to construct the render function in a very efficient way.
let [time, setTime] = createState(0)
let currentTime = createMemo(() => `The current time is ${time()}`)
Formally
type Getter<T> = () => T
declare function createMemo<T>(
computation: (prev: T) => T,
initialValue?: T
): Getter<T>;
computation
- a function to rerun to compute the memo value any time any of the states it depends on changeinitialValue
- a value used to seed the memo- returns - a getter for the memo value
mutableObject
mutableObject
creates a Proxy over an object who tracks modifications to the underlying object,
both for optimization of rendering and for computations. The mutable proxy handles deep objects,
including traversal of arrays and nested objects
It is used as
// assume inputItems is an array of some items
let items = mutableObject(inputItems);
// will track this change
items.push({todo: 'abc', done: false});
// will track this change as well
items[3].done = true;
mutableObject is very useful for Arrays and repeaters as it allows mutating the items directly
import {bind, createState, bindRepeater} from 'velo-hooks';
$w.onReady(() => {
bind($w, refs => {
let [items, setItems] = createState(mutableObject([one, two]));
bindRepeater(refs.repeater, items, (refs, item) => {
refs.title.text = () => item().title;
refs.input.onChange((event) => item().done = !item().done)
})
})
})
Formally
declare function mutableObject<T>(
obj: T
): T
obj
- any object to track mutability for, includingobject
andarray
- returns - a proxy that tracks mutations to the given object
mutableObject tracks object immutability by marking objects who have been mutated with two revision marks
const REVISION = Symbol('revision');
const CHILDRENREVISION = Symbol('children-revision')
When an object is updated, it's REVISION
is updated to a new larger value.
When a nested object is updated, it's parents CHILDRENREVISION
is updated to a new larger value.
For instance, for an array, if the array is pushed a new item, it's REVISION
will increase. If a nested
element of the array is updated, it's REVISION
increase, while the array's CHILDRENREVISION
increases.
The markings can be accessed using the symbols
items[REVISION]
items[CHILDRENREVISION]
bindRepeater
Binds a repeater data
property, creates a per item reactive for isolated hooks scope and binds the
onItemReady
and onItemRemoved
.
Quick example - using immutable state
import {bind, createState, bindRepeater} from 'velo-hooks';
$w.onReady(() => {
bind($w, refs => {
let [items, setItems] = createState([one, two])
bindRepeater(refs.repeater, items, (refs, item) => {
refs.title.text = () => item().title;
refs.input.onChange((event) => {
let newItems = [...items()].map(_ => (_._id === item()._id)?({...item(), title: event.target.value}):_);
setItems(newItems);
})
})
})
})
The same example using mutable state
import {bind, createState, bindRepeater} from 'velo-hooks';
$w.onReady(() => {
bind($w, refs => {
let [items, setItems] = createState(mutableObject([one, two]));
bindRepeater(refs.repeater, items, (refs, item) => {
refs.title.text = () => item().title;
refs.input.onChange((event) => item().tite = event.target.value)
})
})
})
Formally, bindRepeater
is
declare function bindRepeater<Item extends HasId, Comps>(
repeater: RefComponent<RepeaterType<Item, Comps>>,
data: Getter<Array<Item>>,
fn: (
refs: Refs<Comps>,
item: Getter<Item>,
$item: $W<Comps>) => void):
() => Reactive[]
At which
repeater
- is the reference to the repeater componentdata
- is the getter of the state holding the item to show in the repeater. Can be immutable or mutable objectfn
- the state constructor for each item state managementrefs
- references to the$item
elements on the repeater itemitem
- getter for the repeater item object$item
- the underlying raw$item
.
- returns - a getter for
Reactive[]
of all the current items on the repeater, Reactive - see below. Reactive is used for fine-grained computation control - in most cases the usage of Reactive directly is not needed
bindShowHide
bindShowHide
binds an element hidden
property, the show
and hide
functions to a boolean state with animation support.
When the state changes the element visibility will change as well, with the selected animations
bind($w, refs => {
let [state, setState] = createState(12);
bindShowHide(refs.text, () => state() % 3 === 0, {
showAnimation: {effectName: "fade", effectOptions: {duration: 2000, delay: 1000}},
hideAnimation: {effectName: "spin", effectOptions: {duration: 1000, delay: 200, direction: 'ccw'}}
})
refs.up.onClick(() => setState(_ => _ + 1));
refs.down.onClick(() => setState(_ => _ - 1));
})
Formally it is defined as
interface ShowHideOptions {
showAnimation?: {effectName: string, effectOptions?: ArcEffectOptions | BounceEffectOptions | FadeEffectOptions | FlipEffectOptions | FloatEffectOptions | FlyEffectOptions | FoldEffectOptions | GlideEffectOptions | PuffEffectOptions | RollEffectOptions | SlideEffectOptions | SpinEffectOptions | TurnEffectOptions | ZoomEffectOptions}
hideAnimation?: {effectName: string, effectOptions?: ArcEffectOptions | BounceEffectOptions | FadeEffectOptions | FlipEffectOptions | FloatEffectOptions | FlyEffectOptions | FoldEffectOptions | GlideEffectOptions | PuffEffectOptions | RollEffectOptions | SlideEffectOptions | SpinEffectOptions | TurnEffectOptions | ZoomEffectOptions}
}
declare function bindShowHide(
el: RefComponent<$w.HiddenCollapsedMixin>,
bind: Getter<boolean>,
options?: ShowHideOptions
)
bindCollapseExpand
bindCollapseExpand
binds an element collapsed
property, the expand
and collapse
functions to a boolean state.
When the state changes the element collapsed/expand will change as well.
bind($w, refs => {
let [state, setState] = createState(12);
bindCollapseExpand(refs.text, () => state() % 3 === 0)
refs.up.onClick(() => setState(_ => _ + 1));
refs.down.onClick(() => setState(_ => _ - 1));
})
Formally it is defined as
declare function bindCollapseExpand(
el: RefComponent<$w.HiddenCollapsedMixin>,
bind: Getter<boolean>
)
bindEnabled
bindEnabled
binds an element disabled
property, the enable
and disable
functions to a boolean state.
When the state changes the element enablement will change as well.
bind($w, refs => {
let [state, setState] = createState(12);
bindEnabled(refs.text, () => state() % 3 === 0)
refs.up.onClick(() => setState(_ => _ + 1));
refs.down.onClick(() => setState(_ => _ - 1));
})
Formally it is defined as
declare function bindEnabled(
el: RefComponent<$w.DisabledMixin>,
bind: Getter<boolean>
)
bindStorage
binds a state to a one of the Wix storage engines - local
, memory
or session
.
An example of a persistent counter -
import {local} from 'wix-storage';
bind($w, refs => {
let [state, setState] = createState(12);
refs.text.text = () => `${state()}`;
refs.up.onClick(() => setState(_ => _ + 1));
refs.down.onClick(() => setState(_ => _ - 1));
bindStorage(local, 'data', state, setState)
})
formally
declare function bindStorage<T>(
storage: wixStorage.Storage,
key: string,
state: Getter<T>,
setState: Setter<T>,
isMutable: boolean = false
)
storage
- the storage engine to use, imported fromwix-storage
APIkey
- the key to store the data understate
- the state getter to track and persist into the storage enginesetState
- the state setter to update on first load if data exists on the storage engineisMutable
- should the read data be amutableObject
?
Reactive
bind
returns an instance of Reactive
Jay Reactive which exposes the lower level APIs and gives more control over
reactions batching and flushing.
import {bind, createState} from 'velo-hooks';
$w.onReady(() => {
let reactive = bind($w, refs => {
let [state1, setState1] = createState(1);
let [state2, setState2] = createState(1);
let [state3, setState3] = createState(1);
let double = createMemo(() => _ * 2);
let plus10 = createMemo(() => _ + 10);
let sum = createMemo(() => state1() + state2() + state3());
refs.button1.onClick(() => {
setState1(10);
setState2(10);
setState3(10);
}) // computation of double, plus10 and sum reactions done in an async batch
refs.button1.onClick(() => {
reactive.batchReactions(() => {
setState1(10);
setState2(10);
}) // computation of double, plus10 and sum reactions done on exit from batchReactions
setState3(10);
}) // computation of sum reaction done in an async batch
refs.button1.onClick(() => {
setState1(10);
setState2(10);
reactive.flush() // computation of double, plus10 and sum reactions done sync on flush
setState3(10);
}) // computation of sum reaction done in an async batch
refs.button1.onClick(async () => {
setState1(10);
setState2(10);
await reactive.toBeClean() // computation of double, plus10 and sum reactions done async on flush
setState3(10);
}) // computation of sum reaction done in an async batch
})
})