An open API service indexing awesome lists of open source software.

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

Awesome Lists containing this project

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';
}, []);
return

highlighted 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();
}, []);
return

flashing 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,
);
return

width: {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 (


    {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 (


  • setExpanded(!expanded)}>
    {expanded ? 'β–Ύ' : 'β–Έ'} {cat.name} {likes === 0 ? 'πŸ–€' : '❀️'.repeat(likes)} ({likes} like{likes === 1 ? '' : 's'})
    {isPending && ' (updating...)'}

    {expanded && (


      {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 (


    • {post.title}{' '}

      {liked ? '❀️ Liked' : 'πŸ–€ Like'}


    • );
      }}
      $>
      ))}

    )}

  • );
    }}
    $>
    ))}

);
}
```

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