From 60d19c476173b04ffcd6f86e8e1acda3f9a32652 Mon Sep 17 00:00:00 2001
From: James Robb <47126579+jamesrweb@users.noreply.github.com>
Date: Fri, 3 Apr 2026 03:29:45 +0200
Subject: [PATCH] feat: Add updater prop, refactor types, tighten docs and
build (#372)
Adds an `updater` prop to P5Canvas that lets users bridge React state
updates from within the p5 lifecycle without leaking React concerns
into sketch logic.
Type system refactor:
- Introduce P5CanvasInternalProps to cleanly separate component
props from user sketch props
- Use destructuring instead of withoutKeys for prop separation
- Remove React.lazy for P5CanvasWithSketch (direct import preserves
generic type flow)
- Remove generics from internal components (only needed at the
public API surface)
- Delete InputProps, P5CanvasPropsWithSketch, WithChildren, and
withoutKeys as they are no longer needed
- Remove boxed primitive instanceof checks from logErrorBoundaryError
Docs:
- Merge duplicate TypeScript examples into single examples with notes
- Collapse advanced topics into details blocks
- Remove buggy falsy guards from updateWithProps examples
- Fix error UI example to use unknown instead of any
- Fix example sketches link to use main branch
Build:
- Remove dead esbuild config, Vite 8 uses OXC by default
- Split p5 into its own chunk in the demo build via manualChunks
Co-Authored-By: Claude Opus 4.6 (1M context)
---
.github/workflows/claude.yml | 4 +-
README.md | 423 +++++++-----------
config/vite/demo.ts | 8 +-
config/vite/library.ts | 3 -
pnpm-lock.yaml | 71 ++-
pnpm-workspace.yaml | 13 +-
src/components/P5CanvasGuard.tsx | 40 +-
src/components/P5CanvasWithSketch.tsx | 45 +-
...InputProps.ts => P5CanvasInternalProps.ts} | 12 +-
src/contracts/P5CanvasProps.ts | 5 +-
src/contracts/P5CanvasPropsWithSketch.ts | 6 -
src/contracts/Updater.ts | 7 +
src/contracts/WithChildren.ts | 3 -
src/main.tsx | 1 +
src/utils/logErrorBoundaryError.ts | 18 +-
src/utils/withoutKeys.ts | 16 -
tests/components/P5Canvas.test.tsx | 75 ++++
tests/utils/logErrorBoundaryError.test.ts | 8 +-
tests/utils/withoutKeys.test.ts | 19 -
19 files changed, 349 insertions(+), 428 deletions(-)
rename src/contracts/{InputProps.ts => P5CanvasInternalProps.ts} (50%)
delete mode 100644 src/contracts/P5CanvasPropsWithSketch.ts
create mode 100644 src/contracts/Updater.ts
delete mode 100644 src/contracts/WithChildren.ts
delete mode 100644 src/utils/withoutKeys.ts
delete mode 100644 tests/utils/withoutKeys.test.ts
diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml
index 7f70b267..b42c6390 100644
--- a/.github/workflows/claude.yml
+++ b/.github/workflows/claude.yml
@@ -8,7 +8,7 @@ on:
issues:
types: [opened, assigned]
pull_request_target:
- types: [opened, assigned, synchronize, ready_for_review]
+ types: [assigned]
pull_request_review:
types: [submitted]
@@ -19,7 +19,7 @@ jobs:
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
(github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude'))) ||
- (github.event_name == 'pull_request_target' && github.event.pull_request.head.repo.full_name == github.repository)
+ (github.event_name == 'pull_request_target')
runs-on: ubuntu-latest
permissions:
contents: write
diff --git a/README.md b/README.md
index 422b418e..a69322d1 100644
--- a/README.md
+++ b/README.md
@@ -30,64 +30,46 @@ If you are still using version 4, you can find the documentation
## Installation
-To install, use the following command in the format appropriate to your chosen
-package manager:
-
```shell
[npm|yarn|pnpm] [install|add] p5 @p5-wrapper/react
```
-### Peer dependencies
-
-Please note that `p5`, `react` and `react-dom` are peer dependencies. Make sure
-they are installed in your project before installing this package.
+`p5`, `react` and `react-dom` are peer dependencies and must be installed in
+your project.
-```js
-"peerDependencies": {
- "p5": ">= 2.0.0",
- "react": ">= 19.0.0",
- "react-dom": ">= 19.0.0"
-},
-```
-
-### TypeScript
+TypeScript setup
-If you would like to use Typescript, you should install `p5` types in the
-development environment:
+Install the p5 type definitions as a dev dependency:
```shell
[npm|yarn|pnpm] [install|add] -D @types/p5
```
-### Next.js
+
+
+Next.js setup
-If you plan to use this component within a Next.js application, you should
-instead use
-[our Next.js dynamic implementation](https://github.com/P5-wrapper/next)
-instead. To do get started, you can run:
+For Next.js applications, use
+[our Next.js dynamic implementation](https://github.com/P5-wrapper/next):
```shell
[npm|yarn|pnpm] [install|add] p5 @p5-wrapper/next @p5-wrapper/react
```
-Please continue reading these docs and also look at
+See
[the Next.js dynamic implementation docs](https://github.com/P5-wrapper/next)
-for further supporting information.
+for further details.
-## Demo & Examples
+
-### Live demo
+## Demo & Examples
A live demo can be viewed at
-[P5-wrapper.github.io/react](https://P5-wrapper.github.io/react/).
+[P5-wrapper.github.io/react](https://P5-wrapper.github.io/react/). The
+repository also contains
+[example sketches](https://github.com/P5-wrapper/react/tree/main/demo/sketches).
-### Examples
-
-The repository contains further
-[examples](https://github.com/P5-wrapper/react/tree/master/demo/sketches).
-
-To try them out for yourself fork the repository, be sure you have
-[PNPM](https://pnpm.io/) installed and then run the following:
+To run the examples locally:
```sh
git clone git@github.com:/react.git
@@ -96,11 +78,11 @@ pnpm install
pnpm preview
```
-Then just open `http://localhost:3001` in a browser.
+Then open `http://localhost:3001` in a browser.
## Usage
-### Javascript
+### JavaScript
```jsx
import * as React from "react";
@@ -128,20 +110,14 @@ export function App() {
### TypeScript
-TypeScript sketches can be declared in two different ways, below you will find
-two ways to declare a sketch, both examples do the exact same thing.
-
-In short though, the component requires you to pass a `sketch` prop. The
-`sketch` prop is simply a function which takes a `p5` instance as it's first and
-only argument.
-
-#### Option 1: Declaring a sketch using the `P5CanvasInstance` type
+The `sketch` prop is a function that receives a p5 instance. You can type it
+using either the `Sketch` type or the `P5CanvasInstance` type:
```tsx
import * as React from "react";
-import { P5Canvas, P5CanvasInstance } from "@p5-wrapper/react";
+import { P5Canvas, Sketch } from "@p5-wrapper/react";
-function sketch(p5: P5CanvasInstance) {
+const sketch: Sketch = p5 => {
p5.setup = () => p5.createCanvas(600, 400, p5.WEBGL);
p5.draw = () => {
@@ -154,33 +130,40 @@ function sketch(p5: P5CanvasInstance) {
p5.plane(100);
p5.pop();
};
-}
+};
export function App() {
return ;
}
```
-#### Option 2: Declaring a sketch using the `Sketch` type
+> `Sketch` auto-types the `p5` argument for you. If you prefer a regular
+> `function` declaration, you can use `P5CanvasInstance` directly:
+>
+> ```ts
+> import { P5CanvasInstance } from "@p5-wrapper/react";
+>
+> function sketch(p5: P5CanvasInstance) {
+> /* ... */
+> }
+> ```
-Using the `Sketch` type has one nice benefit over using `P5CanvasInstance` and
-that is that the `p5` argument passed to the sketch function is auto-typed as a
-`P5CanvasInstance` for you.
+### Using abstracted setup and draw functions
-> Side note:
->
-> In general, it comes down to personal preference as to how you declare your
-> sketches and there is nothing wrong with using the `P5CanvasInstance` manually
-> in a regular `function` declaration.
+If you prefer to split your sketch logic into separate functions:
-```tsx
+```jsx
import * as React from "react";
-import { P5Canvas, Sketch } from "@p5-wrapper/react";
+import { P5Canvas } from "@p5-wrapper/react";
-const sketch: Sketch = p5 => {
- p5.setup = () => p5.createCanvas(600, 400, p5.WEBGL);
+function setup(p5) {
+ return () => {
+ p5.createCanvas(600, 400, p5.WEBGL);
+ };
+}
- p5.draw = () => {
+function draw(p5) {
+ return () => {
p5.background(250);
p5.normalMaterial();
p5.push();
@@ -190,58 +173,38 @@ const sketch: Sketch = p5 => {
p5.plane(100);
p5.pop();
};
-};
+}
+
+function sketch(p5) {
+ p5.setup = setup(p5);
+ p5.draw = draw(p5);
+}
export function App() {
return ;
}
```
-#### TypeScript Generics
-
-We also support the use of Generics to add type definitions for your props. If
-used, the props will be properly typed when the props are passed to the
-`updateWithProps` method.
-
-To utilise generics you can use one of two methods. In both of the examples
-below, we create a custom internal type called `MySketchProps` which is a union
-type of `SketchProps` and a custom type which has a `rotation` key applied to
-it.
+### Props
-> Side note:
->
-> We could also write the `MySketchProps` type as an interface to do exactly the
-> same thing if that is to your personal preference:
->
-> ```ts
-> interface MySketchProps extends SketchProps {
-> rotation: number;
-> }
-> ```
+You can pass any custom props to `P5Canvas`. These are forwarded to the
+`updateWithProps` method if you define it in your sketch.
-This means, in these examples, that when the `rotation` prop that is provided as
-part of the `props` passed to the `updateWithProps` function, it will be
-correctly typed as a `number`.
+#### Reacting to props
-##### Usage with the `P5CanvasInstance` type
+`updateWithProps` is called on initial render and whenever the props change:
-```tsx
-import { P5Canvas, P5CanvasInstance, SketchProps } from "@p5-wrapper/react";
+```jsx
+import { P5Canvas } from "@p5-wrapper/react";
import React, { useEffect, useState } from "react";
-type MySketchProps = SketchProps & {
- rotation: number;
-};
-
-function sketch(p5: P5CanvasInstance) {
+function sketch(p5) {
let rotation = 0;
p5.setup = () => p5.createCanvas(600, 400, p5.WEBGL);
p5.updateWithProps = props => {
- if (props.rotation) {
- rotation = (props.rotation * Math.PI) / 180;
- }
+ rotation = (props.rotation * Math.PI) / 180;
};
p5.draw = () => {
@@ -273,7 +236,9 @@ export function App() {
}
```
-##### Usage with the `Sketch` type
+#### Typed props with generics
+
+Use generics so that props passed to `updateWithProps` are properly typed:
```tsx
import { P5Canvas, Sketch, SketchProps } from "@p5-wrapper/react";
@@ -289,9 +254,7 @@ const sketch: Sketch = p5 => {
p5.setup = () => p5.createCanvas(600, 400, p5.WEBGL);
p5.updateWithProps = props => {
- if (props.rotation) {
- rotation = (props.rotation * Math.PI) / 180;
- }
+ rotation = (props.rotation * Math.PI) / 180;
};
p5.draw = () => {
@@ -323,71 +286,43 @@ export function App() {
}
```
-### Using abstracted setup and draw functions
-
-```jsx
-import * as React from "react";
-import { P5Canvas } from "@p5-wrapper/react";
-
-function setup(p5) {
- return () => {
- p5.createCanvas(600, 400, p5.WEBGL);
- };
-}
-
-function draw(p5) {
- return () => {
- p5.background(250);
- p5.normalMaterial();
- p5.push();
- p5.rotateZ(p5.frameCount * 0.01);
- p5.rotateX(p5.frameCount * 0.01);
- p5.rotateY(p5.frameCount * 0.01);
- p5.plane(100);
- p5.pop();
- };
-}
-
-function sketch(p5) {
- p5.setup = setup(p5);
- p5.draw = draw(p5);
-}
-
-export function App() {
- return ;
-}
-```
-
-### Props
-
-The only required property is the sketch prop. The sketch prop is a function
-that will be passed a p5 instance to use for rendering your sketches (see the
-usage section above).
+> You can also write `MySketchProps` as an interface:
+>
+> ```ts
+> interface MySketchProps extends SketchProps {
+> rotation: number;
+> }
+> ```
+>
+> And if you prefer `P5CanvasInstance` over `Sketch`, generics work the same
+> way:
+>
+> ```ts
+> function sketch(p5: P5CanvasInstance) {
+> /* ... */
+> }
+> ```
-You can pass as many custom props as you want. These will be passed into the
-updateWithProps method if you have defined it within your sketch.
+### Custom updaters
-#### Reacting to props
+If you need to bridge React state updates from within the p5 lifecycle — for
+example, reading `frameCount` or other instance properties to drive React state
+— you can use the `updater` prop. It receives a callback that runs alongside
+`updateWithProps` on every props change, but lives in the React layer:
-In the below example you see the `updateWithProps` method being used. This is
-called when the component initially renders and when the props passed to the
-`P5Canvas` component are changed, if it is set within your sketch. This way we
-can render our component and react to component prop changes directly within our
-sketches!
+```tsx
+import { P5Canvas, Sketch, SketchProps, Updater } from "@p5-wrapper/react";
+import React, { useCallback, useState } from "react";
-```jsx
-import { P5Canvas } from "@p5-wrapper/react";
-import React, { useEffect, useState } from "react";
+type MySketchProps = SketchProps & { rotation: number };
-function sketch(p5) {
+const sketch: Sketch = p5 => {
let rotation = 0;
p5.setup = () => p5.createCanvas(600, 400, p5.WEBGL);
p5.updateWithProps = props => {
- if (props.rotation) {
- rotation = (props.rotation * Math.PI) / 180;
- }
+ rotation = (props.rotation * Math.PI) / 180;
};
p5.draw = () => {
@@ -399,35 +334,31 @@ function sketch(p5) {
p5.box(100);
p5.pop();
};
-}
+};
export function App() {
const [rotation, setRotation] = useState(0);
+ const [frameCount, setFrameCount] = useState(0);
- useEffect(() => {
- const interval = setInterval(
- () => setRotation(rotation => rotation + 100),
- 100
- );
-
- return () => {
- clearInterval(interval);
- };
+ const updater = useCallback>((instance, props) => {
+ setFrameCount(instance.frameCount);
}, []);
- return ;
+ return ;
}
```
+The `updater` callback receives the p5 instance and the current sketch props. It
+is not passed to `updateWithProps`, so it does not leak React concerns into your
+sketch logic.
+
### Children
-To render a component on top of the sketch, you can add it as a child of the
-`P5Canvas` component and then use the exported constant
-`CanvasContainerClassName` in your css-in-js library of choice to style one
-element above the other via css.
+To render a component on top of the sketch, add it as a child of `P5Canvas` and
+use the exported `CanvasContainerClassName` constant to style the overlay via
+CSS.
-For instance, using [styled components](https://styled-components.com), we could
-center some text on top of our sketch like so:
+Example with styled-components
```jsx
import { CanvasContainerClassName, P5Canvas } from "@p5-wrapper/react";
@@ -477,16 +408,14 @@ export function App() {
}
```
-Of course, you can also use any other css-in-js library or by just using simple
-css to achieve almost anything you can imagine just by using the wrapper class
+
+
+You can use any CSS-in-JS library or plain CSS — just target the wrapper class
as your root selector.
### Fallback UIs
-Lets say you want to have a fallback UI in case the `sketch` ever falls out of
-sync or is undefined for some reason. If this is a use case for you then you
-call use the `fallback` prop to provide the necessary UI to show in the case
-that the `sketch` becomes undefined. An example could be as follows:
+If the `sketch` prop is undefined, you can provide a fallback UI:
```jsx
import * as React from "react";
@@ -494,7 +423,6 @@ import { P5Canvas } from "@p5-wrapper/react";
function sketchOne(p5) {
p5.setup = () => p5.createCanvas(600, 400, p5.WEBGL);
-
p5.draw = () => {
p5.background(250);
p5.normalMaterial();
@@ -509,7 +437,6 @@ function sketchOne(p5) {
function sketchTwo(p5) {
p5.setup = () => p5.createCanvas(600, 400, p5.WEBGL);
-
p5.draw = () => {
p5.background(500);
p5.normalMaterial();
@@ -547,47 +474,37 @@ export function App() {
}
```
-In this case, by default the fallback UI containing
-`No sketch selected yet.
` will be rendered, then if you select a
-sketch, it will be rendered until you choose to once again "show nothing" which
-falls back to the fallback UI.
+When no sketch is selected, the fallback UI is shown. Selecting a sketch
+replaces it with the canvas.
### Error and Loading UIs
-You can pass `error` and `loading` props to the `P5Canvas` component to provide
-custom UIs for error and loading states.
-
-- The `error` state will trigger if the sketch or the wrapper encounter an
- issue, otherwise a default error view will be shown.
-- The `loading` state will trigger while the wrapper is being lazy loaded,
- otherwise a default loading view will be shown.
+You can pass `error` and `loading` props to customise what is shown when
+something goes wrong or while the component is loading.
#### Error UIs
-To show a custom UI when an error occurs within the sketch or the component, you
-can pass a function to the `error` prop.
+Pass a function to the `error` prop to handle errors thrown within the sketch or
+its children:
```tsx
import * as React from "react";
import { P5Canvas, P5CanvasInstance } from "@p5-wrapper/react";
-// This child will throw an error, oh no!
function ErrorChild() {
throw new Error("oops");
}
-// This view will catch the thrown error and give you access to what exactly was thrown.
-function ErrorUI(error: any) {
+function ErrorUI(error: unknown) {
if (error instanceof Error) {
return An error occured: {error.message}
;
}
- return An unknown error occured: {error.toString()}
;
+ return An unknown error occured: {String(error)}
;
}
function sketch(p5: P5CanvasInstance) {
p5.setup = () => p5.createCanvas(600, 400, p5.WEBGL);
-
p5.draw = () => {
p5.background(250);
p5.normalMaterial();
@@ -609,17 +526,13 @@ export function App() {
}
```
-Instead of the sketch, this will render `An error occured: oops
`. Note
-that in truth, the `ErrorView` will **always** receive `any` values since JS /
-TS allow you to `throw` whatever values you want to, this is why we have to add
-the `error instanceof Error` check to be sure the value we got was actually an
-`Error` instance and not some other value like a `number`, `string`, `object` or
-anything else that could be thrown by JS / TS.
+> JS/TS allow you to `throw` any value, not just `Error` instances. Always check
+> `error instanceof Error` before accessing `.message`.
#### Loading UIs
-To show a custom UI while the sketch UI is being lazy loaded, you can pass a
-function to the `loading` prop.
+Pass a function to the `loading` prop to show a custom UI while the component is
+being lazy loaded:
```tsx
import * as React from "react";
@@ -631,7 +544,6 @@ function LoadingUI() {
function sketch(p5: P5CanvasInstance) {
p5.setup = () => p5.createCanvas(600, 400, p5.WEBGL);
-
p5.draw = () => {
p5.background(250);
p5.normalMaterial();
@@ -649,29 +561,14 @@ export function App() {
}
```
-In the initial period between the sketch render starting and it's lazy loading
-ending, the `LoadingUI` will be shown!
-
## P5 plugins and constructors
-As discussed in multiple issues such as
-[#11](https://github.com/P5-wrapper/react/issues/11),
-[#23](https://github.com/P5-wrapper/react/issues/23),
-[#61](https://github.com/P5-wrapper/react/issues/61) and
-[#62](https://github.com/P5-wrapper/react/issues/62), there seems to be
-confusion as to how we can use P5 plugins and constructors out of the box. This
-section aims to clarify these!
+Using plugins (e.g. p5.sound)
-### Plugins
-
-Since P5 is being used in
-[P5 instance mode](https://github.com/processing/p5.js/wiki/Global-and-instance-mode)
-as part of this project, P5 will not automatically load global plugins like it
-usually might in global mode.
-
-Let's say we want to use the
-[P5 sound plugin](https://p5js.org/reference/#/libraries/p5.sound) in our
-component, we could do the following:
+Since P5 is used in
+[instance mode](https://github.com/processing/p5.js/wiki/Global-and-instance-mode),
+plugins are not loaded automatically. You need to set `p5` on the `window`
+object before importing the plugin:
```tsx
import * as p5 from "p5";
@@ -734,57 +631,43 @@ export default function App() {
}
```
-In this Typescript + React example, we can see a few key things.
-
-- Firstly we need to set `p5` on the `window` object manually. This is because
- `p5.sound` requires that it be executed client side only AND that `p5` be
- available BEFORE it is imported into the global (`window`) scope.
-- Secondly, we ensure that audio is played after a user action, in our case this
- happens on a button click. This is because in some browsers, without waiting
- for a user interaction before playing audio, the audio will be blocked by the
- browser from playing at all.
-- Thirdly and relevant especially to Safari users, Safari blocks audio from all
- tabs by default, you will need to manually change this setting in your Safari
- settings. This could affect other browsers but sadly this is a browser
- decision and until [P5 Sound](https://github.com/processing/p5.js-sound) is
- updated to support newer audio APIs and browser requirements. This could
- happen at anytime in other places and is a
- [P5 Sound](https://github.com/processing/p5.js-sound) issue most generally
- because it does not ask for permissions by default, even though browsers have
- been requiring it for some time.
-
-> **Note:** The above example requires support for
+Key points:
+
+- `p5` must be set on `window` before importing the plugin.
+- Audio must be triggered by a user action (e.g. a button click) — browsers
+ block autoplay.
+- Safari blocks audio from all tabs by default; users may need to change this in
+ their browser settings.
+
+> **Note:** This example requires
> [top level await](https://caniuse.com/mdn-javascript_operators_await_top_level),
-> [dynamic import statements](https://caniuse.com/es6-module-dynamic-import) and
-> [the stream API](https://caniuse.com/stream) to be supported in your browser.
-> Furthermore, [the stream API](https://caniuse.com/stream) built into the
-> browser requires that HTTPS is used to ensure secure data transmission.
+> [dynamic imports](https://caniuse.com/es6-module-dynamic-import) and
+> [the stream API](https://caniuse.com/stream) (HTTPS only).
-### Constructors
+
-To access P5 constructors such as `p5.Vector` or `p5.Envelope`, you need to use
-the instance mode syntax instead. For example:
+Using constructors (e.g. p5.Vector)
-| Constructor | Global mode accessor | Instance mode accessor |
-| ----------- | -------------------- | ----------------------- |
-| Vector | p5.Vector | p5.constructor.Vector |
-| Envelope | p5.Envelope | p5.constructor.Envelope |
+In instance mode, constructors are accessed via `p5.constructor` instead of the
+global `p5` namespace:
-So now that we know this, let's imagine we want a random 2D Vector instance. In
-our `sketch` function we would simply call `p5.constructor.Vector.random2D()`
-instead of `p5.Vector.random2D()`. This is because of how the
-[P5 instance mode](https://github.com/processing/p5.js/wiki/Global-and-instance-mode)
-was implemented by the P5 team. While I am not sure why they decided to change
-the API for instance mode specifically, it is still quite simple to use the
-constructs we are used to without much extra work involved.
+| Constructor | Global mode | Instance mode |
+| ----------- | ----------- | ----------------------- |
+| Vector | p5.Vector | p5.constructor.Vector |
+| Envelope | p5.Envelope | p5.constructor.Envelope |
+
+For example, to get a random 2D vector, call `p5.constructor.Vector.random2D()`
+instead of `p5.Vector.random2D()`.
+
+
## Development
-**NOTE:** The source code for the component is in the `src` directory.
+The source code for the component is in the `src` directory.
-To build, watch and serve the examples which will also watch the component
-source, run:
+To build, watch and serve the examples (which also watches the component
+source):
```sh
- pnpm preview
+pnpm preview
```
diff --git a/config/vite/demo.ts b/config/vite/demo.ts
index 1bc6b6bb..1e287535 100644
--- a/config/vite/demo.ts
+++ b/config/vite/demo.ts
@@ -9,10 +9,16 @@ export function demo(root: string): UserConfig {
plugins: [react()],
preview: { open: true },
build: {
+ chunkSizeWarningLimit: 1200,
emptyOutDir: false,
rollupOptions: {
output: {
- dir: resolve(root, "dist", "demo")
+ dir: resolve(root, "dist", "demo"),
+ manualChunks: moduleId => {
+ if (moduleId.includes("node_modules/p5")) {
+ return "p5";
+ }
+ }
}
}
}
diff --git a/config/vite/library.ts b/config/vite/library.ts
index 32daa0b0..6575ef5a 100644
--- a/config/vite/library.ts
+++ b/config/vite/library.ts
@@ -15,9 +15,6 @@ export function library(root: string): UserConfig {
}),
react()
],
- esbuild: {
- legalComments: "external"
- },
build: {
emptyOutDir: true,
lib: {
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 83bfe659..86f49c60 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -7,16 +7,17 @@ settings:
overrides:
'@isaacs/brace-expansion@<=5.0.0': '>=5.0.1'
ajv@>=7.0.0-alpha.0 <8.18.0: '>=8.18.0'
+ brace-expansion@>=2.0.0 <2.0.3: '>=2.0.3'
+ brace-expansion@>=4.0.0 <5.0.5: '>=5.0.5'
diff@>=6.0.0 <8.0.3: '>=8.0.3'
- flatted@<3.4.0: '>=3.4.0'
flatted@<=3.4.1: '>=3.4.2'
- lodash@>=4.0.0 <=4.17.22: '>=4.17.23'
- minimatch@<3.1.3: '>=3.1.3'
+ lodash-es@<=4.17.23: '>=4.18.0'
+ lodash@<=4.17.23: '>=4.18.0'
minimatch@<3.1.4: '>=3.1.4'
- minimatch@>=10.0.0 <10.2.1: '>=10.2.1'
- minimatch@>=10.0.0 <10.2.3: '>=10.2.3'
- minimatch@>=9.0.0 <9.0.6: '>=9.0.6'
minimatch@>=9.0.0 <9.0.7: '>=9.0.7'
+ minimatch@>=10.0.0 <10.2.3: '>=10.2.3'
+ picomatch@<2.3.2: '>=2.3.2'
+ picomatch@>=4.0.0 <4.0.4: '>=4.0.4'
rollup@>=4.0.0 <4.59.0: '>=4.59.0'
importers:
@@ -1167,9 +1168,6 @@ packages:
babel-plugin-react-compiler@1.0.0:
resolution: {integrity: sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw==}
- balanced-match@1.0.2:
- resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
-
balanced-match@4.0.4:
resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==}
engines: {node: 18 || 20 || >=22}
@@ -1183,13 +1181,14 @@ packages:
resolution: {integrity: sha512-k9xFKplee6KIio3IDbwj+uaCLpqzOwakOgmqzPezM0sFJlFKcg30vk2wOiAJtkTSfx0SSQDSe8q+mWA/fSH5Zg==}
hasBin: true
- brace-expansion@2.0.2:
- resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==}
-
brace-expansion@5.0.4:
resolution: {integrity: sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==}
engines: {node: 18 || 20 || >=22}
+ brace-expansion@5.0.5:
+ resolution: {integrity: sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==}
+ engines: {node: 18 || 20 || >=22}
+
braces@3.0.3:
resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
engines: {node: '>=8'}
@@ -1559,7 +1558,7 @@ packages:
resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
engines: {node: '>=12.0.0'}
peerDependencies:
- picomatch: ^3 || ^4
+ picomatch: '>=4.0.4'
peerDependenciesMeta:
picomatch:
optional: true
@@ -2081,11 +2080,11 @@ packages:
resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
engines: {node: '>=10'}
- lodash-es@4.17.23:
- resolution: {integrity: sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==}
+ lodash-es@4.18.1:
+ resolution: {integrity: sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==}
- lodash@4.17.23:
- resolution: {integrity: sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==}
+ lodash@4.18.1:
+ resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==}
loose-envify@1.4.0:
resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
@@ -2297,14 +2296,6 @@ packages:
picocolors@1.1.1:
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
- picomatch@2.3.1:
- resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
- engines: {node: '>=8.6'}
-
- picomatch@4.0.3:
- resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==}
- engines: {node: '>=12'}
-
picomatch@4.0.4:
resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==}
engines: {node: '>=12'}
@@ -3416,7 +3407,7 @@ snapshots:
'@rushstack/terminal': 0.19.5(@types/node@25.5.0)
'@rushstack/ts-command-line': 5.1.5(@types/node@25.5.0)
diff: 8.0.3
- lodash: 4.17.23
+ lodash: 4.18.1
minimatch: 10.2.4
resolve: 1.22.11
semver: 7.5.4
@@ -3514,7 +3505,7 @@ snapshots:
dependencies:
'@types/estree': 1.0.8
estree-walker: 2.0.2
- picomatch: 4.0.3
+ picomatch: 4.0.4
'@rushstack/node-core-library@5.19.1(@types/node@25.5.0)':
dependencies:
@@ -3596,7 +3587,7 @@ snapshots:
'@babel/traverse': 7.29.0
'@babel/types': 7.29.0
javascript-natural-sort: 0.7.1
- lodash-es: 4.17.23
+ lodash-es: 4.18.1
minimatch: 10.2.4
parse-imports-exports: 0.2.4
prettier: 3.8.1
@@ -4000,19 +3991,17 @@ snapshots:
dependencies:
'@babel/types': 7.29.0
- balanced-match@1.0.2: {}
-
balanced-match@4.0.4: {}
baseline-browser-mapping@2.10.13: {}
baseline-browser-mapping@2.9.7: {}
- brace-expansion@2.0.2:
+ brace-expansion@5.0.4:
dependencies:
- balanced-match: 1.0.2
+ balanced-match: 4.0.4
- brace-expansion@5.0.4:
+ brace-expansion@5.0.5:
dependencies:
balanced-match: 4.0.4
@@ -4925,7 +4914,7 @@ snapshots:
chalk: 4.1.2
ci-info: 4.3.1
graceful-fs: 4.2.11
- picomatch: 4.0.3
+ picomatch: 4.0.4
jiti@2.5.1:
optional: true
@@ -5065,9 +5054,9 @@ snapshots:
dependencies:
p-locate: 5.0.0
- lodash-es@4.17.23: {}
+ lodash-es@4.18.1: {}
- lodash@4.17.23: {}
+ lodash@4.18.1: {}
loose-envify@1.4.0:
dependencies:
@@ -5115,17 +5104,17 @@ snapshots:
micromatch@4.0.8:
dependencies:
braces: 3.0.3
- picomatch: 2.3.1
+ picomatch: 4.0.4
min-indent@1.0.1: {}
minimatch@10.2.4:
dependencies:
- brace-expansion: 5.0.4
+ brace-expansion: 5.0.5
minimatch@9.0.9:
dependencies:
- brace-expansion: 2.0.2
+ brace-expansion: 5.0.4
minipass@7.1.3: {}
@@ -5290,10 +5279,6 @@ snapshots:
picocolors@1.1.1: {}
- picomatch@2.3.1: {}
-
- picomatch@4.0.3: {}
-
picomatch@4.0.4: {}
pixelmatch@7.1.0:
diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml
index cb57dc1e..73503599 100644
--- a/pnpm-workspace.yaml
+++ b/pnpm-workspace.yaml
@@ -8,16 +8,17 @@ onlyBuiltDependencies:
overrides:
"@isaacs/brace-expansion@<=5.0.0": ">=5.0.1"
ajv@>=7.0.0-alpha.0 <8.18.0: ">=8.18.0"
+ brace-expansion@>=2.0.0 <2.0.3: ">=2.0.3"
+ brace-expansion@>=4.0.0 <5.0.5: ">=5.0.5"
diff@>=6.0.0 <8.0.3: ">=8.0.3"
- flatted@<3.4.0: ">=3.4.0"
flatted@<=3.4.1: ">=3.4.2"
- lodash@>=4.0.0 <=4.17.22: ">=4.17.23"
- minimatch@<3.1.3: ">=3.1.3"
+ lodash-es@<=4.17.23: ">=4.18.0"
+ lodash@<=4.17.23: ">=4.18.0"
minimatch@<3.1.4: ">=3.1.4"
- minimatch@>=10.0.0 <10.2.1: ">=10.2.1"
- minimatch@>=10.0.0 <10.2.3: ">=10.2.3"
- minimatch@>=9.0.0 <9.0.6: ">=9.0.6"
minimatch@>=9.0.0 <9.0.7: ">=9.0.7"
+ minimatch@>=10.0.0 <10.2.3: ">=10.2.3"
+ picomatch@<2.3.2: ">=2.3.2"
+ picomatch@>=4.0.0 <4.0.4: ">=4.0.4"
rollup@>=4.0.0 <4.59.0: ">=4.59.0"
peerDependencyRules:
diff --git a/src/components/P5CanvasGuard.tsx b/src/components/P5CanvasGuard.tsx
index a6f50562..0e677d80 100644
--- a/src/components/P5CanvasGuard.tsx
+++ b/src/components/P5CanvasGuard.tsx
@@ -1,35 +1,38 @@
import * as React from "react";
+import P5CanvasWithSketch from "@components/P5CanvasWithSketch";
import { type P5CanvasProps } from "@contracts/P5CanvasProps";
-import { type P5CanvasPropsWithSketch } from "@contracts/P5CanvasPropsWithSketch";
-import { type SketchProps } from "@contracts/SketchProps";
import { logErrorBoundaryError } from "@utils/logErrorBoundaryError";
import { ReactNode } from "react";
import { FallbackProps } from "react-error-boundary";
-const P5CanvasWithSketch = React.lazy(
- () => import("@components/P5CanvasWithSketch")
-);
-
const ErrorBoundary = React.lazy(() =>
import("react-error-boundary").then(m => ({
default: m.ErrorBoundary
}))
);
-const P5CanvasGuard = (
- props: P5CanvasProps
-) => {
- if (props.sketch === undefined) {
+const P5CanvasGuard = (props: P5CanvasProps) => {
+ const {
+ sketch,
+ updater,
+ fallback,
+ loading,
+ error,
+ children,
+ ...sketchProps
+ } = props;
+
+ if (sketch === undefined) {
console.error("[P5Canvas] The `sketch` prop is required.");
- return props.fallback?.() ?? null;
+ return fallback?.() ?? null;
}
return (
{
return (
- props.error?.(info.error) ?? (
+ error?.(info.error) ?? (
❌ - Something went wrong
)
);
@@ -39,14 +42,15 @@ const P5CanvasGuard = (
}}
>
🚀 Loading...
- }
+ fallback={loading?.() ?? 🚀 Loading...
}
>
)}
- />
+ sketch={sketch}
+ updater={updater}
+ sketchProps={sketchProps}
+ >
+ {children}
+
);
diff --git a/src/components/P5CanvasWithSketch.tsx b/src/components/P5CanvasWithSketch.tsx
index 7916f198..e50805a1 100644
--- a/src/components/P5CanvasWithSketch.tsx
+++ b/src/components/P5CanvasWithSketch.tsx
@@ -2,28 +2,29 @@ import * as React from "react";
import { CanvasContainerClassName } from "@constants/CanvasContainerClassName";
import { type CanvasContainerRef } from "@contracts/CanvasContainerRef";
import { type P5CanvasInstanceRef } from "@contracts/P5CanvasInstanceRef";
-import { type P5CanvasPropsWithSketch } from "@contracts/P5CanvasPropsWithSketch";
+import { type Sketch } from "@contracts/Sketch";
import { type SketchProps } from "@contracts/SketchProps";
+import { type Updater } from "@contracts/Updater";
import { removeP5CanvasInstance } from "@utils/removeP5CanvasInstance";
import { updateP5CanvasInstance } from "@utils/updateP5CanvasInstance";
-import { withoutKeys } from "@utils/withoutKeys";
+import { type ReactNode } from "react";
-const P5CanvasWithSketch = (
- props: P5CanvasPropsWithSketch
-) => {
+interface P5CanvasWithSketchProps {
+ sketch: Sketch;
+ updater?: Updater;
+ sketchProps: SketchProps;
+ children?: ReactNode;
+}
+
+const P5CanvasWithSketch = (props: P5CanvasWithSketchProps) => {
const canvasContainerRef: CanvasContainerRef = React.useRef(null);
- const p5CanvasInstanceRef: P5CanvasInstanceRef = React.useRef(null);
- const sketchProps: SketchProps = React.useMemo(
- () =>
- withoutKeys(props, [
- "sketch",
- "fallback",
- "loading",
- "error",
- "children"
- ]),
- [props]
- );
+ const p5CanvasInstanceRef: P5CanvasInstanceRef =
+ React.useRef(null);
+ const updaterRef = React.useRef(props.updater);
+
+ React.useEffect(() => {
+ updaterRef.current = props.updater;
+ }, [props.updater]);
React.useEffect(() => {
p5CanvasInstanceRef.current = updateP5CanvasInstance(
@@ -35,10 +36,12 @@ const P5CanvasWithSketch = (
React.useEffect(() => {
/** @see https://github.com/P5-wrapper/react/discussions/360 */
- p5CanvasInstanceRef.current?.updateWithProps?.(
- sketchProps as unknown as Props
- );
- }, [sketchProps, canvasContainerRef, p5CanvasInstanceRef]);
+ p5CanvasInstanceRef.current?.updateWithProps?.(props.sketchProps);
+
+ if (updaterRef.current && p5CanvasInstanceRef.current) {
+ updaterRef.current(p5CanvasInstanceRef.current, props.sketchProps);
+ }
+ }, [props.sketchProps, canvasContainerRef, p5CanvasInstanceRef]);
React.useEffect(() => () => removeP5CanvasInstance(p5CanvasInstanceRef), []);
diff --git a/src/contracts/InputProps.ts b/src/contracts/P5CanvasInternalProps.ts
similarity index 50%
rename from src/contracts/InputProps.ts
rename to src/contracts/P5CanvasInternalProps.ts
index 03c53fa3..ffef7dc9 100644
--- a/src/contracts/InputProps.ts
+++ b/src/contracts/P5CanvasInternalProps.ts
@@ -1,11 +1,15 @@
import { type Sketch } from "@contracts/Sketch";
import { type SketchProps } from "@contracts/SketchProps";
+import { type Updater } from "@contracts/Updater";
import { type ReactNode } from "react";
-import { type FallbackProps } from "react-error-boundary";
-export type InputProps = Props & {
+export interface P5CanvasInternalProps<
+ Props extends SketchProps = SketchProps
+> {
sketch?: Sketch;
+ updater?: Updater;
fallback?: () => ReactNode;
loading?: () => ReactNode;
- error?: (error: FallbackProps["error"]) => ReactNode;
-};
+ error?: (error: unknown) => ReactNode;
+ children?: ReactNode;
+}
diff --git a/src/contracts/P5CanvasProps.ts b/src/contracts/P5CanvasProps.ts
index 3a0722db..684faef5 100644
--- a/src/contracts/P5CanvasProps.ts
+++ b/src/contracts/P5CanvasProps.ts
@@ -1,6 +1,5 @@
-import { type InputProps } from "@contracts/InputProps";
+import { type P5CanvasInternalProps } from "@contracts/P5CanvasInternalProps";
import { type SketchProps } from "@contracts/SketchProps";
-import { type WithChildren } from "@contracts/WithChildren";
export type P5CanvasProps =
- WithChildren>;
+ P5CanvasInternalProps & Props;
diff --git a/src/contracts/P5CanvasPropsWithSketch.ts b/src/contracts/P5CanvasPropsWithSketch.ts
deleted file mode 100644
index 8a75061a..00000000
--- a/src/contracts/P5CanvasPropsWithSketch.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-import { type P5CanvasProps } from "@contracts/P5CanvasProps";
-import { type Sketch } from "@contracts/Sketch";
-import { type SketchProps } from "@contracts/SketchProps";
-
-export type P5CanvasPropsWithSketch =
- P5CanvasProps & { sketch: Sketch };
diff --git a/src/contracts/Updater.ts b/src/contracts/Updater.ts
new file mode 100644
index 00000000..52f87e56
--- /dev/null
+++ b/src/contracts/Updater.ts
@@ -0,0 +1,7 @@
+import { type P5CanvasInstance } from "@contracts/P5CanvasInstance";
+import { type SketchProps } from "@contracts/SketchProps";
+
+export type Updater = (
+ instance: P5CanvasInstance,
+ props: Props
+) => void;
diff --git a/src/contracts/WithChildren.ts b/src/contracts/WithChildren.ts
deleted file mode 100644
index 73f11e3b..00000000
--- a/src/contracts/WithChildren.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-import { type ReactNode } from "react";
-
-export type WithChildren = T & { children?: ReactNode };
diff --git a/src/main.tsx b/src/main.tsx
index eb272f07..29c5c265 100644
--- a/src/main.tsx
+++ b/src/main.tsx
@@ -4,3 +4,4 @@ export { type P5CanvasInstance } from "@contracts/P5CanvasInstance";
export { type P5CanvasProps } from "@contracts/P5CanvasProps";
export { type Sketch } from "@contracts/Sketch";
export { type SketchProps } from "@contracts/SketchProps";
+export { type Updater } from "@contracts/Updater";
diff --git a/src/utils/logErrorBoundaryError.ts b/src/utils/logErrorBoundaryError.ts
index 8761309e..d155ba86 100644
--- a/src/utils/logErrorBoundaryError.ts
+++ b/src/utils/logErrorBoundaryError.ts
@@ -22,28 +22,28 @@ function createErrorMessage(error: unknown): string {
return `${error.name}("${error.message}")`;
}
- if (typeof error === "symbol" || error instanceof Symbol) {
+ if (typeof error === "symbol") {
return error.toString();
}
- if (typeof error === "string" || error instanceof String) {
- return `String("${error.toString()}")`;
+ if (typeof error === "string") {
+ return `String("${error}")`;
}
- if (typeof error === "number" || error instanceof Number) {
- return `Number(${error.toString()})`;
+ if (typeof error === "number") {
+ return `Number(${error})`;
}
- if (typeof error === "bigint" || error instanceof BigInt) {
- return `BigInt(${error.toString()})`;
+ if (typeof error === "bigint") {
+ return `BigInt(${error})`;
}
if (error instanceof Array) {
- return `Array(${JSON.stringify(error.values().toArray())})`;
+ return `Array(${JSON.stringify([...error])})`;
}
if (error instanceof Set) {
- return `Set(${JSON.stringify(error.values().toArray())})`;
+ return `Set(${JSON.stringify([...error])})`;
}
if (Object.getPrototypeOf(error) === Object.prototype) {
diff --git a/src/utils/withoutKeys.ts b/src/utils/withoutKeys.ts
deleted file mode 100644
index c3909430..00000000
--- a/src/utils/withoutKeys.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-export function withoutKeys(
- record: Record,
- keysToIgnore: string[]
-) {
- if (keysToIgnore.length === 0) {
- return record;
- }
-
- return Object.keys(record)
- .filter(key => !keysToIgnore.includes(key))
- .reduce>((accumulator, current) => {
- accumulator[current] = record[current];
-
- return accumulator;
- }, {});
-}
diff --git a/tests/components/P5Canvas.test.tsx b/tests/components/P5Canvas.test.tsx
index 7d6f70f0..faf65886 100644
--- a/tests/components/P5Canvas.test.tsx
+++ b/tests/components/P5Canvas.test.tsx
@@ -2,6 +2,7 @@ import { P5Canvas } from "@components/P5Canvas";
import { CanvasContainerClassName } from "@constants/CanvasContainerClassName";
import { type P5CanvasInstance } from "@contracts/P5CanvasInstance";
import { type Sketch } from "@contracts/Sketch";
+import { type Updater } from "@contracts/Updater";
import { render, RenderResult, waitFor } from "@testing-library/react";
import { renderToStaticMarkup, renderToString } from "react-dom/server";
import { assert, describe, expect, it, vi } from "vitest";
@@ -335,4 +336,78 @@ describe("P5Canvas", () => {
expect(updateFunction).toHaveBeenCalledWith({ y: 100 });
});
});
+
+ describe("Updater", () => {
+ it("Calls the `updater` with the p5 instance and sketch props when the component is mounted", async () => {
+ const updater = vi.fn();
+ const sketch = createSketch();
+
+ const { findByTestId } = render(
+
+ );
+
+ await waitForCanvas(findByTestId);
+
+ expect(updater).toHaveBeenCalledOnce();
+
+ const [instance, props] = updater.mock.calls[0];
+
+ expect(instance).toHaveProperty("setup");
+ expect(props).toEqual({ x: 100 });
+ });
+
+ it("Calls the `updater` with updated props when a prop value changes", async () => {
+ const updater = vi.fn();
+ const sketch = createSketch();
+ const { rerender, findByTestId } = render(
+
+ );
+
+ await waitForCanvas(findByTestId);
+
+ rerender();
+
+ expect(updater).toHaveBeenCalledTimes(2);
+
+ const [instance, props] = updater.mock.calls[1];
+
+ expect(instance).toHaveProperty("setup");
+ expect(props).toEqual({ x: 200 });
+ });
+
+ it("Calls the new `updater` when the updater reference changes", async () => {
+ const firstUpdater = vi.fn();
+ const secondUpdater = vi.fn();
+ const sketch = createSketch();
+ const { rerender, findByTestId } = render(
+
+ );
+
+ await waitForCanvas(findByTestId);
+
+ expect(firstUpdater).toHaveBeenCalledOnce();
+
+ rerender();
+
+ expect(secondUpdater).toHaveBeenCalledOnce();
+
+ const [, props] = secondUpdater.mock.calls[0];
+
+ expect(props).toEqual({ x: 100 });
+ });
+
+ it("Does not pass the `updater` to `updateWithProps`", async () => {
+ const updateFunction = vi.fn();
+ const updater = vi.fn();
+ const sketch = createSketch(updateFunction);
+
+ const { findByTestId } = render(
+
+ );
+
+ await waitForCanvas(findByTestId);
+
+ expect(updateFunction).toHaveBeenCalledWith({ x: 100 });
+ });
+ });
});
diff --git a/tests/utils/logErrorBoundaryError.test.ts b/tests/utils/logErrorBoundaryError.test.ts
index 4288e93f..158a3ba5 100644
--- a/tests/utils/logErrorBoundaryError.test.ts
+++ b/tests/utils/logErrorBoundaryError.test.ts
@@ -39,7 +39,7 @@ describe("logErrorBoundaryError", () => {
);
});
- it("Logs the error correctly when provided a `String` instance", () => {
+ it("Logs the error correctly when provided a string", () => {
logErrorBoundaryError("A string message");
expect(errorLoggerSpy).toHaveBeenCalledOnce();
@@ -53,7 +53,7 @@ describe("logErrorBoundaryError", () => {
);
});
- it("Logs the error correctly when provided a `Number` instance", () => {
+ it("Logs the error correctly when provided a number", () => {
logErrorBoundaryError(123);
expect(errorLoggerSpy).toHaveBeenCalledOnce();
@@ -67,7 +67,7 @@ describe("logErrorBoundaryError", () => {
);
});
- it("Logs the error correctly when provided a `BigInt` instance", () => {
+ it("Logs the error correctly when provided a bigint", () => {
logErrorBoundaryError(BigInt(123));
expect(errorLoggerSpy).toHaveBeenCalledOnce();
@@ -123,7 +123,7 @@ describe("logErrorBoundaryError", () => {
);
});
- it("Logs the error correctly when provided a `Symbol` instance", () => {
+ it("Logs the error correctly when provided a symbol", () => {
logErrorBoundaryError(Symbol("test"));
expect(errorLoggerSpy).toHaveBeenCalledOnce();
diff --git a/tests/utils/withoutKeys.test.ts b/tests/utils/withoutKeys.test.ts
deleted file mode 100644
index caa120f5..00000000
--- a/tests/utils/withoutKeys.test.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-import { withoutKeys } from "@utils/withoutKeys";
-import { describe, expect, it } from "vitest";
-
-describe("withoutKeys", () => {
- it("Returns the original object if the keys to ignore list is empty", () => {
- const object = { a: 1, b: 2 };
- const updated = withoutKeys(object, []);
-
- expect(object).toEqual(updated);
- });
-
- it("Removes all keys provided in the ignore list", () => {
- const object = { a: 1, b: 2, c: 3, d: 4 };
- const updated = withoutKeys(object, ["b", "d"]);
-
- expect(object).not.toEqual(updated);
- expect(Object.keys(updated)).toEqual(["a", "c"]);
- });
-});