A high-performance framework with fine-grained observable-based reactivity for building rich applications

Last update: Jun 17, 2022

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.

//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.

Read upstream documentation.

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.

Read upstream documentation.

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.

Read upstream documentation.

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.

{svg` `}
); };">
const App = () => {
  const hex = `#${Math.floor ( Math.random () * 0xFFFFFF ).toString ( 16 ).padStart ( 6, '0' )}`;
  return (
    <div class="something">
      {svg`
        
  
  
   ${color}
  
  " 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.

{id} {label} ); }); const Table = () => { const rows = [ /* props for all your rows here */ ]; return rows.map ( row => ); };">
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.

Read upstream documentation.

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.

Read upstream documentation.

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.

Read upstream documentation.

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.

Read upstream documentation.

import {useEffect} from 'voby';

useEffect // => Same as require ( 'oby' ).effect

useError

This hook registers a function to be called when the parent computation throws.

Read upstream documentation.

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.

Read upstream documentation.

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.

Read upstream documentation.

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

GitHub

https://github.com/fabiospampinato/voby
Comments
  • 1. Sierpinski triangle demo is missing per-node expensive computation

    Hello, just a thought about the rendering performance demo:

    https://codesandbox.io/s/voby-demo-triangle-l837v0?file=/src/index.tsx:222-385

    const useSeconds = (): Observable<number> => {
    
      const seconds = $(0);
    
      useInterval ( () => seconds ( ( seconds () % 9 ) + 1 ), 1000 );
    
      return seconds;
    };
    

    I think that useSeconds() should be "wrapped" into an intermediary observable that triggers at the same time as the seconds signal which is scheduled to reliably fire at every seconds tick. The goal of this "wrapper" observable is to introduce a computationally-taxing operation, per node of the triangular structure (just as in the original React demo).

    This is specifically what makes this stress-test relevant, as remarked by Ryan Carniato:

    https://github.com/ryansolid/solid-sierpinski-triangle-demo#solid-sierpinski-triangle-demo

    See:

    https://github.com/ryansolid/solid-sierpinski-triangle-demo/blob/889db98d7697bd39d8fe3ca0e0134fb2788b4447/src/main.jsx#L46-L51

        var e = performance.now() + 0.8;
        // Artificially long execution time.
        while (performance.now() < e) {}
    };
    

    (sorry, I don't know enough about Voby to contribute a PR)

    I used the same technique in my stress-test for fine-grain reactivity / observable lib experiment (I use uHTML for DOM rendering). Otherwise, rendering at 200 FPS is way too easy :)

    Reviewed by danielweck at 2022-05-14 13:53
  • 2. Formatting Issue

    I have seen Voby in Reddit (https://www.reddit.com/r/javascript/comments/ukhv0w/voby_a_new_frontend_framework_with_high/) & I am trying to see the code to create a binding library to Urql GraphQL Client (https://formidable.com/open-source/urql/), but the code is not readable enough because of the formatting & there are no comments that vscode can use to give documentation.

    Could You Format the code & add comment documentations?

    Reviewed by Abiti-936 at 2022-05-09 09:58
  • 3. ObservableReadonly creation from Observable

    How is it supposed to create ObservableReadonly<T> from Observable<T>? Is there anything similar to $.readonly(someObservable) utility from oby lib? I saw ObservableAbstract<T, Ti> type has readonly prop but its return type is ObservableReadonlyAbstract<T, TI>, not ObservableReadonly<T>.

    Reviewed by andreichuk at 2022-04-28 16:26
  • 4. Prevent closure variables caching issue

    https://github.com/vobyjs/voby/blob/d685d6079bb0172339d26535a551842545c70ab3/src/hooks/use_timeout.ts#L15

    Maybe it will be better to use there (and in other places) "safe closure" approach? It is described here: https://dmitripavlutin.com/react-hooks-stale-closures/

    Reviewed by Mati365 at 2022-05-08 07:53
  • 5. Simplified clock implementation

    • there's no need to rotate the clock hands for 90 degress, we can just draw those using negative y2, since y1 is 0
    • we can multiply by 360 inside the rotate function, since it's always done, and move that from time equations
    • move the division with 1000 to observable itself
    • use % 1 on miliseconds to get only the miisecond part
    • all this results that values for all clock hands are looping between 0 and 360, which looks better also
    Reviewed by high1 at 2022-05-07 22:38
  • 6. JSX ref

    Is JSX's 'ref' attribute supported? In the following example it does nothing:

    
    // voby 0.16
    
    const App = () => {
        let eInput;
    
        function onClick() {
            console.log("eInput: " + eInput); // outputs 'eInput: undefined'
        }
    
        return (
            <input type="text" ref={eInput} onClick={onClick}/>
        )
    };
    
    render(<App />, document.body);
    
    Reviewed by andreichuk at 2022-04-28 10:11
A frontend Framework for building B2B applications running in the browser on top of REST/GraphQL APIs, using ES6, React and Material Design
A frontend Framework for building B2B applications running in the browser on top of REST/GraphQL APIs, using ES6, React and Material Design

react-admin A frontend Framework for building data-driven applications running in the browser on top of REST/GraphQL APIs, using ES6, React and Materi

Jun 26, 2022
Hydrogen is a React-based framework for building dynamic, Shopify-powered custom storefronts.
Hydrogen is a React-based framework for building dynamic, Shopify-powered custom storefronts.

Hydrogen is a React-based framework for building dynamic, Shopify-powered custom storefronts.

Jun 19, 2022
🛡️ ⚛️ A simple, scalable, and powerful architecture for building production ready React applications.

??️ ⚛️ A simple, scalable, and powerful architecture for building production ready React applications.

Jun 21, 2022
🧰 A simple, maximally extensible, dependency minimized framework for building modern Ethereum dApps

?? A simple, maximally extensible, dependency minimized framework for building modern Ethereum dApps

Jun 23, 2022
web3-react🧰 A simple, maximally extensible, dependency minimized framework for building modern Ethereum dApps

?? A simple, maximally extensible, dependency minimized framework for building modern Ethereum dApps

Jun 24, 2022
A web framework for building virtual reality experiences (VR Web)
A web framework for building virtual reality experiences (VR Web)

Virtual Reality Made Simple: A-Frame handles the 3D and WebVR boilerplate required to get running across platforms including mobile, desktop, Vive, and Rift

Mar 18, 2022
A simple, maximally extensible, dependency minimized framework for building modern Ethereum dApps

A simple, maximally extensible, dependency minimized framework for building modern Ethereum dApps

Nov 1, 2021
A data-driven, functional, and reactive framework for building Modern Web Apps in JavaScript.
A data-driven, functional, and reactive framework for building Modern Web Apps in JavaScript.

A data-driven, functional, and reactive framework for building Modern Web Apps in JavaScript. It leverages React, inspired by re-frame.

Mar 2, 2022
🗺 Universal router for web applications.

Expressive router for nodejs and the browser. Rill brings cascading middleware to the browser and enables a familiar routing solution for web applicat

May 30, 2022
Create beautfiful custom applications using 100ms' React SDK.
Create beautfiful custom applications using 100ms' React SDK.

Create beautfiful custom applications using 100ms' React SDK.

Jun 17, 2022
A declarative, efficient, and flexible JavaScript library for building user interfaces.

React · React is a JavaScript library for building user interfaces. Declarative: React makes it painless to create interactive UIs. Design simple view

Jun 20, 2022
The React Framework

Next.js Getting Started Visit https://nextjs.org/learn to get started with Next.js. Documentation Visit https://nextjs.org/docs to view the full docum

Jun 19, 2022
The Full-stack Framework for React and other in Deno.

The Full-stack Framework for React and other in Deno.

Jun 17, 2022
⚡️The Fullstack React Framework — built on Next.js
⚡️The Fullstack React Framework — built on Next.js

⚡️The Fullstack React Framework — built on Next.js

Jun 16, 2022
A most advanced ssr framework support React/Vue2/Vue3 on Earth that implemented serverless-side render specification.
A most advanced ssr framework support React/Vue2/Vue3 on Earth that implemented serverless-side render specification.

A most advanced ssr framework support React/Vue2/Vue3 on Earth that implemented serverless-side render specification.

Jun 18, 2022
⚡️The Fullstack React Framework — built on Next.js — Inspired by Ruby on Rails
⚡️The Fullstack React Framework — built on Next.js — Inspired by Ruby on Rails

⚡️The Fullstack React Framework — built on Next.js — Inspired by Ruby on Rails

Oct 12, 2021
Web App using react framework

waldo.vision This is the website for waldo-anticheat :D Libraries used: React React-router React-router-hash-link React-helmet TODO: improve this list

Nov 12, 2021
Create a SPA project using Mithril JS framework and design layout by JSX syntax and use Typescript and Vite for build system.
Create a SPA project using Mithril JS framework and design layout by JSX syntax and use Typescript and Vite for build system.

Mithril TS(Type script) JSX Create a SPA project using Mithril JS framework and design layout by JSX syntax and use Typescript and Vite for build syst

Feb 22, 2022
Declarative data-fetching and caching framework for REST APIs with React
Declarative data-fetching and caching framework for REST APIs with React

Declarative data-fetching and caching framework for REST APIs with React

Jun 12, 2022