Gooey

A focused and flexible frontend web framework.

Works like a spreadsheet for UI: build cells of data or UI that may read other cells and Gooey makes sure everything is always up to date.

Inspired by build systems, spreadsheets, and Backbone.

Find out more about how it works and why it’s interesting.


Familiar

Build reusable Component functions (or classes, if you like) with JSX.


Straightforward

Build your application state with simple building blocks:

Place your calculations directly to the DOM:


Flexible

Define custom elements to add dynamic functionality onto plain old HTML documents:

tsx
import Gooey, { defineCustomElement, model, calc, dynGet } from '@srhazi/gooey';

document.body.innerHTML = `<p><my-greeting>World</my-greeting></p>`;

defineCustomElement({
  tagName: 'my-greeting',
  Component: ({ children }) => {
    const state = model({ clicks: 0 });
    return (
      <button on:click={() => (state.clicks += 1)}>
        Hello, {children}
        {calc(() => '!'.repeat(state.clicks))}
      </button>
    );
  },
});

Unique

There are a few features that make Gooey stand apart from other frameworks:

Check out how these work in the guide.


Precise

Gooey is designed to make changes to UI quickly and easily.

While it’s not the fastest tool out there, it’s decently fast!

Compared to React (which relies on virtual DOM diffing), Gooey is surgically precise: it knows what data relates to which pieces of UI and apply updates without needing to do all the work required by a virtual DOM diffing algorithm: render an entirely new tree and diff it from the prior one.

This has two benefits:

This also means that the common act of updating a small amount of data in a large application is very fast.

Here’s a crude benchmark to demonstrate rendering / updating 10k items:

tsx
import Gooey, {
  VERSION,
  Component,
  Model,
  mount,
  model,
  collection,
  calc,
  flush,
} from '@srhazi/gooey';

const measurements = collection<string>([]);

const measure = (name: string, fn: () => void) => {
  return () => {
    const start = performance.now();
    fn();
    flush();
    const time = performance.now() - start;
    console.log(`gooey ${name} duration`, time);
    measurements.push(`gooey ${name} duration: ${time}ms`);
  };
};

const Benchmark: Component = () => {
  const items = collection<Model<{ val: number }>>([]);
  let itemId = 0;

  const addItems = measure('1:add', () => {
    for (let i = 0; i < 10000; ++i) {
      items.push(model({ val: itemId++ }));
    }
  });

  const updateAllItems = measure('2:update-all', () => {
    items.forEach((item) => (item.val *= 2));
  });

  const updateSomeItems = measure('3:update-some', () => {
    if (items.length === 0) return;
    for (let i = 0; i < 10; ++i) {
      items[Math.floor(Math.random() * items.length)].val *= 2;
    }
  });

  const clearItems = measure('4:clear', () => {
    items.splice(0, items.length);
  });

  return (
    <div>
      <p>Gooey version {VERSION}</p>
      <p>
        <button data-gooey-add on:click={addItems}>
          Add items
        </button>
        <button data-gooey-update-all on:click={updateAllItems}>
          Update all items
        </button>
        <button data-gooey-update-some on:click={updateSomeItems}>
          Update 10 random items
        </button>
        <button data-gooey-clear on:click={clearItems}>
          Clear items
        </button>
      </p>
      <ul class="bx by" style="height: 100px; overflow: auto; contain: strict">
        {items.mapView((item) => (
          <li>{calc(() => item.val)}</li>
        ))}
      </ul>
      <ul>
        {measurements.mapView((measurement) => (
          <li>{measurement}</li>
        ))}
      </ul>
    </div>
  );
};

mount(document.body, <Benchmark />);
tsx
import React, { useRef, useMemo, useCallback, useState } from 'react';
import * as ReactDOM from 'react-dom';
import * as ReactDOMClient from 'react-dom/client';

