velo-hooks v1.0.1
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
textandsetTextare state getter and setters, which ensure on the state update, any user of the state is also updated.refs.text1.textis a setter property, which accepts a function. Anytime any dependency of the function changes, thetext1.textvalue will be updated. In the above example, when thetext()state changes, therefs.text1.textwill be updated.
Some important differences from React
- Unlike React,
textis a function and reading the state value is a function calltext()refs.text1.textis 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
createStateis used to create the counter statecreateMemoare used to create derived (or computed state). note that unlike React useEffect, we do not need to specify the dependenciescreateEffectis used to print to the console any time thetensderives state changes.onClickevents are bound to functions who update thecounterstatebindShowHideis used to bind thehiddenproperty,showandhidefunctions to a boolean state and to animations. Alternatively, we could have usedcreateEffectfor the same result, if a bit more verbose code.bindbindCollapseExpandis used to bind thecollapsedproperty,expandandcollapsefunctions to a boolean state.bindEnabledis used to bind theenabledproperty,enableanddisablefunctions to a boolean state.bindRepeateris used to bind a repeaterdataproperty,onItemReadyandonItemRemovedto 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$wto build state management onfn- state management constructorrefs- the the equivalent of$wfor 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- theeffectfunction can return aEffectCleanupfunction 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
): Tobj- any object to track mutability for, includingobjectandarray- 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$itemelements 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-storageAPIkey- 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
})
})