Skip to content

Commit 2c6770a

Browse files
authored
Add generic prop type support (#152)
1 parent f6eb46e commit 2c6770a

File tree

3 files changed

+181
-34
lines changed

3 files changed

+181
-34
lines changed

README.md

Lines changed: 133 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ Then just open `http://localhost:3001` in a browser.
4444

4545
### Javascript
4646

47-
```js
47+
```javascript
4848
import React from "react";
4949
import { ReactP5Wrapper } from "react-p5-wrapper";
5050

@@ -68,9 +68,9 @@ export function App() {
6868
}
6969
```
7070

71-
### Typescript
71+
### TypeScript
7272

73-
Typescript sketches can be declared in two different ways, below you will find
73+
TypeScript sketches can be declared in two different ways, below you will find
7474
two ways to declare a sketch, both examples do the exact same thing.
7575

7676
In short though, the `ReactP5Wrapper` component requires you to pass a `sketch`
@@ -80,7 +80,7 @@ of type `P5Instance`, you are good to go!
8080

8181
#### Option 1: Declaring a sketch using the `P5Instance` type
8282

83-
```ts
83+
```typescript
8484
import React from "react";
8585
import { ReactP5Wrapper, P5Instance } from "react-p5-wrapper";
8686

@@ -116,7 +116,7 @@ that the `p5` argument passed to the sketch function is auto-typed as a
116116
> sketches and there is nothing wrong with using the `P5Instance` manually in a
117117
> regular `function` declaration.
118118
119-
```ts
119+
```typescript
120120
import React from "react";
121121
import { ReactP5Wrapper, Sketch } from "react-p5-wrapper";
122122

@@ -140,9 +140,135 @@ export function App() {
140140
}
141141
```
142142

143+
#### TypeScript Generics
144+
145+
We also support the use of Generics to add type definitions for your props. If
146+
used, the props will be properly typed when the props are passed to the
147+
`updateWithProps` method.
148+
149+
To utilise generics you can use one of two methods. In both of the examples
150+
below, we create a custom internal type called `MySketchProps` which is a union
151+
type of `SketchProps` and a custom type which has a `rotation` key applied to
152+
it.
153+
154+
> Sidenote:
155+
>
156+
> We could also write the `MySketchProps` type as an interface to do exactly the
157+
> same thing if that is to your personal preference:
158+
>
159+
> ```typescript
160+
> interface MySketchProps extends SketchProps {
161+
> rotation: number;
162+
> }
163+
> ```
164+
165+
This means, in these examples, that when the `rotation` prop that is provided as
166+
part of the `props` passed to the `updateWithProps` function, it will be
167+
correctly typed as a `number`.
168+
169+
##### Usage with the `P5Instance` type
170+
171+
```typescript
172+
import React, { useState, useEffect } from "react";
173+
import { ReactP5Wrapper, P5Instance, SketchProps } from "react-p5-wrapper";
174+
175+
type MySketchProps = SketchProps & {
176+
rotation: number;
177+
};
178+
179+
function sketch(p5: P5Instance<MySketchProps>) {
180+
let rotation = 0;
181+
182+
p5.setup = () => p5.createCanvas(600, 400, p5.WEBGL);
183+
184+
p5.updateWithProps = props => {
185+
if (props.rotation) {
186+
rotation = (props.rotation * Math.PI) / 180;
187+
}
188+
};
189+
190+
p5.draw = () => {
191+
p5.background(100);
192+
p5.normalMaterial();
193+
p5.noStroke();
194+
p5.push();
195+
p5.rotateY(rotation);
196+
p5.box(100);
197+
p5.pop();
198+
};
199+
}
200+
201+
export function App() {
202+
const [rotation, setRotation] = useState(0);
203+
204+
useEffect(() => {
205+
const interval = setInterval(
206+
() => setRotation(rotation => rotation + 100),
207+
100
208+
);
209+
210+
return () => {
211+
clearInterval(interval);
212+
};
213+
}, []);
214+
215+
return <ReactP5Wrapper sketch={sketch} rotation={rotation} />;
216+
}
217+
```
218+
219+
##### Usage with the `Sketch` type
220+
221+
```typescript
222+
import React, { useState, useEffect } from "react";
223+
import { ReactP5Wrapper, Sketch, SketchProps } from "react-p5-wrapper";
224+
225+
type MySketchProps = SketchProps & {
226+
rotation: number;
227+
};
228+
229+
const sketch: Sketch<MySketchProps> = p5 => {
230+
let rotation = 0;
231+
232+
p5.setup = () => p5.createCanvas(600, 400, p5.WEBGL);
233+
234+
p5.updateWithProps = props => {
235+
if (props.rotation) {
236+
rotation = (props.rotation * Math.PI) / 180;
237+
}
238+
};
239+
240+
p5.draw = () => {
241+
p5.background(100);
242+
p5.normalMaterial();
243+
p5.noStroke();
244+
p5.push();
245+
p5.rotateY(rotation);
246+
p5.box(100);
247+
p5.pop();
248+
};
249+
};
250+
251+
export function App() {
252+
const [rotation, setRotation] = useState(0);
253+
254+
useEffect(() => {
255+
const interval = setInterval(
256+
() => setRotation(rotation => rotation + 100),
257+
100
258+
);
259+
260+
return () => {
261+
clearInterval(interval);
262+
};
263+
}, []);
264+
265+
return <ReactP5Wrapper sketch={sketch} rotation={rotation} />;
266+
}
267+
```
268+
143269
### Using abstracted setup and draw functions
144270

145-
```js
271+
```javascript
146272
import React from "react";
147273
import { ReactP5Wrapper } from "react-p5-wrapper";
148274

@@ -193,7 +319,7 @@ wrapper are changed, if it is set within your sketch. This way we can render our
193319
`ReactP5Wrapper` component and react to component prop changes directly within
194320
our sketches!
195321

196-
```js
322+
```javascript
197323
import React, { useState, useEffect } from "react";
198324
import { ReactP5Wrapper } from "react-p5-wrapper";
199325

package-lock.json

Lines changed: 6 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/index.tsx

Lines changed: 42 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,48 @@
11
import diff from "microdiff";
22
import p5 from "p5";
3-
import React, { createRef, FC, memo, MutableRefObject, useRef } from "react";
3+
import React, { createRef, memo, MutableRefObject, useRef } from "react";
44
import { useIsomorphicEffect } from "rooks";
55

66
type Wrapper = HTMLDivElement;
7-
export type Sketch = (instance: P5CanvasInstance) => void;
8-
export type SketchProps = {
9-
[key: string]: any;
7+
type WithChildren<T = unknown> = T & { children?: React.ReactNode };
8+
type InputProps<Props extends SketchProps = SketchProps> = Props & {
9+
sketch: Sketch<Props>;
1010
};
11-
export type P5WrapperProps = SketchProps & {
12-
sketch: Sketch;
13-
};
14-
export type P5CanvasInstance = p5 & {
15-
updateWithProps?: (props: SketchProps) => void;
11+
export type Sketch<Props extends SketchProps = SketchProps> = (
12+
instance: P5CanvasInstance<Props>
13+
) => void;
14+
export type SketchProps = { [key: string]: unknown };
15+
export type P5WrapperProps<Props extends SketchProps = SketchProps> =
16+
WithChildren<InputProps<Props>>;
17+
export type P5CanvasInstance<Props extends SketchProps = SketchProps> = p5 & {
18+
updateWithProps?: (props: Props) => void;
1619
};
1720

1821
// @TODO: remove in next major version, keep for compatibility reasons for now.
19-
export type P5Instance = P5CanvasInstance;
22+
export type P5Instance<Props extends SketchProps = SketchProps> =
23+
P5CanvasInstance<Props>;
2024

21-
function createCanvasInstance(
22-
sketch: Sketch,
25+
function createCanvasInstance<Props extends SketchProps = SketchProps>(
26+
sketch: Sketch<Props>,
2327
wrapper: Wrapper
24-
): P5CanvasInstance {
28+
): P5CanvasInstance<Props> {
2529
return new p5(sketch, wrapper);
2630
}
2731

28-
function removeCanvasInstance(
29-
canvasInstanceRef: MutableRefObject<P5CanvasInstance | undefined>
32+
function removeCanvasInstance<Props extends SketchProps = SketchProps>(
33+
canvasInstanceRef: MutableRefObject<P5CanvasInstance<Props> | undefined>
3034
) {
3135
canvasInstanceRef.current?.remove();
3236
canvasInstanceRef.current = undefined;
3337
}
3438

35-
const ReactP5WrapperComponent: FC<P5WrapperProps> = ({
39+
function ReactP5WrapperComponent<Props extends SketchProps = SketchProps>({
3640
sketch,
3741
children,
3842
...props
39-
}) => {
43+
}: P5WrapperProps<Props>) {
4044
const wrapperRef = createRef<Wrapper>();
41-
const canvasInstanceRef = useRef<P5CanvasInstance>();
45+
const canvasInstanceRef = useRef<P5CanvasInstance<Props>>();
4246

4347
useIsomorphicEffect(() => {
4448
if (wrapperRef.current === null) {
@@ -53,16 +57,33 @@ const ReactP5WrapperComponent: FC<P5WrapperProps> = ({
5357
}, [sketch]);
5458

5559
useIsomorphicEffect(
56-
() => canvasInstanceRef.current?.updateWithProps?.(props),
60+
/**
61+
* The `as any` cast is begrudgingly required due to a known limitation of the TypeScript compiler as demonstrated in issues:
62+
*
63+
* - https://github.com/microsoft/TypeScript/issues/35858
64+
* - https://github.com/microsoft/TypeScript/issues/37670
65+
*
66+
* Potentially this will be resolved by this PR once it is eventually merged:
67+
*
68+
* - https://github.com/microsoft/TypeScript/pull/42382
69+
*
70+
* Either way, until a resolution is merged into the TypeScript compiler that addresses this issue, we need to use this workaround.
71+
* We could also remove this if we manage find a reasonable, more fitting workaround of some sort to avoid casting in the first place.
72+
* If a workaround / change of implementation comes to mind, please raise an issue on the repository or feel free to open a PR!
73+
*/
74+
() => canvasInstanceRef.current?.updateWithProps?.(props as any),
5775
[props]
5876
);
5977

6078
useIsomorphicEffect(() => () => removeCanvasInstance(canvasInstanceRef), []);
6179

6280
return <div ref={wrapperRef}>{children}</div>;
63-
};
81+
}
6482

65-
function propsAreEqual(previous: P5WrapperProps, next: P5WrapperProps) {
83+
function propsAreEqual<Props extends SketchProps = SketchProps>(
84+
previous: P5WrapperProps<Props>,
85+
next: P5WrapperProps<Props>
86+
) {
6687
const differences = diff(previous, next);
6788

6889
return differences.length === 0;

0 commit comments

Comments
 (0)