I'm using react-three-renderer (npm, github) for building a scene with three.js.
I'm having a problem that I've boiled down to an MVCE. Refs aren't updating in the sequence I expect them to. First, here's the main code to look at:
var React = require('react');
var React3 = require('react-three-renderer');
var THREE = require('three');
var ReactDOM = require('react-dom');
class Simple extends React.Component {
constructor(props, context) {
super(props, context);
// construct the position vector here, because if we use 'new' within render,
// React will think that things have changed when they have not.
this.cameraPosition = new THREE.Vector3(0, 0, 5);
this.state = {
shape: 'box'
};
this.toggleShape = this.toggleShape.bind(this);
}
toggleShape() {
if(this.state.shape === 'box') {
this.setState({ shape: 'circle' });
} else {
this.setState({ shape: 'box' });
}
}
renderShape() {
if(this.state.shape === 'box') {
return <mesh>
<boxGeometry
width={1}
height={1}
depth={1}
name='box'
ref={
(shape) => {
this.shape = shape;
console.log('box ref ' + shape);
}
}
/>
<meshBasicMaterial
color={0x00ff00}
/>
</mesh>;
} else {
return <mesh>
<circleGeometry
radius={2}
segments={50}
name='circle'
ref={
(shape) => {
this.shape = shape;
console.log('circle ref ' + shape);
}
}
/>
<meshBasicMaterial
color={0x0000ff}
/>
</mesh>
}
}
componentDidUpdate() {
console.log('componentDidUpdate: the active shape is ' + this.shape.name);
}
render() {
const width = window.innerWidth; // canvas width
const height = window.innerHeight; // canvas height
var position = new THREE.Vector3(0, 0, 10);
var scale = new THREE.Vector3(100,50,1);
var shape = this.renderShape();
return (<div>
<button onClick={this.toggleShape}>Toggle Shape</button>
<React3
mainCamera="camera"
width={width}
height={height}
onAnimate={this._onAnimate}>
<scene>
<perspectiveCamera
name="camera"
fov={75}
aspect={width / height}
near={0.1}
far={1000}
position={this.cameraPosition}/>
{shape}
</scene>
</React3>
</div>);
}
}
ReactDOM.render(<Simple/>, document.querySelector('.root-anchor'));
This renders a basic scene with a green box, a fork of the example on react-three-renderer's github landing page. The button on the top left toggles the shape in the scene to be a blue circle, and if clicked again, back to the green box. I'm doing some logging in the ref callbacks and in componentDidUpdate. Here's where the core of the problem I'm encountering occurs. After clicking the toggle button for the first time, I expect the ref for the shape to be pointing to the circle. But as you can see from the logging, in componentDidUpdate the ref is still pointing to the box:
componentDidUpdate: the active shape is box
Logging in lines after that reveals the ref callbacks are hit
box ref null [React calls null on the old ref to prevent memory leaks]
circle ref [object Object]
You can drop breakpoints in to verify and to inspect. I would expect these two things to happen before we enter componentDidUpdate, but as you can see, it's happening in reverse. Why is this? Is there an underlying issue in react-three-renderer (if so, can you diagnose it?), or am I misunderstanding React refs?
The MVCE is available in this github repository. Download it, run npm install, and open _dev/public/home.html.
Thanks in advance.
I checked the source in react-three-renderer. In lib/React3.jsx, there is a two phased render.
componentDidMount() {
this.react3Renderer = new React3Renderer();
this._render();
}
componentDidUpdate() {
this._render();
}
The _render method is the one that seems to loads the children - the mesh objects within Three.
_render() {
const canvas = this._canvas;
const propsToClone = { ...this.props };
delete propsToClone.canvasStyle;
this.react3Renderer.render(
<react3
{...propsToClone}
onRecreateCanvas={this._onRecreateCanvas}
>
{this.props.children}
</react3>, canvas);
}
The render method draws the canvas and does not populate the children or invoke Three.
render() {
const {
canvasKey,
} = this.state;
return (<canvas
ref={this._canvasRef}
key={canvasKey}
width={this.props.width}
height={this.props.height}
style={{
...this.props.canvasStyle,
width: this.props.width,
height: this.props.height,
}}
/>);
}
Summarizing, this is the sequence:
App Component render is called.
This renders react-three-renderer which draws out a canvas.
componentDidUpdate of App component is called.
componentDidUpdate of react-three-renderer is called.
This calls the _render method.
The _render method updates the canvas by passing props.children (mesh objects) to the Three library.
When the mesh objects are mounted, the respective refs are invoked.
This explains what you are observing in the console statements.
Related
I have a strange bug. I have a React app that is using fabricjs. I can render things onto the canvas just fine, but when I try to save the canvas as an image, a strange bug occurs: after canvas.toDataURL is called, the entire canvas goes blank until the user clicks on one of the elements on the canvas itself, after which all elements will reappear.
The url created by toDataURL works fine: downloading this file will produce the expected image with all of the elements rendered
Here is a fiddle that demonstrates this behavior:
https://jsfiddle.net/db1gc45p/22/
const {useState, useEffect, useRef, useCallback} = React;
const useFabric = (onChange) => {
const fabricRef = useRef();
const disposeRef = useRef();
return useCallback(
(node) => {
if (node) {
fabricRef.current = new fabric.Canvas(node);
if (onChange) {
disposeRef.current = onChange(fabricRef.current);
}
} else if (fabricRef.current) {
fabricRef.current.dispose();
if (disposeRef.current) {
disposeRef.current();
disposeRef.current = undefined;
}
}
},
[onChange]
);
};
function Example() {
const ref = useFabric((canvas) => {
canvas.add(new fabric.Text("hello"))
canvas.toDataURL()
});
return (
<div style={{ display: "inline-block", border: "solid" }}>
<canvas
ref={ref}
width={100}
height={100}
/>
</div>
);
}
ReactDOM.render( <Example />, document.getElementById('root') );
You can see the bug for yourself. By commenting/uncommenting line 30 canvas.toDataURL(), you can see that the canvas element stays visible when we don't call canvas.toDataURL()
However, even if we do call canvas.toDataURL(), we can get the element to re-appear by clicking on it on the canvas.
How can I prevent all of the elements from going invisible?
Fabric.js does some rendering updates asynchronously, similar issue, however you can force a synchronous update (after doing something that triggers the async update) by using canvas.renderAll().
Try
function Example() {
const ref = useFabric((canvas) => {
canvas.add(new fabric.Text("hello"))
canvas.renderAll()
canvas.toDataURL()
});
I am developing a React app on my Ubuntu 20.4 VM machine in a chrome Version 100.0.4896.127 (Official Build) (64-bit) browser.
For creating the polygons I use "h3-js". h3-js can only be added via npm as the the core library is transpiled from C using emscripten.
I want to implement the following: When I set a marker I would like to show the hexagon that are in the data array.
Setting the marker works fine. Also adding the data.
I do this the following way:
Setting the marker
<MapContainer
// center={[40.0151, -105.2921]}
center={[40.7579747, -73.9877313]}
// #ts-ignore
// onClick={addMarker}
zoom={15}
style={{ height: "90%" }}
// #ts-ignore
onZoomEnd={console.log}
>
<SetMarker />
<TileLayer
url="https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png"
maxZoom={19}
/>
{Object.entries(groups).map(([weight, polygons]) => {
const color = getColor(weight);
return (
<Polygon
key={weight}
positions={polygons}
pathOptions={{
...baseStyle,
fillColor: color,
color: color, // stroke color
weight: 0 // stroke weight
}}
/>
);
})}
</MapContainer>
The functional component <SetMarker />, sets my marker the following:
function SetMarker() {
console.log("set marker");
const [position, setPosition] = useState(null);
const map = useMapEvents({
click(e) {
setPosition(e.latlng);
}
});
// render hexagon
if (position !== null) {
data.pop(); // delete the last element that was added before the click
data.push([position.lat, position.lng]); // add new element
}
return position === null ? null : (
<div>
<Marker position={position}>
<Popup>You are here</Popup>
</Marker>
</div>
);
}
In the data array are all my coordinates stores that create my hexagons.
However, when clicking the map the marker is set, but the map does not re-render.
I have created the following app:
https://codesandbox.io/s/react-leaflet-map-with-marker-forked-0uexf6
Sorry that I do not use the inbuilt stackoverflow editor as h3-js has to be installed via npm and cannot added via a cdn.
How can I update my leaflet map and show the polygon on the location where the user clicked.
Any suggestions what I am doing wrong?
I appreciate your replies!
The component doesn't rerender because react is not tracking the data that you're importing. You can use that data to initialize a react state and use it to render your hexagons.
const [mapData, setMapData] = useState(data);
[...mapData].forEach((point) => {
and add a new item to that state every time the position changes inside your SetMarker component
function SetMarker({ setMapData }) {
...
useEffect(() => {
if (position !== null) {
setMapData((prevMapData) => [
...prevMapData,
[position.lat, position.lng]
]);
}
}, [position, setMapData]);
// render hexagon
...
}
and don't forget to pass the setter in the component props
<SetMarker setMapData={setMapData} />
You can see it working here https://codesandbox.io/s/react-leaflet-map-with-marker-answer-ycm2u0?file=/Map.js
As an additional note, your code generally does not follow react's good practices about immutability. I'd recommend you revisit the official documentation and adjust your code accordingly. Every time you use an imperative js function like pop, push, or set is a red flag. Personally, learning redux helped me to understand more immutability; Dan's courses in egghead are a good start https://egghead.io/q/resources-by-dan-abramov
I have a canvas (made in p5.js). When you first load the page, the canvas is drawn in the background using the full window width. But i have a button that reveals my projects. The button increases the window height. The canvas does not update to draw on the extra added height. The website without the canvas is live at (https://umr.now.sh/).
My Questions:
How would i go about getting the compnentdidmount to "refresh" or re-draw the canvas to cover the new window size.
Do i even need to worry about componentdidmount or does p5.js have some sort of function that can help with the problem
//homepage.js
class Index extends React.Component{
constructor(){
super()
this.state = {
isLoading: true
}
this.isLoaded = this.isLoaded.bind(this)
}
isLoaded() {
console.log("Clicked")
this.setState({
isLoading: !this.state.isLoading,
})
}
render(){
let view;
if (this.state.isLoading){
view = <div> <Home isLoading={this.state.isLoading} isLoaded={this.isLoaded}/></div>
} else {
view = <div> <Project isLoading={this.state.isLoading} isLoaded={this.isLoaded}/></div>
}
return(
<div className="Homepage">
<div className="Name-social">
{view}
</div>
<div>
<SketchLayOut />
</div>
</div>
)
}
}
export default Index
I don't know anything about p5.js, but there is an event-listener in JS where you can listen for changes in window size:
window.addEventListener('resize', handleResize);
This code should be in componentDidMount. In your handleResize function that you provide, you can add logic that updates the size of the canvas based on window.innerWidth and window.innerHeight. Something like:
function handleResize() {
//you would pass these values to your canvas to update the width/height
this.setState({
width: window.innerWidth,
height: window.innerHeight
});
//etc
}
I'm not sure about the internals of p5.js or other parts of your code, so you will need to find out to update the canvas. This will at least get the width/height values you need, however. You should probably also make sure to remove the listener when the component unmounts (so that would be in componentWillUnmount)
In the project's repo the react-redux app is set with CSS in JS files. Now I'm supposed to animate pictures with mouse hover just like this website : https://www.madwell.com/
The component was initially a functional component and I have changed that to a class based component as follows:
```
class BannerContainer extends React.Component{
constructor(props){
super(props);
this.state = {
x: 0,
y: 0
};
this.handleMouseMove = this.handleMouseMove.bind(this);
}
componentDidMount(){
window.addEventListener("mousemove", this.handleMouseMove);
}
componentWillUnmount(){
window.removeEventListener('mousemove', this.handleMouseMove);
}
handleMouseMove({ x, y }){
this.setState({
x : x / window.innerWidth,
y : y / window.innerHeight
})
}
render(){
const { banner = {} } = this.props;
const {
title = ' <br> ',
text = ' <br> ',
image__google_350x80 = '',
image__app_350x80 = '',
image__bg1_1166x1878 = '',
image__bg2_961x1782 = '',
image__curve_730x151 = ''
} = banner;
return (
<Container>
<BGPatch>
{console.log(this.state.x , this.state.y)}
<img src={'/images/bg_purple.jpg'} alt="" />
</BGPatch>
```
In this example I am able to get listen to the mouse-move event and and get the x & y coordinates accordingly. But now I have to use react-spring library to implement it so how do I do that? Also the CSS should be written in separate JS file for each component where as in react-spring example they directly modify the opacity or trasform in the Spring component directly as well which is what I don't want
the spring component example given on their docs
<Spring from={{ opacity: 0 }} to={{ opacity: 1 }}>
{props => <div style={props}>hello</div>}
</Spring>
Parallaxing effects are relatively easy to do if you have mouse coordinates, here's an example with a similar effect: https://twitter.com/0xca0a/status/1058505720574959616
It uses hooks, but the same thing applies to a regular spring. Though this is probably a good example even, because it doesn't re-render the component on mousemove, which is very fast. You wouldn't use rotates in your case, translates i guess, but the point is, you receive x/y and use it to interpolate your images into position.
EDIT:
I've forked the example, this version uses translates: https://codesandbox.io/embed/r5x34869vq
I've got a problem using React JS and canvas together.
everything seems to work smooth, but when render method is called due to changes in other part of my app canvas flicker. It seems that react is re-appending it into the DOM (maybe because I'm playing an animation on it and its drawn frame is different each time).
This is my render method:
render: function() {
var canvasStyle = {
width: this.props.width
};
return (
<canvas id="interactiveCanvas" style={canvasStyle}/>
);
}
As a temporal solution I append my canvas tag manually, without reactjs:
render: function() {
var canvasStyle = {
width: this.props.width
};
return (
<div id="interactivediv3D">
</div>
);
}
And then:
componentDidMount: function() {
var canvasStyle = {
width: this.props.width
};
var el = document.getElementById('interactivediv3D');
el.innerHTML = el.innerHTML +
'<canvas id="interactive3D" style={'+canvasStyle+'}/>';
},
And it works right. But I think this is not a clean way and it's better to render this canvas tag using reactjs.
Any idea of how can I solve the problem? or at least avoiding reactjs re-rendering this canvas when only the drawn frame changed.
I spent time observing with your advice in mind and I find out this flickering was caused by the fact that I was accessing the canvas DOM element and changing canvas.width and canvas.height every time onComponentDidUpdate was called.
I needed to change this property manually because while rendering I don't know canvas.width yet. I fill style-width with "100%" and after rendering I look at "canvas.clientWidth" to extract the value in pixels.
As explained here: https://facebook.github.io/react/tips/dom-event-listeners.html I attached resize event and now I'm changing canvas properties only from my resizeHandler.
Re-rendering the entire tree shouldn't cause your canvas to flicker. But if you think that re-rendering is causing this issue, you could use shouldComponentUpdate lifecycle function and return false whenever you don't want to to update.
https://facebook.github.io/react/docs/component-specs.html#updating-shouldcomponentupdate
So for example:
shouldComponentUpdate: function(nextProps, nextState) {
return false;
}
Any component with the above function defined will not ever run render more than once (at component mounting).
However I've not been able to replicate the flickering issue you complain of. Is your entire tree rendering too often perhaps? In which case the above will fix your canvas, but you should take a look at how many times and how often you are re-rendering your entire tree and fix any issues there.
I had the same problem. I used react-konva library and my component structure looked something like this: canvas had a background image <SelectedImage /> which flicked every time any state property changed.
Initial code
import React, { useState } from "react";
import { Stage, Layer, Image } from "react-konva";
import useImage from "use-image";
function SomeComponent() {
const [imgWidth, setImgWidth] = useState(0);
const [imgHeight, setImgHeight] = useState(0);
//...and other state properties
const SelectedImage = () => {
const [image] = useImage(imgSRC);
if (imgWidth && imgHeight) {
return <Image image={image} width={imgWidth} height={imgHeight} />;
}
return null;
};
return (
<div>
<Stage width={imgWidth} height={imgHeight}>
<Layer>
<SelectedImage />
{/* some other canvas components*/}
</Layer>
</Stage>
</div>
);
}
export default SomeComponent;
The solution
The problem was in a wrong structure of SomeComponent where one component was initialized inside another. In my case it was <SelectedImage /> inside of SomeComponent. I took it out to a separate file and flickering disappeared!
Here is a correct structure
SelectedImage.js
import React from "react";
import { Image } from "react-konva";
import useImage from "use-image";
function SelectedImage({ width, height, imgSRC }) {
const [image] = useImage(imgSRC);
if (width && height) {
return <Image image={image} width={width} height={height} />;
}
return null;
}
export default SelectedImage;
SomeComponent.js
import React, { useState } from "react";
import { Stage, Layer, Image } from "react-konva";
import SelectedImage from "./SelectedImage";
function SomeComponent() {
const [imgWidth, setImgWidth] = useState(0);
const [imgHeight, setImgHeight] = useState(0);
//...and other state properties
return (
<div>
<Stage width={imgWidth} height={imgHeight}>
<Layer>
<SelectedImage width={imgWidth} height={imgHeight} imgSRC={imgSRC} />
{/* some other canvas components*/}
</Layer>
</Stage>
</div>
);
}
export default SomeComponent;