Skip to content

Instantly share code, notes, and snippets.

@markmals
Created May 30, 2025 02:25
Show Gist options
  • Save markmals/2b14bd845767579410552360116a74e8 to your computer and use it in GitHub Desktop.
Save markmals/2b14bd845767579410552360116a74e8 to your computer and use it in GitHub Desktop.
Fine(r)-grained rendering for React, backed by Solid's reactive primitives
import { createComponent, For } from "./runtime";
import { createMemo, createSignal } from "solid-js";
import { createMutable as createStore } from "solid-js/store";
export const Counter = createComponent<{ initialValue: number }>(props => {
const [counter, setCounter] = createSignal(props.initialValue);
const doubled = () => counter() * 2;
const inc = () => setCounter(c => c + 1);
return () => (
<div>
<div>
{counter()} × 2 = {doubled()}
</div>
<button onClick={inc}>Increment</button>
</div>
);
});
export const Counters = createComponent(() => {
const counters = createStore([1, 2, 3, 4, 5]);
const last = createMemo(() => counters[counters.length - 1] ?? 0);
const inc = () => counters.push(last() + 1);
const dec = () => counters.splice(counters.length - 1, 1);
return () => (
<>
<h1>Counters</h1>
<For each={() => counters} fallback={() => <i>No Counters</i>}>
{counter => <Counter key={counter} initialValue={counter} />}
</For>
<button onClick={inc}>Add More Counters</button>
<button onClick={dec}>Remove Counters</button>
</>
);
});
import { useEffect, useState, memo, useRef } from "react";
import type { MemoExoticComponent, ReactNode } from "react";
import { createComputed, createMemo, createRoot, mapArray, untrack } from "solid-js";
import { createStore, reconcile } from "solid-js/store";
import type { SetStoreFunction } from "solid-js/store";
export type SetupFunction<Props extends object> = (props: Props) => () => ReactNode;
export type MemoizedComponent<Props extends object> = MemoExoticComponent<
(props: Props) => ReactNode
>;
export function createComponent<Props extends object>(
setup: SetupFunction<Props>,
): MemoizedComponent<Props> {
return memo((props: Props) => {
const setProps = useRef<SetStoreFunction<Props>>(null);
const [vNode, setTemplate] = useState<ReactNode>(null);
useEffect(() => setProps.current?.(reconcile(props)), [props]);
useEffect(() => {
let cleanup: (() => void) | undefined = undefined;
createRoot(dispose => {
cleanup = dispose;
const [store, setStore] = createStore(props);
setProps.current = setStore;
const template = untrack(() => setup(store));
createComputed(() => setTemplate(template()));
});
return cleanup;
}, []);
return vNode;
});
}
export type ShowProps<T> = (
| {
when: () => boolean;
children: () => ReactNode;
}
| {
when: () => T;
children: (item: T) => ReactNode;
}
) & { fallback?: () => ReactNode };
const _Show = createComponent<ShowProps<any>>(props => {
return createMemo(() => {
const condition = props.when();
return condition ? props.children(condition) : (props.fallback?.() ?? null);
});
});
export function Show<T>(props: ShowProps<T>) {
return <_Show {...props} />;
}
export type ForProps<T> = {
each: () => T[];
children: (item: T) => ReactNode;
fallback?: () => ReactNode;
};
const _For = createComponent<ForProps<any>>(props => {
return () => (
<Show when={() => props.each().length} fallback={props.fallback}>
{mapArray(props.each, props.children)}
</Show>
);
});
export function For<T>(props: ForProps<T>) {
return <_For {...props} />;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment