Skip to content

Commit

Permalink
Add generic prop type support (#152)
Browse files Browse the repository at this point in the history
  • Loading branch information
jamesrweb authored Apr 3, 2022
1 parent f6eb46e commit 2c6770a
Show file tree
Hide file tree
Showing 3 changed files with 181 additions and 34 deletions.
140 changes: 133 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand All @@ -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`
Expand All @@ -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";

Expand Down Expand Up @@ -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";

Expand All @@ -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<MySketchProps>) {
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 <ReactP5Wrapper sketch={sketch} rotation={rotation} />;
}
```
##### 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<MySketchProps> = 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 <ReactP5Wrapper sketch={sketch} rotation={rotation} />;
}
```

### Using abstracted setup and draw functions

```js
```javascript
import React from "react";
import { ReactP5Wrapper } from "react-p5-wrapper";

Expand Down Expand Up @@ -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";

Expand Down
12 changes: 6 additions & 6 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

63 changes: 42 additions & 21 deletions src/index.tsx
Original file line number Diff line number Diff line change
@@ -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 = unknown> = T & { children?: React.ReactNode };
type InputProps<Props extends SketchProps = SketchProps> = Props & {
sketch: Sketch<Props>;
};
export type P5WrapperProps = SketchProps & {
sketch: Sketch;
};
export type P5CanvasInstance = p5 & {
updateWithProps?: (props: SketchProps) => void;
export type Sketch<Props extends SketchProps = SketchProps> = (
instance: P5CanvasInstance<Props>
) => void;
export type SketchProps = { [key: string]: unknown };
export type P5WrapperProps<Props extends SketchProps = SketchProps> =
WithChildren<InputProps<Props>>;
export type P5CanvasInstance<Props extends SketchProps = SketchProps> = p5 & {
updateWithProps?: (props: Props) => void;
};

// @TODO: remove in next major version, keep for compatibility reasons for now.
export type P5Instance = P5CanvasInstance;
export type P5Instance<Props extends SketchProps = SketchProps> =
P5CanvasInstance<Props>;

function createCanvasInstance(
sketch: Sketch,
function createCanvasInstance<Props extends SketchProps = SketchProps>(
sketch: Sketch<Props>,
wrapper: Wrapper
): P5CanvasInstance {
): P5CanvasInstance<Props> {
return new p5(sketch, wrapper);
}

function removeCanvasInstance(
canvasInstanceRef: MutableRefObject<P5CanvasInstance | undefined>
function removeCanvasInstance<Props extends SketchProps = SketchProps>(
canvasInstanceRef: MutableRefObject<P5CanvasInstance<Props> | undefined>
) {
canvasInstanceRef.current?.remove();
canvasInstanceRef.current = undefined;
}

const ReactP5WrapperComponent: FC<P5WrapperProps> = ({
function ReactP5WrapperComponent<Props extends SketchProps = SketchProps>({
sketch,
children,
...props
}) => {
}: P5WrapperProps<Props>) {
const wrapperRef = createRef<Wrapper>();
const canvasInstanceRef = useRef<P5CanvasInstance>();
const canvasInstanceRef = useRef<P5CanvasInstance<Props>>();

useIsomorphicEffect(() => {
if (wrapperRef.current === null) {
Expand All @@ -53,16 +57,33 @@ const ReactP5WrapperComponent: FC<P5WrapperProps> = ({
}, [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 <div ref={wrapperRef}>{children}</div>;
};
}

function propsAreEqual(previous: P5WrapperProps, next: P5WrapperProps) {
function propsAreEqual<Props extends SketchProps = SketchProps>(
previous: P5WrapperProps<Props>,
next: P5WrapperProps<Props>
) {
const differences = diff(previous, next);

return differences.length === 0;
Expand Down

0 comments on commit 2c6770a

Please sign in to comment.