I’m working on a project that integrates p5.js and React. My project consists of App.js and two child components: View1.js and View2.js. I want to pass information from View1 to View2 and have that changing information reflected in the sketch of View 2. My problem is that although I can pass data from View 1 to View 2, I don’t know how to update the sketch with the new value.
I think part of the problem might be caused by the fact that the sketch within the View 2 component is in instance mode, so once it’s initialized, it doesn’t change. Essentially, I’m looking for a way to reinitialize/refresh the sketch inside a component every time one of the component’s props changes in value so that the sketch is using the most recent value. I know p5-react-wrapper does have a way of dealing with this problem, but ideally, I'd like to do this with just the p5 library.
Does anyone know how to do that? I've included an example of what I want to do below (although it's a bit more complicated than this with my actual project).
App.js
import React, { useState } from 'react';
import View1 from "./components/View1.js";
import View2 from './components/View2.js';
function App() {
const [data, setData] = useState(0);
const firstViewToParent = (num) => {setData(num);}
return (
<div className="App">
<View1
firstViewToParent={firstViewToParent}
/>
<View2
dataFromSibling={data}
/>
</div>
);
}
export default App;
View1.js
import React, { useEffect } from "react";
import p5 from 'p5';
const View1 = props => {
const Sketch = (p) => {
p.setup = () => {
p.createCanvas(400, 400);
}
p.draw = () => {
p.background(0);
}
p.mouseClicked = () => {
// Passing information from the child up to the parent
props.firstViewToParent(p.mouseX);
}
}
useEffect(() => {
new p5(Sketch);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<></>
);
}
export default View1;
View2.js
import React, { useEffect } from "react";
import p5 from 'p5';
const View2 = props => {
const Sketch = (p) => {
p.setup = () => {
p.createCanvas(400, 400);
}
p.draw = () => {
p.background(255, 0, 0);
// Want to be able to access updated information from sibling
// component inside the sketch
p.print(props.dataFromSibling);
}
}
useEffect(() => {
new p5(Sketch);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<></>
);
}
export default View2;
useEffect
function calls, your View2
effect was not getting updated.props
object that is passed to your component function is never mutated. When props
change a new props
object is passed to a new call to your component function.Here's a working example:
// This is in place of an import statement
const { useRef, useState, useEffect } = React;
const View1 = props => {
const containerRef = useRef();
const Sketch = (p) => {
p.setup = () => {
p.print("View1 Initializing");
p.createCanvas(200, 100);
}
p.draw = () => {
p.background(150);
}
p.mouseClicked = () => {
p.print('click!');
// Passing information from the child up to the parent
props.firstViewToParent(p.map(p.mouseX, 0, p.width, 0, 255));
}
}
useEffect(
() => {
// make sure the p5.js canvas is a child of the component in the DOM
new p5(Sketch, containerRef.current);
},
// This empty list tells React that this effect never needs to get re-rendered!
[]
);
// Note: you're going to want to defined an element to contain the p5.js canvas
return (
<div ref={containerRef}></div>
);
}
const View2 = props => {
console.log('view2');
const containerRef = useRef();
const Sketch = (p) => {
p.setup = () => {
p.print("View2 Initializing");
p.createCanvas(200, 100);
}
p.draw = () => {
p.background(props.dataFromSibling);
}
}
useEffect(
() => {
let inst = new p5(Sketch, containerRef.current);
// Cleanup function! Without this the new p5.js sketches
// generated with each click will just appear one after the other.
return () => inst.remove();
},
// Let React know that this effect needs re-rendering when the dataFromSibling prop changes
[props.dataFromSibling]
);
return (
<div ref={containerRef}></div>
);
}
function App() {
const [data, setData] = useState(0);
const firstViewToParent = (num) => {
console.log('handling firstViewToParent callback');
setData(num);
};
return (
<div className="App">
<View1 firstViewToParent={firstViewToParent}/>
<View2 dataFromSibling={data} />
</div>
);
}
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
);
.App {
display: flex;
flex-direction: row;
}
<html>
<head>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.development.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/p5@1.4.0/lib/p5.js"></script>
</head>
<body>
<div id="root"></div>
</body>
</html>
So I was curious: What if you wanted to update an existing p5.js sketch that was created with useEffect()
without removing and recreating the entire p5.js sketch. Well, disclaimer: I am not a ReactJS expert, but I think the answer is yes. Basically you need to do the following:
useEffect
call that creates the sketch:
props
p.updateFn = ...
) that updates said local variableuseEffect
call that creates the sketch, if the sketch already exists, call said update function with the new value from props
.Kind of complicated I know, but here's an example:
// This is in place of an import statement
const { useRef, useState, useEffect } = React;
const View1 = props => {
const containerRef = useRef();
const Sketch = (p) => {
p.setup = () => {
p.print("View1 Initializing");
p.createCanvas(200, 100);
}
p.draw = () => {
p.background(150);
}
p.mouseClicked = (e) => {
// It turns out that p5.js listens for clicks anywhere on the page!
if (e.target.parentElement === containerRef.current) {
p.print('click!');
// Passing information from the child up to the parent
props.firstViewToParent(p.map(p.mouseX, 0, p.width, 0, 255));
}
}
}
useEffect(
() => {
// make sure the p5.js canvas is a child of the component in the DOM
let sketch = new p5(Sketch, containerRef.current);
return () => sketch.remove();
},
// This empty list tells React that this effect never needs to get re-rendered!
[]
);
// Note: you're going to want to defined an element to contain the p5.js canvas
return (
<div ref={containerRef}></div>
);
}
const View2 = props => {
console.log('view2');
const containerRef = useRef();
const [sketch, setSketch] = useState(undefined);
const Sketch = (p) => {
let bgColor = props.dataFromSibling;
p.setup = () => {
p.print("View2 Initializing");
p.createCanvas(200, 100);
}
p.draw = () => {
p.background(bgColor);
}
p.updateBackgroundColor = function(value) {
bgColor = value;
}
}
useEffect(
() => {
if (!sketch) {
// Initialize sketch
let inst = new p5(Sketch, containerRef.current);
setSketch(inst);
} else {
// Update sketch
sketch.updateBackgroundColor(props.dataFromSibling);
}
// We cannot return a cleanup function here, be cause it would run every time the dataFromSibling prop changed
},
// Let React know that this effect needs re-rendering when the dataFromSibling prop changes
[props.dataFromSibling]
);
useEffect(
() => {
// This effect is only responsible for cleaning up after the previous one 😅
return () => {
if (sketch) {
console.log('removing sketch!');
// Removing p5.js sketch because the component is un-mounting
sketch.remove();
}
};
},
// This effect needs to be re-initialized *after* the sketch gets created
[sketch]
);
return (
<div ref={containerRef}></div>
);
}
function App() {
const [data, setData] = useState(0);
const [showSketch, setShowSketch] = useState(true);
const firstViewToParent = (num) => {
console.log('handling firstViewToParent callback');
setData(num);
};
return (
<div className="App">
<View1 firstViewToParent={firstViewToParent}/>
{showSketch && <View2 dataFromSibling={data} />}
<button onClick={() => setShowSketch(showSketch ? false : true )}>
Toggle
</button>
</div>
);
}
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
);
.App {
display: flex;
flex-direction: row;
}
<html>
<head>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.development.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/p5@1.4.0/lib/p5.js"></script>
</head>
<body>
<div id="root"></div>
</body>
</html>
If this violates any ReactJS best practices or will be broken in some scenarios hopefully somebody who knows React better than me will chime in.
useLayoutEffect
I have recently learned that this sort of effect, which adds a new element to the DOM, should generally be initialized with useLayoutEffect
instead of useEffect
. This is because useEffect
runs in parallel with the update to the DOM and does not block rendering, whereas useLayoutEffect
does block rendering. So when you modify the DOM in useEffect
(by telling p5.js to create a canvas element) it may result in objects in the page flickering or suddenly moving from where they are initially positioned. Once I have more time to test this out I will update the code examples above.