How to properly use Pixi.js / Canvas Drawing with Next.js / React? - reactjs

Context
I like to use pixi.js for a game I have in mind. Over the years I always used React and next.js.
So what I want to do it use pixi.js inside React / Next.
There are 2 packages out there that integrate into react fiber to render component like but they are outdated and give so many errors to me (for example doesn't work with react 18 and also have poor documentations). (#inlet/react-pixi and react-pixi-fiber)
So I decided to go the useRef route.
Here is some code that works fine:
import { useRef, useEffect } from "react";
import { Application, Sprite, Texture } from "pixi.js";
import bunnyImg from "../public/negx.jpg";
const app = new Application({
width: 800,
height: 600,
backgroundColor: 0x5bba6f,
});
const Pixi = () => {
const ref = useRef();
useEffect(() => {
// On first render add app to DOM
ref.current.appendChild(app.view);
// Start the PixiJS app
app.start();
const texture = Texture.from(bunnyImg.src);
const bunny = new Sprite(texture);
bunny.anchor.set(0.5);
bunny.x = 0;
bunny.y = 0;
bunny.width = 100;
bunny.height = 100;
app.stage.addChild(bunny);
return () => {
// On unload stop the application
app.stop();
};
}, []);
return <div ref={ref} />;
};
export default Pixi;
The Problem
The only problem I have with this is, that hot reload doesn't work. So if I change something in the useEffect hook, I have to go into the browser and manually refresh the page. So basically hot reloading doesn't work.
I think since it uses a ref that basically never changes.
The question
Is there a better way of coding pixi.js inside react / next?

It's probably unrelated to react-pixi. It looks like the default behavior of hot reloading as described on the Github repo.
Hooks would be auto updated on HMR if they should be. There is only one condition for it - a non zero dependencies list.
and
❄️ useEffect(effect, []); // "on mount" hook. "Not changing the past"
The code in the question is using such an empty dependency list, so the behavior is as expected.
Please post the full setup including the hot reload configuration if this is not the issue.

If you want Pixi/Canvas gets auto refreshed on the rendering cycle, you probably need to define those values with react states and add those states to the useEffect dependencies
For example:
const [bunnyProps, setBunnyProps] = React.useState({
anchor: 0.5,
x: 0,
y: 0,
width: 100,
height: 100,
})
React.useEffect(() => {
// ...
bunny.x = bunnyProps.x;
bunny.y = bunnyProps.y;
// ...
}, [bunnyProps])

Related

Simple animation with react and gsap

I have been struggling with this for days and am just having trouble wrapping my head around the varied documentation and examples out there, so I'm hoping someone can help with the specific example I'm working on and that might in turn help me understand this a bit better. This seems like it's far more convoluted than it should be, but I'm sure that's just my lack of understanding of how it all works.
The goal: make a very simple animation of a red ball moving back and forth. Hell, make any kind of animation at all (once I can confirm that gsap is animating anything at all, I should be able to take it from there).
The problem: Nothing happens. No errors, nothing clear to go on, just nothing. I still don't have a strong understanding of how this should work; a lot of the guides I've checked seem to be using different methods and don't go into a lot of detail as to why, so it's made it difficult to extend that knowledge to my specific scenario.
The code. I've simplified this greatly because, as I mentioned, all I really need is to just get any kind of gsap animation to work at all and from there I'm confident I can do with it what I need. If anyone feels like it would make a difference, I'm happy to update with the full code:
import React, { useRef, useEffect } from 'react';
import { gsap } from 'gsap';
const tl = gsap.timeline({paused: true, repeat: 0});
function App() {
const waitingAnimationRef = useRef(null);
useEffect(() => {
tl.set(waitingAnimationRef, {autoAlpha: 0});
tl.play();
}, []);
return (
<div className="App">
<div id="red-circle" ref={waitingAnimationRef}></div>
</div>
);
}
export default App;
Here's a working example and some tips to help you make some sense of it:
Create a timeline with gasp.timeline and store it in a ref. You can do this as you've done, creating the timeline outside your component, but then you need to pass that timeline to the ref in your component. In this example, I did that by passing the variable name for the timeline to the useRef hook directly: const tl = useRef(timeline);.
I used the timeline options { repeat: -1, yoyo: true } so that the animation would loop infinitely in alternating directions because you said you wanted to make a ball "moving back and forth".
You'll also need a DOM node ref so you can pass that to the gsap.context() method. Create the ref in your component and pass it to the wrapping element of the component. Here, I called mine app (const app = useRef(null)) and then passed it to the top level div in the App component with ref={app}. Make sure you're passing the ref to a DOM node and not a React component (or you'll have to forward the ref down to a node child within it). Why are we using refs? Because refs are stable between rerenders of your components like state, but unlike state, modifying refs don't cause rerenders. The useRef hook returns an object with a single property called current. Whatever you put in the ref is accessed via the current property.
Use a useLayoutEffect() hook instead of useEffect. React guarantees that the code inside useLayoutEffect and any state updates scheduled inside it will be processed before the browser repaints the screen. The useEffect hook doesn't prevent the browser from repainting. Most of the time we want this to be the case so our app doesn't slow-down. However, in this case the useLayoutEffect hook ensures that React has performed all DOM mutations, and the elements are accessible to gsap to animate them.
Inside the useLayoutEffect, is where you'll use the gsap context method. The gsap context method takes two arguments: a callback function and a reference to the component. The callback is where you can access your timeline (don't forget to access via the current property of the ref object) and run your animations.
There are two ways to target the elements that you're going to animate on your timeline: either use a ref to store the DOM node or via a selector. I used a selector with the ".box" class for the box element. This is easy and it's nice because it will only select matching elements which are children of the current component. I used a ref for the circle component. I included this as an example, so you could see how to use forwardRefs to pass the ref from the App component through Circle component to the child div DOM node. Even though this is a more "React-like" approach, it's harder and less flexible if you have a lot of elements to animate.
Just like useEffect, useLayoutEffect returns a clean up function. Conveniently, the gsap context object has a clean up method called revert.
import { useLayoutEffect, useRef } from "react";
import gsap from "gsap";
const timeline = gsap.timeline({ repeat: -1, yoyo: true });
function App() {
const tl = useRef(timeline);
const app = useRef(null);
const circle = useRef(null);
useLayoutEffect(() => {
const ctx = gsap.context(() => {
tl.current
// use scoped selectors
// i.e., selects matching children only
.to(".box", {
rotation: 360,
borderRadius: 0,
x: 100,
y: 100,
scale: 1.5,
duration: 1
})
// or refs
.to(circle.current, {
rotation: 360,
borderRadius: 50,
x: -100,
y: -100,
scale: 1.5,
duration: 1
});
}, app.current);
return () => ctx.revert();
}, []);
return (
<div ref={app} className="App">
<Box />
<Circle ref={circle} />
</div>
);
}

ArcGIS Map and NextJS rendering

I have been trying to fix this issue for a while and I haven't found any suitable solution on the net.
I am trying to solve a rendering issue using nextJS and Arcgis JS API. In this codesandbox, I have created a simple app where I am incrementing a counter every 5 seconds and display a map.The counter is an useState hook and it seems that every time it is updated, the map is re render again. How can I render the map a single time.
I have been following this recommendation and the get started page on the NextJS website.I had a read at some questions on stackoverflow as well...
My code to add the map is the following...
const EsriMap = ()=>{
const mapDiv = useRef(null);
useEffect(() => {
console.log("load map")
// Grab the webmap object out of the UseRef() hook
// so that we can mutate it.
const map=new ArcGISMap({
basemap: "osm",
})
// let map = webmap.current;
const view = new MapView({
map,
container: mapDiv.current,
center: [140, -27],
scale: 40000000,
});
// Configure the map...
}, []);
return <div className={styles.mapDiv} ref={mapDiv}></div>
}
export default EsriMap;
I would appreciate if someone can help me to solve this one.
Problem:
I noticed that you are using the counter directly in App.js
This will cause the application to rerender each time the state of the counter changes
Solution:
You may consider moving your counter into a separate component or custom hook to avoid the rerender of all app
Ps: if you did not figure out how to do that I could prepare an example for you
I hope this helps you

React useEffect strange behaviour with custom layout component

I'm trying to use scroll position for my animations in my web portfolio. Since this portfolio use nextJS I can't rely on the window object, plus I'm using navigation wide slider so I'm not actually scrolling in the window but in a layout component called Page.
import React, { useEffect } from 'react';
import './page.css';
const Page = ({ children }) => {
useEffect(() => {
const scrollX = document.getElementsByClassName('page')
const scrollElement = scrollX[0];
console.log(scrollX.length)
console.log(scrollX)
scrollElement.addEventListener("scroll", function () {
console.log(scrollX[0].scrollTop)
});
return () => {
scrollElement.removeEventListener("scroll", () => { console.log('listener removed') })
}
}, [])
return <div className="page">{children}</div>;
};
export default Page;
Here is a production build : https://next-portfolio-kwn0390ih.vercel.app/
At loading, there is only one Page component in DOM.
The behaviour is as follow :
first listener is added at first Page mount, when navigating, listener is also added along with a new Page component in DOM.
as long as you navigate between the two pages, no new listener/page is added
if navigating to a third page, listener is then removed when the old Page is dismounted and a new listener for the third page is added when third page is mounted (etc...)
Problem is : when you navigate from first to second, everything looks fine, but if you go back to the first page you'll notice the console is logging the scrollX value of the second listener instead of the first. Each time you go on the second page it seems to add another listener to the same scrollElement even though it's not the same Page component.
How can I do this ? I'm guessing the two component are trying to access the same scrollElement somewhat :/
Thanks for your time.
Cool site. We don't have complete info, but I suspect there's an issue with trying to use document.getElementsByClassName('page')[0]. When you go to page 2, the log for scrollX gives an HTMLCollection with 2 elements. So there's an issue with which one is being targeted. I would consider using a refs instead. Like this:
import React, { useEffect, useRef } from 'react';
import './page.css';
const Page = ({ children }) => {
const pageRef = useRef(null)
const scrollListener = () => {
console.log(pageRef.current.scrollTop)
}
useEffect(() => {
pageRef.addEventListener("scroll", scrollListener );
return () => {
pageRef.removeEventListener("scroll", scrollListener )
}
}, [])
return <div ref={pageRef}>{children}</div>;
};
export default Page;
This is a lot cleaner and I think will reduce confusion between components about what dom element is being referenced for each scroll listener. As far as the third page goes, your scrollX is still logging the same HTMLElement collection, with 2 elements. According to your pattern, there should be 3. (Though there should really only be 1!) So something is not rendering properly on page 3.
If we see more code, it might uncover the error as being something else. If refs dont solve it, can you post how Page is implemented in the larger scope of things?
also, remove "junior" from the "junior developer" title - you won't regret it

React component reference own DOM-element in typescript

Still being quite new to React I've run across the following issue:
I'm creating a component this way:
export const CSpiderWeb = (props: iSpiderWebProps) => {
const classes = useStyles();
const [drawingObject, setDrawingObject] = useState({} as iDrawingObject);
const _InitRaphael = (target : HTMLDivElement) => {
while (target.firstChild) {
target.removeChild(target.firstChild);
}
const workDrawingObject : iDrawingObject = {
width : target.offsetWidth,
height : target.offsetHeight,
centerX : target.offsetWidth / 2,
centerY : target.offsetHeight /2
}
workDrawingObject.paper = Raphael(target, workDrawingObject.width, workDrawingObject.height);
setDrawingObject(workDrawingObject);
}
var workRef = createRef<HTMLDivElement>();
_InitRaphael(workRef.current as HTMLDivElement);
return <div ref={workRef} className={classes.paperContainer}>{drawingObject.centerX}x{drawingObject.centerY}</div>
}
What I'm trying to accomplish here is get the rendered DIV element and pass it to the _InitRaphael method, but it appears that this is called before the element is rendered.
Makes sense, but HOW could this be done. I've googled and googled and sometimes I run across the componentDidMount hook, but can thhat be used here and if thats the case then how?
You can't use componentDidMount because it could only be used in class components, when you use function component as in your example use hooks https://reactjs.org/docs/hooks-effect.html
I would recommend you to use the useEffect hook which imitates componentDidMount.
You should consider using the useEffect hook. It called after the rendering done so the ref should have value.
useEffect(() => {
_InitRaphael(workRef.current as HTMLDivElement);
}, [])
But unless you use an external non React library, you should not use this pattern. In a React app ref usage is the exception, not the normal way of dooing things.

How do I take a screenshot of a React component without mounting it in the DOM?

My use case is: I have a web site editor and a list of available web pages for users. Each page in this list is represented by a thumbnail. Every time a user makes a change to a page using the editor, the thumbnail of the respective site has to be updated to reflect the change. The way I'm doing is by mounting a ThumbnailSandbox component in the page, passing the props from the Redux store and then using dom-to-png to create the screenshot and use it in the list. But I wanted to do it without mounting the component on the page, because I think it would be a cleaner solution and with less chances of being affected by other interactions happening. So, I created a CodeSanbox to illustrate what I'm trying to achieve.
My logic is this:
import React from "react";
import ReactDOMServer from "react-dom/server";
import html2canvas from "html2canvas";
import MyComp from "./component.jsx";
export const createScrenshot = () => {
const el = (
<div>
test component <MyComp />
</div>
);
const markup = ReactDOMServer.renderToString(el);
let doc = new DOMParser().parseFromString(markup, "text/html");
let target = doc.body.getElementsByClassName("my-comp")[0];
console.log(markup, target);
html2canvas(target, {
useCORS: true,
allowTaint: true,
scale: 1,
width: 500,
height: 500,
x: 0,
y: 0,
logging: true,
windowWidth: 500,
windowHeight: 500
})
.then(function(canvas) {
console.log(">> ", canvas);
})
.catch(error => {
console.log(error);
});
};
So, I'm passing the component to ReactDOM, then creating a DOM node using the string from first step and passing the node to html2canvas. But at this point I get the error Uncaught TypeError: Cannot read property 'pageXOffset' of null. Because the ownerDocument of the element passed to html2canvas is null and it doesn't have the properties: devicePixelRation, innerWidth, innerHeight, pageYOffset, and pageXOffset. As I understand, that's because the node element is not part of the DOM.
Now, my questions are:
1) Is there a way to solve this problem using html2canvas?
2) Is there any other way to take a screenshot of a React component, in the browser, without mounting the component in the DOM?
Thank you in advance!!
For point 1:
Why don't you mount the component and then after your handling delete the component in the ref? (can be done in ComponentDidMount too but ref would come before DidMount) That's the most standard solution to perform downloads (create an a tag do a click and then remove it)
This is a sample untested code using ref call back
export class CreateScrenshot extends React.Component {
constructor() {
super() {
this._reactRef = this._reactRef.bind(this);
this.state = {
removeNode: false
};
}
}
_reactRef(node) {
if(node) {
// your html2Canvas handling and in the returned promise remove the node by
this.setState({removeNode: true});
}
}
render() {
let childComponent = null;
if(!this.state.removeNode) {
{/*pass the ref of the child node to the parent component using the ref callback*/}
childComponent = (
<div>
test component <MyComp refCallBack={this._reactRef}/>
</div>
);
}
return childComponent;
}
}
the limitation however is that this will be asynchronous and might cause a flicker.
So if possible try using a sync library so that the node can be removed on the next render.
For point 2: https://reactjs.org/docs/react-component.html#componentdidmount
From react's componentDidMount() doc section:
"It can, however, be necessary for cases like modals and tooltips when you need to measure a DOM node before rendering something that depends on its size or position."
This makes it clear that you can only get the nodes' measurements after it has been mounted.
Set the react element to z-index and bottom -9999px

Resources