Skip to content

Commit fa6a299

Browse files
committed
Add generic prop type support
1 parent 6b53fc9 commit fa6a299

File tree

3 files changed

+190
-46
lines changed

3 files changed

+190
-46
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 exampled
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<MySketchProps>(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: 12 additions & 12 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/index.tsx

Lines changed: 45 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,58 +1,76 @@
11
import diff from "microdiff";
22
import p5 from "p5";
3-
import React, { createRef, FC, memo, useState } from "react";
3+
import React, { createRef, memo, useState } from "react";
44
import { useIsomorphicEffect } from "rooks";
55

66
type Wrapper = HTMLDivElement;
7+
type WithChildren<T = unknown> = T & { children?: React.ReactNode };
8+
type InputProps<Props extends SketchProps = SketchProps> = Props & {
9+
sketch: Sketch<Props>;
10+
};
11+
export type Sketch<Props extends SketchProps = SketchProps> = (
12+
instance: P5Instance<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 P5Instance<Props extends SketchProps = SketchProps> = p5 & {
18+
updateWithProps?: (props: Props) => void;
19+
};
720

8-
export interface SketchProps {
9-
[key: string]: any;
10-
}
11-
12-
export interface Sketch {
13-
(instance: P5Instance): void;
14-
}
15-
16-
export interface P5WrapperProps extends SketchProps {
17-
sketch: Sketch;
18-
}
19-
20-
export interface P5Instance extends p5 {
21-
updateWithProps?: (props: SketchProps) => void;
22-
}
23-
24-
function createCanvas(sketch: Sketch, wrapper: Wrapper): P5Instance {
21+
function createCanvas<Props extends SketchProps = SketchProps>(
22+
sketch: Sketch<Props>,
23+
wrapper: Wrapper
24+
): P5Instance<Props> {
2525
return new p5(sketch, wrapper);
2626
}
2727

28-
const ReactP5WrapperComponent: FC<P5WrapperProps> = ({
28+
function ReactP5WrapperComponent<Props extends SketchProps = SketchProps>({
2929
sketch,
3030
children,
3131
...props
32-
}) => {
32+
}: P5WrapperProps<Props>) {
3333
const wrapperRef = createRef<Wrapper>();
34-
const [instance, setInstance] = useState<P5Instance>();
34+
const [instance, setInstance] = useState<P5Instance<Props>>();
3535

3636
useIsomorphicEffect(() => {
3737
if (wrapperRef.current === null) {
3838
return;
3939
}
4040

4141
instance?.remove();
42-
const canvas = createCanvas(sketch, wrapperRef.current);
42+
const canvas = createCanvas<Props>(sketch, wrapperRef.current);
4343
setInstance(canvas);
4444
}, [sketch]);
4545

46-
useIsomorphicEffect(() => {
47-
instance?.updateWithProps?.(props);
48-
}, [props, instance]);
46+
useIsomorphicEffect(
47+
/**
48+
* The `as any` cast is begrudgingly required due to a known limitation of the TypeScript compiler as demonstrated in issues:
49+
*
50+
* - https://github.com/microsoft/TypeScript/issues/35858
51+
* - https://github.com/microsoft/TypeScript/issues/37670
52+
*
53+
* Potentially this will be resolved by this PR once it is eventually merged:
54+
*
55+
* - https://github.com/microsoft/TypeScript/pull/42382
56+
*
57+
* Etiher way, until a resolution is merged into the TypeScript compiler that addresses this isssue, we need to use this workaround.
58+
* We could also remove this if we manage find a reasonable, more fitting workaround of some sort to avoid casting in the first place.
59+
* If a workaround / change of implementation comes to mind, please raise an issue on the repository or feel free to open a PR!
60+
*/
61+
() => instance?.updateWithProps?.(props as any),
62+
[props, instance]
63+
);
4964

5065
useIsomorphicEffect(() => () => instance?.remove(), []);
5166

5267
return <div ref={wrapperRef}>{children}</div>;
53-
};
68+
}
5469

55-
function propsAreEqual(previous: P5WrapperProps, next: P5WrapperProps) {
70+
function propsAreEqual<Props extends SketchProps = SketchProps>(
71+
previous: P5WrapperProps<Props>,
72+
next: P5WrapperProps<Props>
73+
) {
5674
const differences = diff(previous, next);
5775

5876
return differences.length === 0;

0 commit comments

Comments
 (0)