diff --git a/README.md b/README.md index 4c151ac..bb6d06f 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ Then just open `http://localhost:3001` in a browser. ### Javascript -```js +```javascript import React from "react"; import { ReactP5Wrapper } from "react-p5-wrapper"; @@ -68,9 +68,9 @@ export function App() { } ``` -### Typescript +### TypeScript -Typescript sketches can be declared in two different ways, below you will find +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 `ReactP5Wrapper` component requires you to pass a `sketch` @@ -80,7 +80,7 @@ of type `P5Instance`, you are good to go! #### Option 1: Declaring a sketch using the `P5Instance` type -```ts +```typescript import React from "react"; import { ReactP5Wrapper, P5Instance } from "react-p5-wrapper"; @@ -116,7 +116,7 @@ that the `p5` argument passed to the sketch function is auto-typed as a > sketches and there is nothing wrong with using the `P5Instance` manually in a > regular `function` declaration. -```ts +```typescript import React from "react"; import { ReactP5Wrapper, Sketch } from "react-p5-wrapper"; @@ -140,9 +140,135 @@ export function App() { } ``` +#### 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. + +> Sidenote: +> +> We could also write the `MySketchProps` type as an interface to do exactly the +> same thing if that is to your personal preference: +> +> ```typescript +> interface MySketchProps extends SketchProps { +> rotation: number; +> } +> ``` + +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`. + +##### Usage with the `P5Instance` type + +```typescript +import React, { useState, useEffect } from "react"; +import { ReactP5Wrapper, P5Instance, SketchProps } from "react-p5-wrapper"; + +type MySketchProps = SketchProps & { + rotation: number; +}; + +function sketch(p5: P5Instance) { + let rotation = 0; + + p5.setup = () => p5.createCanvas(600, 400, p5.WEBGL); + + p5.updateWithProps = props => { + if (props.rotation) { + rotation = (props.rotation * Math.PI) / 180; + } + }; + + p5.draw = () => { + p5.background(100); + p5.normalMaterial(); + p5.noStroke(); + p5.push(); + p5.rotateY(rotation); + p5.box(100); + p5.pop(); + }; +} + +export function App() { + const [rotation, setRotation] = useState(0); + + useEffect(() => { + const interval = setInterval( + () => setRotation(rotation => rotation + 100), + 100 + ); + + return () => { + clearInterval(interval); + }; + }, []); + + return ; +} +``` + +##### Usage with the `Sketch` type + +```typescript +import React, { useState, useEffect } from "react"; +import { ReactP5Wrapper, Sketch, SketchProps } from "react-p5-wrapper"; + +type MySketchProps = SketchProps & { + rotation: number; +}; + +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; + } + }; + + p5.draw = () => { + p5.background(100); + p5.normalMaterial(); + p5.noStroke(); + p5.push(); + p5.rotateY(rotation); + p5.box(100); + p5.pop(); + }; +}; + +export function App() { + const [rotation, setRotation] = useState(0); + + useEffect(() => { + const interval = setInterval( + () => setRotation(rotation => rotation + 100), + 100 + ); + + return () => { + clearInterval(interval); + }; + }, []); + + return ; +} +``` + ### Using abstracted setup and draw functions -```js +```javascript import React from "react"; import { ReactP5Wrapper } from "react-p5-wrapper"; @@ -193,7 +319,7 @@ wrapper are changed, if it is set within your sketch. This way we can render our `ReactP5Wrapper` component and react to component prop changes directly within our sketches! -```js +```javascript import React, { useState, useEffect } from "react"; import { ReactP5Wrapper } from "react-p5-wrapper"; diff --git a/package-lock.json b/package-lock.json index 407e227..9af5641 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10142,9 +10142,9 @@ } }, "node_modules/minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", + "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", "dev": true }, "node_modules/minipass": { @@ -21316,9 +21316,9 @@ } }, "minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", + "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", "dev": true }, "minipass": { diff --git a/src/index.tsx b/src/index.tsx index 9151060..138125f 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,44 +1,48 @@ import diff from "microdiff"; import p5 from "p5"; -import React, { createRef, FC, memo, MutableRefObject, useRef } from "react"; +import React, { createRef, memo, MutableRefObject, useRef } from "react"; import { useIsomorphicEffect } from "rooks"; type Wrapper = HTMLDivElement; -export type Sketch = (instance: P5CanvasInstance) => void; -export type SketchProps = { - [key: string]: any; +type WithChildren = T & { children?: React.ReactNode }; +type InputProps = Props & { + sketch: Sketch; }; -export type P5WrapperProps = SketchProps & { - sketch: Sketch; -}; -export type P5CanvasInstance = p5 & { - updateWithProps?: (props: SketchProps) => void; +export type Sketch = ( + instance: P5CanvasInstance +) => void; +export type SketchProps = { [key: string]: unknown }; +export type P5WrapperProps = + WithChildren>; +export type P5CanvasInstance = p5 & { + updateWithProps?: (props: Props) => void; }; // @TODO: remove in next major version, keep for compatibility reasons for now. -export type P5Instance = P5CanvasInstance; +export type P5Instance = + P5CanvasInstance; -function createCanvasInstance( - sketch: Sketch, +function createCanvasInstance( + sketch: Sketch, wrapper: Wrapper -): P5CanvasInstance { +): P5CanvasInstance { return new p5(sketch, wrapper); } -function removeCanvasInstance( - canvasInstanceRef: MutableRefObject +function removeCanvasInstance( + canvasInstanceRef: MutableRefObject | undefined> ) { canvasInstanceRef.current?.remove(); canvasInstanceRef.current = undefined; } -const ReactP5WrapperComponent: FC = ({ +function ReactP5WrapperComponent({ sketch, children, ...props -}) => { +}: P5WrapperProps) { const wrapperRef = createRef(); - const canvasInstanceRef = useRef(); + const canvasInstanceRef = useRef>(); useIsomorphicEffect(() => { if (wrapperRef.current === null) { @@ -53,16 +57,33 @@ const ReactP5WrapperComponent: FC = ({ }, [sketch]); useIsomorphicEffect( - () => canvasInstanceRef.current?.updateWithProps?.(props), + /** + * The `as any` cast is begrudgingly required due to a known limitation of the TypeScript compiler as demonstrated in issues: + * + * - https://github.com/microsoft/TypeScript/issues/35858 + * - https://github.com/microsoft/TypeScript/issues/37670 + * + * Potentially this will be resolved by this PR once it is eventually merged: + * + * - https://github.com/microsoft/TypeScript/pull/42382 + * + * Either way, until a resolution is merged into the TypeScript compiler that addresses this issue, we need to use this workaround. + * We could also remove this if we manage find a reasonable, more fitting workaround of some sort to avoid casting in the first place. + * If a workaround / change of implementation comes to mind, please raise an issue on the repository or feel free to open a PR! + */ + () => canvasInstanceRef.current?.updateWithProps?.(props as any), [props] ); useIsomorphicEffect(() => () => removeCanvasInstance(canvasInstanceRef), []); return
{children}
; -}; +} -function propsAreEqual(previous: P5WrapperProps, next: P5WrapperProps) { +function propsAreEqual( + previous: P5WrapperProps, + next: P5WrapperProps +) { const differences = diff(previous, next); return differences.length === 0;