Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
969a9d1
feat: Initial version of @typegpu/react
iwoplaza Sep 10, 2025
9613f46
Scaffold @typegpu/react (#1696)
piaccho Mar 3, 2026
ba16551
Confetti example
iwoplaza Mar 3, 2026
9338a20
elapsedSeconds provided through useFrame, useConfigureContext for eas…
iwoplaza Mar 12, 2026
6d17a10
useBindGroup
iwoplaza Mar 12, 2026
292d094
Better useBuffer and more examples
iwoplaza Mar 12, 2026
292cddf
Example updates
iwoplaza Mar 12, 2026
1a93e55
Example updates v2
iwoplaza Mar 12, 2026
babcaa5
Update readme and peerDep versions
iwoplaza Mar 12, 2026
e4ebcb0
(TO-BE-REVISED) Example tests
iwoplaza Mar 13, 2026
1a5fd83
@typegpu/react-v0.10.0-alpha.1
iwoplaza Mar 16, 2026
dd7b2be
@typegpu/react-v0.10.0-alpha.2
iwoplaza Mar 16, 2026
8a2d8cf
Fix canvasRef type
iwoplaza Mar 19, 2026
ae528c8
feat: Better useConfigureContext options
iwoplaza Mar 23, 2026
206566a
@typegpu/react-v0.10.0-alpha.3
iwoplaza Mar 24, 2026
29f9634
Better hooks
iwoplaza Mar 31, 2026
bcae2ba
Better tests for useMirroredUniform
iwoplaza Mar 31, 2026
02e79a3
More hook
iwoplaza Apr 1, 2026
973c736
Fix createBuffer overloads
iwoplaza Apr 1, 2026
b0a659d
Better testing utils
iwoplaza Apr 2, 2026
4049204
Recreating buffer when schema or root changes
iwoplaza Apr 2, 2026
db81fe0
Tests for useBuffer hook
iwoplaza Apr 2, 2026
78b467b
Tests for useUniformValue hook
iwoplaza Apr 2, 2026
40f875d
Update example to match the new API
iwoplaza Apr 2, 2026
8d1bab6
@typegpu/react-v0.10.0-alpha.4
iwoplaza Apr 2, 2026
9180408
Separate browser and react-native entrypoints
iwoplaza Apr 3, 2026
0b5693b
More useful hooks like useReadonly and useUniform
iwoplaza Apr 3, 2026
3b7ff41
@typegpu/react-v0.10.0-alpha.5
iwoplaza Apr 3, 2026
d42da28
Not relying on `.getConfiguration()` as that's unavailable on React N…
iwoplaza Apr 3, 2026
d7f943e
@typegpu/react-v0.10.0-alpha.6
iwoplaza Apr 3, 2026
ecf2526
Better type for useConfigureContext
iwoplaza Apr 3, 2026
cb0bdec
@typegpu/react-v0.10.0-alpha.7
iwoplaza Apr 3, 2026
f9b774b
Cleanup
iwoplaza Apr 3, 2026
b4e674f
Make react-native an optional peer dependency
iwoplaza Apr 5, 2026
4a7bb67
Docs
iwoplaza Apr 8, 2026
c976eb6
Making sure react-native-wgpu is installed
iwoplaza Apr 12, 2026
5206db5
@typegpu/react-v0.10.0-alpha.8
iwoplaza Apr 12, 2026
f8d4ea1
Update docs
iwoplaza Apr 14, 2026
817664e
@typegpu/react-v0.11.0-alpha.1
iwoplaza Apr 22, 2026
608edeb
A `use` shim for older React versions
iwoplaza Apr 22, 2026
747f7e8
@typegpu/react-v0.11.0-alpha.2
iwoplaza Apr 22, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions apps/typegpu-docs/astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -200,10 +200,6 @@ export default defineConfig({
label: '@typegpu/noise',
slug: 'ecosystem/typegpu-noise',
},
{
label: '@typegpu/three',
slug: 'ecosystem/typegpu-three',
},
{
label: '@typegpu/sdf',
slug: 'ecosystem/typegpu-sdf',
Expand All @@ -212,6 +208,14 @@ export default defineConfig({
label: '@typegpu/color',
slug: 'ecosystem/typegpu-color',
},
{
label: '@typegpu/three',
slug: 'ecosystem/typegpu-three',
},
{
label: '@typegpu/react',
slug: 'ecosystem/typegpu-react',
},
DEV && {
label: 'Third-party',
slug: 'ecosystem/third-party',
Expand Down
1 change: 1 addition & 0 deletions apps/typegpu-docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"@typegpu/color": "workspace:*",
"@typegpu/geometry": "workspace:*",
"@typegpu/noise": "workspace:*",
"@typegpu/react": "workspace:*",
"@typegpu/sdf": "workspace:*",
"@typegpu/sort": "workspace:*",
"@typegpu/three": "workspace:*",
Expand Down
19 changes: 10 additions & 9 deletions apps/typegpu-docs/src/components/ExampleView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,21 +67,22 @@ function useExample(
export function ExampleView({ example, common }: Props) {
const { tsFiles: srcFiles, tsImport, htmlFile } = example;

const [snackbarText, setSnackbarText] = useAtom(currentSnackbarAtom);
const [currentFilePath, setCurrentFilePath] = useState<string>('index.ts');

const codeEditorShown = useAtomValue(codeEditorShownAtom);
const tsoverUsed = useAtomValue(tsoverUsedAtom);
const exampleHtmlRef = useRef<HTMLDivElement>(null);

const tsFiles = filterRelevantTsFiles(srcFiles, common);
const filePaths = tsFiles.map((file) => file.path);
const entryFile = filePaths.find((path) => path.startsWith('index.ts')) as string;
const editorTabsList = [
'index.ts',
...filePaths.filter((name) => name !== 'index.ts'),
entryFile,
...filePaths.filter((name) => name !== entryFile),
'index.html',
];

const [snackbarText, setSnackbarText] = useAtom(currentSnackbarAtom);
const [currentFilePath, setCurrentFilePath] = useState(entryFile);

const codeEditorShown = useAtomValue(codeEditorShownAtom);
const tsoverUsed = useAtomValue(tsoverUsedAtom);
const exampleHtmlRef = useRef<HTMLDivElement>(null);

useEffect(() => {
if (!exampleHtmlRef.current) {
return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import typegpuColorPackageJson from '@typegpu/color/package.json' with { type: '
import typegpuNoisePackageJson from '@typegpu/noise/package.json' with { type: 'json' };
import typegpuSdfPackageJson from '@typegpu/sdf/package.json' with { type: 'json' };
import typegpuThreePackageJson from '@typegpu/three/package.json' with { type: 'json' };
import typegpuReactPackageJson from '@typegpu/react/package.json' with { type: 'json' };
import typegpuPackageJson from 'typegpu/package.json' with { type: 'json' };
import unpluginPackageJson from 'unplugin-typegpu/package.json' with { type: 'json' };
import pnpmWorkspace from '../../../../../pnpm-workspace.yaml?raw';
Expand Down Expand Up @@ -122,6 +123,7 @@ ${example.htmlFile.content}
"@typegpu/color": "${typegpuColorPackageJson.version}",
"@typegpu/sdf": "${typegpuSdfPackageJson.version}",
"@typegpu/three": "${typegpuThreePackageJson.version}"
"@typegpu/react": "${typegpuReactPackageJson.version}"
}
}`,
'vite.config.js': `\
Expand Down
217 changes: 217 additions & 0 deletions apps/typegpu-docs/src/content/docs/ecosystem/typegpu-react/index.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
---
title: "@typegpu/react"
---

Setting up and managing TypeGPU resources in React components is not a trivial task, which is why we provide a set of hooks and utilities to
help with it.

## Setup

import { Tabs, TabItem } from '@astrojs/starlight/components';

Refer to [TypeGPU's installation guide](/TypeGPU/getting-started/) for setting up TypeGPU if you haven't already.
After that, install React and `@typegpu/react` using the package manager of your choice.

<Tabs syncKey="package-manager">
<TabItem label="npm" icon="seti:npm">
```sh frame=none
npm install @typegpu/react
```
</TabItem>
<TabItem label="pnpm" icon="pnpm">
```sh frame=none
pnpm add @typegpu/react
```
</TabItem>
<TabItem label="yarn" icon="seti:yarn">
```sh frame=none
yarn add @typegpu/react
```
</TabItem>
</Tabs>

## Your first effect

We first get a reference to a TypeGPU root via the [useRoot](#use-root) hook.

```tsx twoslash
import { useRoot } from '@typegpu/react';

function MyEffect() {
const root = useRoot();

// the rest of our code will be here
}
```

Then we create a [Render Pipeline](/TypeGPU/fundamentals/pipelines) that emits a red color over the whole canvas, and we memoize it.

```tsx twoslash
import React from 'react';
import { useMemo } from 'react';
import { d, common } from 'typegpu';
import { useConfigureContext, useFrame, useRoot, useUniform } from '@typegpu/react';

function MyEffect() {
const root = useRoot();
// ---cut---

const renderPipeline = useMemo(
() =>
root.createRenderPipeline({
vertex: common.fullScreenTriangle,
fragment: ({ uv }) => {
'use gpu';
// r g b a
return d.vec4f(1, 0, 0, 1);
},
}),
[root],
);
// ---cut-after---
}
```

We need to update our imports to get access to both `d` and `common` from typegpu:

```ts
import { d, common } from 'typegpu';
```

Then we call the `useConfigureContext` hook, which will give us a `ref` to pass into a `<canvas>` element, as well as access to the WebGPU context via `ctxRef`.

```ts
// ...
const { ref, ctxRef } = useConfigureContext();

return <canvas ref={ref} className="absolute inset-0" />;
}
```

After these changes, you should see a blank red canvas on your webpage. Let's make the effect more interesting.

### Dynamic values

To pass dynamic values to your shader without having to recompile it (compilation takes significant time), you can use [`useUniform`](#useuniform) or [`useMirroredUniform`](#usemirroreduniform), depending on the situation.
- [`useUniform`](#useuniform) - allocates a uniform that can be updated outside of the React lifecycle.
- [`useMirroredUniform`](#usemirroreduniform) - allocates a uniform and keeps it synchronized with the latest value.

```ts

```

### Complete example

```tsx twoslash
import React from 'react';
// ---cut---
import { useMemo } from 'react';
import { d, common } from 'typegpu';
import { useConfigureContext, useFrame, useRoot, useUniform } from '@typegpu/react';

function MyEffect() {
const root = useRoot();
const time = useUniform(d.f32);

const renderPipeline = useMemo(
() =>
root.createRenderPipeline({
vertex: common.fullScreenTriangle,
fragment: ({ uv }) => {
'use gpu';
return d.vec4f((uv * 5 + time.$) % 1, 0, 1);
},
}),
[root, time],
);

const { ref, ctxRef } = useConfigureContext();
useFrame(({ elapsedSeconds }) => {
if (!ctxRef.current) return;

time.write(elapsedSeconds);

renderPipeline.withColorAttachment({ view: ctxRef.current }).draw(3);
});

return <canvas ref={ref} className="absolute inset-0" />;
}
```

## API Reference

### useRoot

Returns the [TypeGPU root](/TypeGPU/fundamentals/roots) shared between all components
under the nearest `<Root>` provider. (equivalent to calling `await tgpu.init()`).

It's not necessary to use a `<Root>` provider; if none is found, the global root is used instead.

```tsx twoslash
import { useRoot } from '@typegpu/react';

function MyEffect() {
const root = useRoot();

// ...
}
```

If the root hasn't been initialized yet, it will suspend the component until it is.
This hook will also throw if there is an error initializing the root (like no WebGPU support).
If you'd like to handle these cases yourself, you can use `useRootOrError` or
`useRootWithStatus` instead.

### useUniform

```tsx twoslash
import { d } from 'typegpu';
import React from 'react';
import { useUniform } from '@typegpu/react';

function App() {
// ---cut---
const bruh = useUniform(d.f32);
// ---cut-after---
return null;
}
```

### useMirroredUniform

```tsx twoslash
import { d } from 'typegpu';
import React from 'react';
import { useMirroredUniform } from '@typegpu/react';

function App({ albedo }: { albedo: d.v3f }) {
// ---cut---
const bruh = useMirroredUniform(d.vec3f, albedo);
// ---cut-after---
return null;
}
```


### The `<Root>` component

The `<Root>` component is a context provider for all of it's descendants. All `useRoot` calls made by components beneath this wrapper
will return the same `TgpuRoot` object.

| Property | Type | Description |
|-|-|-|
| root | `TgpuRoot\|undefined` | A custom root to provide for all descendants. If not provided, one will be created on demand. |

```tsx twoslash
import React from 'react';
// ---cut---
import { Root } from '@typegpu/react';

function App() {
return (
<Root>
{/* ... other components ... */}
</Root>
);
}
```
19 changes: 12 additions & 7 deletions apps/typegpu-docs/src/examples/exampleContent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,35 +59,40 @@
);

// Files that were generated by stripping away use of overloaded operators in .ts files
// './<category>/<example>/<file>.tsnotover.ts'
const exampleTsnotoverFiles = import.meta.glob('./*/*/*.tsnotover.ts', {
// './<category>/<example>/<file>.tsnotover.tsx?'
const exampleTsnotoverFiles = import.meta.glob(['./*/*/*.tsnotover.ts', './*/*/*.tsnotover.tsx'], {
query: 'raw',
eager: true,
import: 'default',
}) as Record<string, string>;

function replaceExt(key: string, newExt: string) {
return `${key.substring(0, key.length - pathe.extname(key).length)}${newExt}`;
}

const exampleTsFiles = R.pipe(
// './<category>/<example>/[<subdir>/]<file>.ts'
import.meta.glob('./*/**/*.ts', {
// './<category>/<example>/[<subdir>/]<file>.tsx?'
import.meta.glob(['./*/**/*.ts', './*/**/*.tsx'], {
query: 'raw',
eager: true,
import: 'default',
}) as Record<string, string>,
R.entries(),
R.filter(([key]) => !key.endsWith('.tsnotover.ts')),
R.filter(([key]) => !key.includes('.tsnotover.')),
R.map(
([key, content]): ExampleSrcFile => ({
exampleKey: pathToExampleKey(key),
path: pathToRelativePath(key),
content,
tsnotoverContent: exampleTsnotoverFiles[`${key}notover.ts`],
tsnotoverContent:
exampleTsnotoverFiles[`${replaceExt(key, `.tsnotover${pathe.extname(key)}`)}`],

Check warning on line 88 in apps/typegpu-docs/src/examples/exampleContent.ts

View workflow job for this annotation

GitHub Actions / build-and-test

typescript-eslint(no-unnecessary-template-expression)

Template literal expression is unnecessary and can be simplified.
}),
),
R.groupBy(R.prop('exampleKey')),
);

const tsFilesImportFunctions = R.pipe(
import.meta.glob('./**/index.ts') as Record<string, () => Promise<unknown>>,
import.meta.glob(['./**/index.ts', './**/index.tsx']) as Record<string, () => Promise<unknown>>,
R.mapKeys(pathToExampleKey),
);

Expand Down
1 change: 1 addition & 0 deletions apps/typegpu-docs/src/examples/react/confetti/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<div id="example-app" class="absolute inset-0"></div>
Loading
Loading