Voby
A high-performance framework with fine-grained observable-based reactivity for building rich applications.
Features
This works similarly to Solid, but without the need for the Babel transform and with a different API.
- No VDOM: there's no VDOM overhead, the framework deals with raw DOM nodes directly.
- No stale closures: components are executed once, so you don't need to worry about stale closures.
- No dependencies arrays: the framework is able to detect what depends on what else automatically, no need to specify dependencies manually.
- No diffing: updates are fine grained, there's no reconciliation overhead, no props diffing, whenever an attribute/property/class/handler/etc. should be updated it's updated directly and immediately.
- No Babel: there's no need to use Babel with this framework, it works with plain old JS (plus JSX if you are into that). As a consequence we have 0 transform function bugs, because we don't have a transform function.
- No server support: for the time being this framework is focused on local-first rich applications, ~no server-related features are implemented: no hydration, no server components, no SSR, no suspense etc.
- Observable-based: observables are at the core of our reactivity system. The way it works is very different from a React-like system, it may be more challenging to learn, but the effort is well worth it.
- Work in progress: this is at best alpha software, I'm working on it because I need something with great performance for Notable, I'm allergic to third-party dependencies, I'd like something with an API that resonates with me, and I wanted to deeply understand how the more solid Solid, which you should probably use instead, works.
Demo
You can find some CodeSandbox demos below, more demos are contained inside the repository.
- Playground: https://codesandbox.io/s/voby-playground-7w2pxg
- Counter: https://codesandbox.io/s/voby-demo-counter-23fv5
- Benchmark: https://codesandbox.io/s/voby-demo-benchmark-x0nr40
//TODO: Add more demos
APIs
//TODO: List types too
Usage
The following is going to be a very shallow documentation of the API. As I mentioned this isn't production-grade software, it may become that in the future though, are you interested?
Observable
First of all this framework is just a UI layer built on top of the Observable library oby, knowing how that works is necessary to understand how this works.
Everything that oby
provides is used internally and it's simply re-exported by voby
.
Generally whenever you can use a raw value you can also use an observable, for example if you pass a plain string as the value of an attribute it will never change, it you use an observable instead it will change whenever the value inside the observable changes, automatically.
import {$, $$} from 'voby';
$ // => Same as require ( 'oby' )
$$ // => Same as require ( 'oby' ).get
Methods
The following top-level methods are provided.
batch
This function holds onto updates within its scope and flushes them out at once once it exits.
import {batch} from 'voby';
batch // => Same as require ( 'oby' ).batch
createElement
This is the function that will make DOM nodes and call/instantiate components, it will be called for you automatically via JSX.
import {createElement} from 'voby';
const element = createElement ( 'div', { class: 'foo' }, 'child' ); // => () => HTMLDivElement
isObservable
This function tells you if a variable is an observable or not.
import {$, isObservable} from 'voby';
isObservable ( 123 ); // => false
isObservable ( $(123) ); // => false
render
This function mounts a component inside a provided DOM element and returns a disposer function.
import {render} from 'voby';
const App = () => <p>Hello, World!</p>;
const dispose = render ( <App />, document.body );
dispose (); // Unmounted and all reactivity inside it stopped
renderToString
This works just like render
, but it returns an HTML representation of the rendered component.
This is currently implemented in a way that works only inside a browser environement, so you'll need to use JSDOM or similar for this to work server-side.
import {renderToString} from 'voby';
const App = () => <p>Hello, World!</p>;
const html = await renderToString ( <App /> );
sample
This function executes the provided function without creating dependencies on observables retrieved inside it.
import {sample} from 'voby';
sample // => Same as require ( 'oby' ).sample
styled
This is an object providing styled-components-like API, it's based on the awesome goober and it largely just re-exports some of its methods.
import {styled} from 'voby';
const GlobalStyle = styled.global`
:root {
--color-bg: tomato;
--color-fg: white;
}
`;
const rotate = styled.keyframes`
from, to {
width: 50px;
}
50% {
width: 150px;
}
`;
const disabled = styled.class ( 'disabled' );
const P = styled.p`
background-color: var(--color-bg);
color: var(--color-fg);
animation: ${rotate} 1s ease-in-out infinite;
&${disabled} {
opacity: .5;
pointer-events: none;
}
`;
const App = () => {
return (
<>
<GlobalStyle />
<P class={{ [disabled.raw]: true }}>content</P>
</>
);
};
svg
This function enables you to embed an SVG relatively cleanly in your page.
This function internally uses innerHTML
and must therefor only be used with trusted input.
const App = () => { const hex = `#${Math.floor ( Math.random () * 0xFFFFFF ).toString ( 16 ).padStart ( 6, '0' )}`; return ( <div class="something"> {svg` " stroke-width="3" fill="white">`} </div> ); };
template
This function enables constructing elements with Solid-level performance without using the Babel transform, but also without the convenience of that.
It basically works like sinuous's template function, but with a slightly cleaner API, since you don't have to access your props any differently inside the template here.
import {template} from 'voby'; const Row = template ( ({ id, cls, label, onSelect, onRemove }) => { return ( <tr class={cls}> <td class="col-md-1">{id}</td> <td class="col-md-4"> <a onClick={onSelect}>{label}</a> </td> <td class="col-md-1"> <a onClick={onRemove}> <span class="glyphicon glyphicon-remove" ariaHidden={true}></span> </a> </td> <td class="col-md-6"></td> </tr> ); }); const Table = () => { const rows = [ /* props for all your rows here */ ]; return rows.map ( row => <Row {...row}> ); };
Components
The following components are provided.
Crucially some components are provided for control flow, since regular control flow primitives wouldn't be reactive.
Component
This is the base class for your class-based components, if you are into that.
The nice thing about class-based components is that you get ref forwarding for free, the eventual ref passed to a class component will automatically receive the class instance corresponding to the component.
import {Component} from 'voby';
class App extends Component<{ value: number }> {
render ( ({ value }) ): JSX.Element {
return <p>Value: {value}</p>;
}
}
ErrorBoundary
The error boundary catches errors happening while synchronously mounting its children, and renders a fallback compontent when that happens.
import {ErrorBoundary} from 'voby';
const Fallback = ({ reset, error }: { reset: () => void, error: Error }) => {
return (
<>
<p>Error: {error.message}</p>
<button onClick={error}>Recover</button>
</>
);
};
const SomeComponentThatThrows = () => {
throw 'whatever';
};
const App = () => {
return (
<ErrorBoundary fallback={Fallback}>
<SomeComponentThatThrows />
</ErrorBoundary>
)
};
For
This component is the reactive alternative to natively mapping over an array.
import {For} from 'voby';
const App = () => {
const numbers = [1, 2, 3, 4, 5];
return (
<For values={numbers}>
{( value ) => {
return <p>Value: {value}</p>
}}
</For>
)
};
Fragment
This is just the internal component used for rendering fragments: <>
, you probably would never use this directly even if you are not using JSX, since you can return plain arrays from your components anyway.
import {Fragment} from 'voby';
const App = () => {
return (
<Fragment>
<p>child 1</p>
<p>child 2</p>
</Fragment>
)
}
If
This component is the reactive alternative to the native if
.
import {If} from 'voby';
const App = () => {
const visible = $(false);
const toggle = () => visible ( !visible () );
return (
<>
<button onClick={toggle}>Toggle</button>
<If when={visible}>
<p>Hello!</p>
</If>
</>
)
};
Portal
This component mounts its children inside a provided DOM element, or inside document.body
otherwise.
Events will propagate natively according to the resulting DOM hierarchy, not the components hierarchy.
import Portal from 'voby';
const Modal = () => {
// Some modal component maybe...
};
const App = () => {
return (
<Portal mount={document.body}>
<Modal />
</Portal>
);
};
Switch
This component is the reactive alternative to the native switch
.
import {Switch} from 'voby';
const App = () => {
const value = $(0);
const increment = () => value ( value () + 1 );
const decrement = () => value ( value () - 1 );
return (
<>
<Switch when={value}>
<Switch.Case when={0}>
<p>0, the boundary between positives and negatives! (?)</p>
</Switch.Case>
<Switch.Case when={1}>
<p>1, the multiplicative identity!</p>
</Switch.Case>
<Switch.Default>
<p>{value}, I don't have anything interesting to say about that :(</p>
</Switch.Default>
</Switch>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
</>
)
};
Ternary
This component is the reactive alternative to the native ternary operator.
The first child will be rendered when the condition is true
, otherwise the second child will be rendered.
import {Ternary} from 'voby';
const App = () => {
const visible = $(false);
const toggle = () => visible ( !visible () );
return (
<>
<button onClick={toggle}>Toggle</button>
<Ternary when={visible}>
<p>Visible :)</p>
<p>Invisible :(</p>
</Ternary>
</>
)
};
Hooks
The following hooks are provided.
Many of these are just functions that oby
provides, re-exported as use*
functions.
Hooks are just regular functions, if their name starts with use
then we call them hooks.
useAbortController
This hook is just an alternative to new AbortController ()
that automatically aborts itself when the parent computation is disposed.
import {useAbortController} from 'voby';
const controller = useAbortController ();
useAnimationFrame
This hook is just an alternative to requestAnimationFrame
that automatically clears itself when the parent computation is disposed.
import {useAnimationFrme} from 'voby';
useAnimationFrme ( () => console.log ( 'called' ) );
useAnimationLoop
This hook is just a version of useAnimationFrame
that loops.
import {useAnimationLoop} from 'voby';
useAnimationLoop ( () => console.log ( 'called' ) );
useCleanup
This hook registers a function to be called when the parent computation is disposed.
import {useCleanup} from 'voby';
useCleanup // => Same as require ( 'oby' ).cleanup
useComputed
This hook is the crucial other ingredients that we need, other than observables themselves, to have a powerful reactive system that can track dependencies and re-execute computations when needed.
This hook registers a function to be called when any of its dependencies change, and the return of that function is wrapped in a read-only observable and returned.
import {useComputed} from 'voby';
useComputed // => Same as require ( 'oby' ).computed
useDisposed
This hook returns a boolean read-only observable that is set to true
when the parent computation gets disposed of.
import {useDisposed} from 'voby';
useDisposed // => Same as require ( 'oby' ).disposed
useEffect
This hook registers a function to be called when any of its dependencies change. If a function is returned it's automatically registered as a cleanup function.
import {useEffect} from 'voby';
useEffect // => Same as require ( 'oby' ).effect
useError
This hook registers a function to be called when the parent computation throws.
import {useError} from 'voby';
useError // => Same as require ( 'oby' ).error
useFetch
This hook wraps the output of a fetch request in an observable, so that you can be notified when it resolves or rejects. The request is also aborted automatically when the parent computation gets disposed of.
import {useFetch} from 'voby';
const App = () => {
const state = useFetch ( 'https://my.api' );
return state.on ( state => {
if ( state.loading ) return <p>loading...</p>;
if ( state.error ) return <p>{state.error.message}</p>;
return <p>Status: {state.value.status}</p>
});
};
useFrom
This hook is useful for encapsulating values that may change over time into an observable.
import {useFrom} from 'voby';
useFrom // => Same as require ( 'oby' ).from
useIdleCallback
This hook is just an alternative to requestIdleCallback
that automatically clears itself when the parent computation is disposed.
import {useIdleCallback} from 'voby';
useIdleCallback ( () => console.log ( 'called' ) );
useIdleLoop
This hook is just a version of useIdleCallback
that loops.
import {useIdleLoop} from 'voby';
useIdleLoop ( () => console.log ( 'called' ) );
useInterval
This hook is just an alternative to setInterval
that automatically clears itself when the parent computation is disposed.
import {useInterval} from 'voby';
useInterval ( () => console.log ( 'called' ), 1000 );
usePromise
This hook wraps a promise in an observable, so that you can be notified when it resolves or rejects.
import {usePromise} from 'voby';
const App = () => {
const request = fetch ( 'https://my.api' ).then ( res => res.json ( 0 ) );
const promise = usePromise ( request );
return resolved.on ( state => {
if ( state.loading ) return <p>loading...</p>;
if ( state.error ) return <p>{state.error.message}</p>;
return <p>{JSON.stringify ( state.value )}</p>
});
};
useResolved
This hook receives a value potentially wrapped in functions and/or observables, and unwraps it recursively.
If no callback is used then it returns the unwrapped value, otherwise it returns whatever the callback returns.
This is useful for handling reactive and non reactive values the same way. Usually if the value is a function, or always for convenience, you'd want to wrap the useResolved
call in a useComputed
, to maintain reactivity.
import {$, useResolved} from 'voby';
useResolved ( 123 ); // => 123
useResolved ( $($(123)) ); // => 123
useResolved ( () => () => 123 ); // => 123
useResolved ( 123, value => 321 ); // => 321
useResolved ( $($(123)), value => 321 ); // => 321
useResolved ( () => () => 123, value => 321 ); // => 321
useRoot
This hook creates a new computation root, detached from any parent computation.
import {useRoot} from 'voby';
useRoot // => Same as require ( 'oby' ).root
useTimeout
This hook is just an alternative to setTimeout
that automatically clears itself when the parent computation is disposed.
import {useTimeout} from 'voby';
useTimeout ( () => console.log ( 'called' ), 1000 );
Extras
The following extra functionalities are provided via submodules.
vite
A basic Vite plugin is provided.
// vite.js
const voby = require ( 'voby/vite-plugin' );
module.exports = defineConfig ({
plugins: [
voby ()
]
});
Thanks
- S: for paving the way to this awesome reactive way of writing software.
- sinuous/observable: for making me fall in love with Observables.
- Solid: for being a great sort of reference implementation, popularizing Observable-based reactivity, and having built a great community.
License
MIT © Fabio Spampinato