const Benchmark = () => {
  const [items, setItems] = useState<{ key: number; val: number }[]>([]);
  const [measurements, setMeasurements] = useState<string[]>([]);
  let itemId = useRef(0);

  const measure = useCallback(
    (name: string, fn: () => void) => {
      return () => {
        const start = performance.now();
        ReactDOM.flushSync(() => {
          fn();
        });
        const time = performance.now() - start;
        console.log(`react ${name} duration`, time);
        setMeasurements((measurements) => [
          ...measurements,
          `react ${name} duration: ${time}ms`,
        ]);
      };
    },
    [setMeasurements]
  );

  const addItems = useMemo(
    () =>
      measure('1:add', () => {
        const newItems = items.slice();
        for (let i = 0; i < 10000; ++i) {
          newItems.push({
            key: itemId.current,
            val: itemId.current++,
          });
        }
        setItems(newItems);
      }),
    [items, setItems, measure]
  );

  const updateAllItems = useMemo(
    () =>
      measure('2:update-all', () => {
        setItems(items.map((item) => ({ key: item.key, val: item.val * 2 })));
      }),
    [items, setItems]
  );

  const updateSomeItems = useMemo(
    () =>
      measure('3:update-some', () => {
        const newItems = items.slice();
        for (let i = 0; i < 10; ++i) {
          newItems[Math.floor(Math.random() * newItems.length)].val *= 2;
        }
        setItems(newItems);
      }),
    [items, setItems, measure]
  );

  const clearItems = useMemo(
    () =>
      measure('4:clear', () => {
        setItems([]);
      }),
    [setItems, measure]
  );

  return (
    <div>
      <p>
        React version {React.version}; ReactDOM version {ReactDOM.version}
      </p>
      <p>
        <button data-react-add onClick={addItems}>
          Add items
        </button>
        <button data-react-update-all onClick={updateAllItems}>
          Update all items
        </button>
        <button data-react-update-some onClick={updateSomeItems}>
          Update 10 random items
        </button>
        <button data-react-clear onClick={clearItems}>
          Clear items
        </button>
      </p>
      <ul
        className="bx by"
        style={{ height: '100px', overflow: 'auto', contain: 'strict' }}
      >
        {items.map((item) => (
          <li key={item.key}>{item.val}</li>
        ))}
      </ul>
      <ul>
        {measurements.map((measurement, i) => (
          <li key={i}>{measurement}</li>
        ))}
      </ul>
    </div>
  );
};

const root = ReactDOMClient.createRoot(
  document.body
);
root.render(<Benchmark />);

As the numbers show, React is pretty good at bulk update, but Gooey really excels at applying precision updates to an existing application.

ReactGooeyComparison factor
Add 10k items205.60ms
(48.64 items/ms)
147.80ms
(67.66 items/ms)
Gooey is 1.39x faster
Update 10k items13.80ms
(724.64 items/ms)
63.20ms
(158.23 items/ms)
React is 4.58x faster
Update 10 random items4.40ms
(2.27 items/ms)
0.60ms
(16.67 items/ms)
Gooey is 7.34x faster
Clear 10k items29.20ms
(342.46 items/ms)
57.80ms
(173.01 items/ms)
React is 1.98x faster
Gooey v0.20.0 vs React 19.0.0; Median run over 100 runs on Chrome 134.0.6998.89
Benchmark performed on a MacBook Pro (16-inch, 2019)

To be honest, React is fast at updates, but slow at initial rendering. It really suffers when applications get large and small state changes cause big re-renders.

On the other hand, Gooey is designed to be very fast at initial rendering and in the presence of small state changes, which are the most common places where speed really matters.


Bleeding edge

Should you use this? Maybe!

While it has a robust set of tests, it’s still bleeding edge, so unless you want to help contribute, it’s probably best to think of this as a set of possibilities.

If you want to help, reach out!

The author can be reached via email, check https://abstract.properties.


News

- v0.20.0

Version 0.20.0 (npm, git, github) has been released.

Changes from 0.19.1:

- v0.19.0

Version 0.19.1 (npm, git, github) has been released.

Changes from 0.19.0:

- v0.19.0

Version 0.19.0 (npm, git, github) has been released.

Changes from 0.18.3

- v0.18.3

Version 0.18.3 (npm, git, github) has been released.

Changes from 0.18.0

- v0.18.0

Version 0.18.0 (npm, git, github) has been released.

Changes from 0.17.3

- v0.17.3

Version 0.17.3 (npm, git, github) has been released. It is a minor bugfix release.

Changes from v0.17.2

- v0.17.2

Version 0.17.2 (npm, git, github) has been released. It has some major breaking changes.

Note: Version 0.17.0 and 0.17.1 were not published to NPM and should be avoided.

Changes from v0.16.0

- v0.16.0

Version 0.16.0 (npm, git, github) has been released. It has some major breaking changes.

Changes from v0.15.0