Live is a reimplementation of the React <Component>
tree and its hook system. It allows you to use popular reactive patterns to write code beyond UI widgets.
It's built to serve as the reactive core of Use.GPU, but there is nothing GPU- or graphics-specific about it.
Unlike React, Live does not produce an output DOM. Components can only render other components, or yield values back to parents.
Live is designed to play nice with real React, but shares no code with it.
Non-React extensions:
if
/else
with hooks safely. Return early.react-react
).Modules
@use-gpu/live
- Live effect run-time@use-gpu/react
- Live ↔︎ React portals@use-gpu/inspect
- Inspector/debugger for Live, built using ReactLive follows the classic React model for <Component>
.
Components: functions of immutable props
and mutable state
: no side-effects. Returns a declarative tree of child components to render. Parent components pass props and state to their children explicitly, only one-way.
Hooks: Components use hooks like useState
to hold state. Effect hooks bind events to callbacks that modify that state. Hooks are memoized by declaring dependencies.
Context: pass implicit props down to an entire sub-tree at once, to avoid tedious "prop drilling".
Callbacks: if a child needs to call a parent, it has to have a specific purpose-made callback prop.
All the basics work the same and the mental model for hooks translates 95%.
These examples assume you are familiar with React 17-style functional components.
// This allows Live <JSX> to be used, by pretending to be React.
// You can't mix React and Live <JSX> in the same file.
import React from '@use-gpu/live';
// Main render entry point
import { render } from '@use-gpu/live';
import { App } from './app';
// e.g. on DOM load
render(<App />);
Here, App
is a LiveComponent
(LC
). This is just like a React FunctionComponent
(FC
), with the same hooks API:
import React, { LC, useState } from '@use-gpu/live';
import { OtherComponent } from './other-component';
export const App: LC = () => {
const [state, setState] = useState<number>(0);
// ...
return <OtherComponent state={state} />
};
import React from '@use-gpu/live';
type Props = {
state: number,
};
export const OtherComponent: LC<Props> = (props: Props) => {
// ...
};
There is also a children
prop, as well as key
for items in arrays.
TL;DR
useCallback
, useContext
, useMemo
, useRef
, useState
work like in React
useOne
is shorthand for a useMemo
with only 0-1 dependency (not an array)
useEffect
and useLayoutEffect
don't exist, use useResource
useResource
is a sync useEffect
+ useMemo
const t = useResource((dispose) => {
// Creation
const thing = new Thing();
// Deferred cleanup function
dispose(() => thing.dispose());
// Returned value
return thing;
}, [...dependencies]);
Every built-in useHook
has a no-op useNoHook
. You can safely write e.g.:
if (condition) {
useMemo(...)
}
else {
useNoMemo();
}
No-hooks will discard prior state, unsubscribe from contexts, cleanup resources, and so on.
Additionally, you are allowed to return
early from a component, before calling all hooks. This will implicitly call a no-hook for them.
import { makeContext } from '@use-gpu/live';
type ContextValue = { foo: number };
const defaultValue = { foo: 1 };
const displayName = 'MyContext';
const MyContext = makeContext<ContextValue>(defaultValue, displayName);
If defaultValue
is undefined
, the context is required.
Its value has type T
, and will throw an exception if used while missing.
If defaultValue
is null
, the context is optional.
Its value has type T | null
and can be used without being provided.
import React, { Provide } from '@use-gpu/live';
<Provide context={MyContext} value={value}>...</Provide>
import { useContext } from '@use-gpu/live';
const value = useContext(MyContext);
This is the reverse of a context provider: it captures values from specific children across an entire sub-tree.
import { makeCapture } from '@use-gpu/live';
type ContextValue = { foo: number };
const displayName = 'MyContext';
const MyContext = makeCapture<ContextValue>(displayName);
import { useCapture } from '@use-gpu/live';
// In a child
useCapture(MyContext, value);
<Capture>
takes a capture context and two props:
children
to be renderedthen
continuation (values: T[]) => JSX.Element
that receives captured values in tree orderimport React, { Capture } from '@use-gpu/live';
<Capture context={MyContext} then={(values: T[]) => {
// ...
}>...</Capture>
A continuation (aka a then
prop) is similar to a classic render
prop, except that it is run after children are done. Another difference is that it acts as a fully fledged component: you can e.g. use hooks, and it appears as a separate Resume(Component)
node in the component tree.
Similar to a capture, a component can render a <Gather>
to gather values and continue after its children are rendered.
The main difference with Capture
is that Gather
is not context- or hook-based, and does not skip across the tree. The components of the tree are used directly as the nodes of a Map-Reduce, and there is only one gathering context active at any time.
<Gather>
does not require a separate component to be mounted, and it does not appear in the component tree except in special cases. Here, both <LinearRGB>
and <RenderToTexture>
are gathering:
Any child can yeet a value, or an array of values. Yeets are indicated with triangles in the image.
return <Yeet>{value}</Yeet>
The parent renders a <Gather>
, which takes two props:
children
to be renderedthen
continuation (value: T[]) => JSX.Element
that receives gathered values in tree-orderimport React, { Gather } from '@use-gpu/live';
export const Component: LC = () => {
const then = (values: any[]) => {
// ...
};
return (
<Gather then={then}>
<Component />
<Component />
</Gather>
);
};
Whenever the gathered values
change, the continuation is re-run.
To gather multiple sets of values at the same time, use <MultiGather>
. It will collect a Record<string, any[]>
, grouped by key.
For general reductions, use <MapReduce>
with a custom map
and reduce
function.
A common pattern is to make a component's render
prop optional. If absent, the component will then <Yeet>
the render argument(s) instead. This allows the render
prop to handle the simple cases, while also allowing the component to be part of a <Gather>
for more complex use.
Similar to how React reconciles HTML into a new DOM tree, Live can reconcile other Live components into a new subtree. This is done by quoting inside a <Reconcile>
. This works the same way as quoting and unquoting in Lisp.
Quotes are always targeted to a specific reconciler, created with makeReconciler
. This works similar to React contexts.
When you render:
<Reconcile to={MyReconciler}>
<First>
<Quote to={MyReconciler}>
<Second>
<Second>
<Unquote>
<First />
</Unquote>
</Second>
<Second>
<Unquote>
<First />
</Unquote>
</Second>
</Second>
</Quote>
</First>
</Reconcile>
This will produce the tree:
<Reconcile>
<First>
<First />
<First />
</First>
<Resume(Reconcile)>
<Second>
<Second />
<Second />
</Second>
</Reconcile>
Reconcilers can be nested and quoted, to produce a series of derived trees, all updating incrementally.
When a component in the tree changes type, Live will unmount it and its children, like React. Usually this is what you want.
But for e.g. site pages in a router, this is undesirable, because two pages will often render the same layout inside. They might want to retain state for shared controllers too, but only if they match.
This is a Morph. Just wrap the parent component that is changing type in <Morph>
(or morph
).
This will keep its matching children mounted, i.e. only if also rendered by the new parent. The parent itself always loses all state.
A component can render a detached child, to control when it renders. This is used e.g. by <Loop>
to run its rendering loop.
Detaching will call a callback with a render
function. The component calls it whenever it wants. It can be stored in a Ref
:
import type { Task } from '@use-gpu/live';
import { detach, useRef } from '@use-gpu/live';
// Render a detached `<Run />` and store its render function in a ref.
const ref = useRef<Task>(null);
return detach(<Run />, (render: Task) => ref.current = render);
Detaching only defers the render: detached children are still part of the same logical tree and inherit the same context/captures/gathers. When a dependency reaches across, both sides will still update in concert.
To insulate a detached tree from particular changes, its contents needs to be wrapped in specific context providers. e.g. <Loop>
's <Run>
provides a new FrameContext
, so that per-frame children will only update when it dispatches the next paint cycle.
Live has a native non-JSX syntax, which is designed to be ergonomic in use, unlike React.createElement
.
This is useful e.g. when mixing React code with Live code in the same file, while reserving JSX for React.
import { use, wrap, provide, yeet, gather } from '@use-gpu/live';
// <Component foo={bar} />
use(Component, {foo: bar})
// <Component><Child /></Component>
use(Component, {children: use(Child)})
// or
wrap(Component, use(Child))
// <Component><Child /><Child /></Component>
use(Component, {
children: [
use(Child),
use(Child),
],
})
// <Provide context={MyContext} value={value}>...</Provide>
provide(MyContext, value, children)
// <Yeet>{value}</Yeet>
yeet(value)
// <Gather then={(value) => { }}>
// <Child />
// </Gather>
gather(use(Child), (value) => { })
In native syntax, live components are not limited to a single props
argument, but can have any number, including 0. The wider type is LiveFunction
instead of LiveComponent
.