https://github.com/brandonmcconnell/render-hooks
Inline render-block-stable React hooks
https://github.com/brandonmcconnell/render-hooks
Last synced: about 1 month ago
JSON representation
Inline render-block-stable React hooks
- Host: GitHub
- URL: https://github.com/brandonmcconnell/render-hooks
- Owner: brandonmcconnell
- License: mit
- Created: 2025-05-15T18:40:39.000Z (about 1 month ago)
- Default Branch: main
- Last Pushed: 2025-05-15T18:40:39.000Z (about 1 month ago)
- Last Synced: 2025-05-15T18:49:52.315Z (about 1 month ago)
- Size: 0 Bytes
- Stars: 0
- Watchers: 1
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
- awesome - brandonmcconnell/render-hooks - Inline render-block-stable React hooks (TypeScript)
README
# Render Hooks
*Inline React hooks inside JSX.*
Render Hooks lets you place hooks right next to the markup that needs themβno wrapper components, no breaking the [Rules of Hooks](https://react.dev/reference/rules/rules-of-hooks), and zero boilerplate even when you supply your own custom hooks.
- [Render Hooks](#render-hooks)
- [π How it works](#-how-it-works)
- [β¨ Features](#-features)
- [π Install](#-install)
- [β‘ Quick start](#-quick-start)
- [π§© API](#-api)
- [π Examples by hook](#-examples-by-hook)
- [`useState`β(React β₯ 16.8)](#usestatereact--168)
- [`useReducer`β(React β₯ 16.8)](#usereducerreact--168)
- [`useCallback`β(React β₯ 16.8)](#usecallbackreact--168)
- [`useContext`β(React β₯ 16.8)](#usecontextreact--168)
- [`useMemo`β(React β₯ 16.8)](#usememoreact--168)
- [`useEffect`β(React β₯ 16.8)](#useeffectreact--168)
- [`useLayoutEffect`β(React β₯ 16.8)](#uselayouteffectreact--168)
- [`useImperativeHandle`β(React β₯ 16.8)](#useimperativehandlereact--168)
- [`useRef`β(React β₯ 16.8)](#userefreact--168)
- [`useInsertionEffect`β(React β₯ 18)](#useinsertioneffectreact--18)
- [`useId`β(React β₯ 18)](#useidreact--18)
- [`useSyncExternalStore`β(React β₯ 18)](#usesyncexternalstorereact--18)
- [`useDeferredValue`β(React β₯ 18)](#usedeferredvaluereact--18)
- [`useTransition`β(React β₯ 18)](#usetransitionreact--18)
- [`useActionState`β(React β₯ 19, experimental in 18)](#useactionstatereact--19-experimental-in-18)
- [`useFormStatus`β(React-DOM β₯ 19)](#useformstatusreact-dom--19)
- [`use`β(awaitable hook, React β₯ 19)](#useawaitable-hook-react--19)
- [π Custom hooks](#-custom-hooks)
- [π§± Nesting `RenderHooks`](#-nesting-renderhooks)
- [π€ Collaboration](#-collaboration)
- [π License](#-license)---
## π How it works
1. At runtime Render Hooks scans the installed `react` and `react-dom`
modules and wraps every export whose name starts with **`use`**.
2. A TypeScript mapped type reproduces *exactly* the same keys from the typings,
so autocompletion never lies.
3. The callback you give to `` (commonly aliased, e.g. `<$>`) is executed during that same render
pass, keeping the Rules of Hooks intact.
4. Custom hooks are merged in onceβstable reference, fully typed.---
## β¨ Features
| βοΈ | Description |
|----|-------------|
| **One element** | `<$>` merges every `use*` hook exposed by the consumer's version of **`react` + `react-dom`** into a single helpers object. |
| **Version-adaptive** | Only the hooks that exist in *your* React build appear. Upgrade React β new hooks show up automatically. |
| **Custom-hook friendly** | Pass an object of your own hooks onceβfull IntelliSense inside the render callback. |
| **100 % type-safe** | No `any`, no `unknown`. Generic signatures flow through the helpers object. |
| **Tiny runtime** | Just an object mergeβ`<$>` renders nothing to the DOM. |---
## π Install
```bash
npm install render-hooks # or yarn / pnpm / bun
```Render Hooks lists **`react`** and **`react-dom`** as peer dependencies, so it
always tracks *your* versions.---
## β‘ Quick start
```tsx
import $ from 'render-hooks';export function Counter() {
return (
<$>
{({ useState }) => {
const [n, set] = useState(0);
return set(n + 1)}>Clicked {n};
}}
$>
);
}
```The hook runs during the same render, so the Rules of Hooks are upheld.
---
## π§© API
| Prop | Type | Description |
|-------------|----------------------------------------------------|-------------|
| `hooks` | `Record unknown>` | (optional) custom hooks to expose. |
| `children` | `(helpers) β ReactNode` | Render callback receiving **all** built-in hooks available in your React version **plus** the custom hooks you supplied. |---
## π Examples by hook
Below is a **minimal, practical snippet for every built-in hook**.
Each header lists the **minimum React (or React-DOM) version** requiredβif your
project uses an older version, that hook simply won't appear in the helpers
object.> All snippets assume
> `import $ from 'render-hooks';`---
### `useState`β(React β₯ 16.8)
```tsx
export function UseStateExample() {
return (
<$>
{({ useState }) => {
const [value, set] = useState('');
return set(e.target.value)} />;
}}
$>
);
}
```---
### `useReducer`β(React β₯ 16.8)
```tsx
export function UseReducerExample() {
return (
<$>
{({ useReducer }) => {
const [count, dispatch] = useReducer(
(s: number, a: 'inc' | 'dec') => (a === 'inc' ? s + 1 : s - 1),
0,
);
return (
<>
dispatch('dec')}>-
{count}
dispatch('inc')}>οΌ
>
);
}}
$>
);
}
```---
### `useCallback`β(React β₯ 16.8)
```tsx
export function UseCallbackExample() {
return (
<$>
{({ useState, useCallback }) => {
const [txt, setTxt] = useState('');
const onChange = useCallback(
(e: React.ChangeEvent) => setTxt(e.target.value),
[],
);
return ;
}}
$>
);
}
```---
### `useContext`β(React β₯ 16.8)
```tsx
const ThemeCtx = React.createContext<'light' | 'dark'>('light');export function UseContextExample() {
return (
<$>
{({ useContext }) =>Theme: {useContext(ThemeCtx)}
}
$>
);
}
```---
### `useMemo`β(React β₯ 16.8)
```tsx
export function UseMemoExample() {
return (
<$>
{({ useState, useMemo }) => {
const [n, setN] = useState(25);
const fib = useMemo(() => {
const f = (x: number): number =>
x <= 1 ? x : f(x - 1) + f(x - 2);
return f(n);
}, [n]);
return (
<>
setN(+e.target.value)}
/>
Fib({n}) = {fib}
>
);
}}
$>
);
}
```---
### `useEffect`β(React β₯ 16.8)
```tsx
export function UseEffectExample() {
return (
<$>
{({ useState, useEffect }) => {
const [time, setTime] = useState('');
useEffect(() => {
const id = setInterval(
() => setTime(new Date().toLocaleTimeString()),
1000,
);
return () => clearInterval(id);
}, []);
return{time}
;
}}
$>
);
}
```---
### `useLayoutEffect`β(React β₯ 16.8)
```tsx
export function UseLayoutEffectExample() {
return (
<$>
{({ useRef, useLayoutEffect }) => {
const box = useRef(null);
useLayoutEffect(() => {
box.current!.style.background = '#ffd54f';
}, []);
returnhighlighted after layout;
}}
$>
);
}
```---
### `useImperativeHandle`β(React β₯ 16.8)
```tsx
const Fancy = React.forwardRef((_, ref) => (
<$>
{({ useRef, useImperativeHandle }) => {
const local = useRef(null);
useImperativeHandle(ref, () => ({ focus: () => local.current?.focus() }));
return ;
}}
$>
));export function UseImperativeHandleExample() {
const ref = React.useRef<{ focus: () => void }>(null);
return (
<>
ref.current?.focus()}>Focus
>
);
}
```---
### `useRef`β(React β₯ 16.8)
```tsx
export function UseRefExample() {
return (
<$>
{({ useRef }) => {
const input = useRef(null);
return (
<>
input.current?.focus()}>focus
>
);
}}
$>
);
}
```---
### `useInsertionEffect`β(React β₯ 18)
```tsx
export function UseInsertionEffectExample() {
return (
<$>
{({ useInsertionEffect }) => {
useInsertionEffect(() => {
const style = document.createElement('style');
style.textContent = `.flash{animation:flash 1s steps(2) infinite;}
@keyframes flash{to{opacity:.2}}`;
document.head.append(style);
return () => style.remove();
}, []);
returnflashing text
;
}}
$>
);
}
```---
### `useId`β(React β₯ 18)
```tsx
export function UseIdExample() {
return (
<$>
{({ useId, useState }) => {
const id = useId();
const [v, set] = useState('');
return (
<>
Name
set(e.target.value)} />
>
);
}}
$>
);
}
```---
### `useSyncExternalStore`β(React β₯ 18)
```tsx
export function UseSyncExternalStoreExample() {
return (
<$>
{({ useSyncExternalStore }) => {
const width = useSyncExternalStore(
(cb) => {
window.addEventListener('resize', cb);
return () => window.removeEventListener('resize', cb);
},
() => window.innerWidth,
);
returnwidth: {width}px
;
}}
$>
);
}
```---
### `useDeferredValue`β(React β₯ 18)
```tsx
export function UseDeferredValueExample() {
return (
<$>
{({ useState, useDeferredValue }) => {
const [text, setText] = useState('');
const deferred = useDeferredValue(text);
return (
<>
setText(e.target.value)} />
deferred: {deferred}
>
);
}}
$>
);
}
```---
### `useTransition`β(React β₯ 18)
```tsx
export function UseTransitionExample() {
return (
<$>
{({ useState, useTransition }) => {
const [list, setList] = useState([]);
const [pending, start] = useTransition();
const filter = (e: React.ChangeEvent) => {
const q = e.target.value;
start(() =>
setList(
Array.from({ length: 5_000 }, (_, i) => `Item ${i}`).filter((x) =>
x.includes(q),
),
),
);
};
return (
<>
{pending &&updatingβ¦
}
{list.length} items
>
);
}}
$>
);
}
```---
### `useActionState`β(React β₯ 19, experimental in 18)
```tsx
export function UseActionStateExample() {
return (
<$>
{({ useActionState }) => {
const [msg, submit, pending] = useActionState(
async (_prev: string, data: FormData) => {
await new Promise((r) => setTimeout(r, 400));
return data.get('text') as string;
},
'',
);
return (
Send
{msg &&You said: {msg}
}
);
}}
$>
);
}
```---
### `useFormStatus`β(React-DOM β₯ 19)
```tsx
export function UseFormStatusExample() {
return (
<$>
{({ useState, useFormStatus }) => {
const [done, setDone] = useState(false);
const { pending } = useFormStatus();const action = async () => {
await new Promise((r) => setTimeout(r, 400));
setDone(true);
};return (
{pending ? 'Savingβ¦' : 'Save'}
{done &&saved!
}
);
}}
$>
);
}
```---
### `use`β(awaitable hook, React β₯ 19)
```tsx
function fetchQuote() {
return new Promise((r) =>
setTimeout(() => r('"Ship early, ship often."'), 800),
);
}export function UseAwaitExample() {
return (
<$>
{({ use }) =>{use(fetchQuote())}}
$>
);
}
```---
## π Custom hooks
Inject any custom hooks once via the `hooks` prop:
```tsx
import $ from 'render-hooks';
import { useToggle, useDebounce } from './myHooks';export function Example() {
return (
<$ hooks={{ useToggle, useDebounce }}>
{({ useToggle, useDebounce }) => {
const [open, toggle] = useToggle(false);
const dOpen = useDebounce(open, 250);
return (
<>
toggle
debounced: {dOpen.toString()}
>
);
}}
$>
);
}
```---
## π§± Nesting `RenderHooks`
You can nest `RenderHooks` (`$`) as deeply as you need. Each instance provides its own fresh set of hooks, scoped to its render callback. This is particularly useful for managing item-specific state within loops, where you'd otherwise need to create separate components.
Here's an example where RenderHooks is used to manage state for both levels of a nested list directly within the `.map()` callbacks, and a child can affect a parent RenderHook's state:
```tsx
import React from 'react'; // Needed for useState, useTransition in this example
import $ from 'render-hooks';type Category = {
id: number;
name: string;
posts: { id: number; title: string }[];
};const data: Category[] = [
{
id: 1,
name: 'Tech',
posts: [{ id: 11, title: 'Next-gen CSS' }],
},
{
id: 2,
name: 'Life',
posts: [
{ id: 21, title: 'Minimalism' },
{ id: 22, title: 'Travel hacks' },
],
},
];export function NestedExample() {
return (
-
setExpanded(!expanded)}>
{expanded ? 'βΎ' : 'βΈ'} {cat.name} {likes === 0 ? 'π€' : 'β€οΈ'.repeat(likes)} ({likes} like{likes === 1 ? '' : 's'})
{isPending && ' (updating...)'}
{expanded && (
-
{post.title}{' '}
{liked ? 'β€οΈ Liked' : 'π€ Like'}
{cat.posts.map((post) => (
/* βββββ 2οΈβ£ Inner RenderHooks per post row βββββ */
<$ key={post.id}>
{({ useState: useItemState }) => {
const [liked, setItemLiked] = useItemState(false);const toggleLike = () => {
setItemLiked((prev) => {
// π Update outer Β«likesΒ» using startTransition from the parent RenderHooks
const next = !prev;
startTransition(() => {
setLikes((c) => c + (next ? 1 : -1));
});
return next;
});
};return (
);
}}
$>
))}
)}
-
{data.map((cat) => (
/* βββββ 1οΈβ£ Outer RenderHooks for each category row βββββ */
<$ key={cat.id}>
{({ useState, useTransition }) => {
const [expanded, setExpanded] = useState(false);
const [likes, setLikes] = useState(0);
const [isPending, startTransition] = useTransition();
return (
);
}}
$>
))}
);
}
```
In this example:
- The main `NestedExample` component does not use RenderHooks directly.
- The **first `.map()`** iterates through `data`. Inside this map, `<$>` is used to give each `category` its own states: `expanded` and `likes`. It also gets `useTransition` to acquire `startTransition`.
- The **second, inner `.map()`** iterates through `cat.posts`. Inside *this* map, another, nested `<$>` is used to give each `post` its own independent `liked` state.
- Crucially, when a post's `toggleLike` function is called, it updates its local `liked` state and then calls `startTransition` (obtained from the parent category's RenderHooks scope) to wrap the update to the parent's `likes` state.
This demonstrates not only nesting for independent state but also how functions and transition control from a parent RenderHooks instance can be utilized by children that also use RenderHooks, facilitating robust cross-scope communication.
> [!IMPORTANT]
> **Note on `startTransition`**: Using `startTransition` here is important. When an interaction (like clicking "Like") in a nested `RenderHooks` instance needs to update state managed by a parent `RenderHooks` instance, React might issue a warning about "updating one component while rendering another" if the update is synchronous. Wrapping the parent's state update in `startTransition` signals to React that this update can be deferred, preventing the warning and ensuring smoother UI updates. This is a general React pattern applicable when updates across component boundaries (or deeply nested state updates) might occur.
---
## π€ Collaboration
I welcome any issues or pull requests. Thank you for checking out the package!
---
## π License
MIT Β© 2025 Brandon McConnell