React + fabric.js - reactjs

I am trying to combine react and fabricjs but I am stuck.
Here is my code
import React, { useState, useEffect, useRef } from 'react';
import { fabric } from "fabric";
function App() {
const [canvas, setCanvas] = useState('');
useEffect(() => {
setCanvas(initCanvas());
}, []);
const initCanvas = () => (
new fabric.Canvas('canvas', {
height: 800,
width: 800,
backgroundColor: 'pink' ,
selection: false,
renderOnAddRemove: true,
})
)
canvas.on("mouse:over", ()=>{
console.log('hello')
})
return (
<div >
<canvas id="canvas" />
</div>
);
}
export default App;
The problem is canvas.on as it causes the error 'Uncaught TypeError: canvas.on is not a function'
Please tell me what am I doing wrong here

During the initial render, your canvas variable is set to your initial state, '' from useState(''). It's not until after this that your useEffect will run, updating the state value.
Recommendation: Move your event handlers into the useEffect and use a ref instead of state for your canvas value. refs have the property of being directly mutable and not requiring a rerender for their new value to be available.
import React, { useState, useEffect, useRef } from 'react';
import { fabric } from "fabric";
function App() {
const canvas = useRef(null);
useEffect(() => {
canvas.current = initCanvas();
canvas.current.on("mouse:over", () => {
console.log('hello')
});
// destroy fabric on unmount
return () => {
canvas.current.dispose();
canvas.current = null;
};
}, []);
const initCanvas = () => (
new fabric.Canvas('canvas', {
height: 800,
width: 800,
backgroundColor: 'pink' ,
selection: false,
renderOnAddRemove: true,
})
);
return (
<div >
<canvas ref={canvas} />
</div>
);
}
export default App;
It's worth noting that if you don't need a reference to the canvas elsewhere in your component, you don't need to use state or a ref and can use a local variable within the useEffect.
useEffect(() => {
const canvas = initCanvas();
canvas.on("mouse:over", () => {
console.log('hello')
});
// destroy fabric on unmount
return () => {
canvas.dispose();
};
})

Actually the problem is that you trying to call canvas.on when it is an empty string in canvas (initial state)
Since we are only need to create fabric.Canvas once, I would recommend to store instance with React.useRef
I created an example for you here:
--> https://codesandbox.io/s/late-cloud-ed5r6q?file=/src/FabricExample.js
Will also show the source of the example component here:
import React from "react";
import { fabric } from "fabric";
const FabricExample = () => {
const fabricRef = React.useRef(null);
const canvasRef = React.useRef(null);
React.useEffect(() => {
const initFabric = () => {
fabricRef.current = new fabric.Canvas(canvasRef.current);
};
const addRectangle = () => {
const rect = new fabric.Rect({
top: 50,
left: 50,
width: 50,
height: 50,
fill: "red"
});
fabricRef.current.add(rect);
};
const disposeFabric = () => {
fabricRef.current.dispose();
};
initFabric();
addRectangle();
return () => {
disposeFabric();
};
}, []);
return <canvas ref={canvasRef} />;
};
export default FabricExample;

Related

why useRef current value , isn't sharing trough custom hook?

I wanted to calculate the user scroll height , so I created a custom hook. and I wanted to share this value to another component. but it doesnt work.
code:
const useScroll = () => {
let scrollHeight = useRef(0);
const scroll = () => {
scrollHeight.current =
window.pageYOffset ||
(document.documentElement || document.body.parentNode || document.body)
.scrollTop;
};
useEffect(() => {
window.addEventListener("scroll", scroll);
return () => {
window.removeEventListener("scroll", () => {});
};
}, []);
return scrollHeight.current;
};
export default useScroll;
the value is not updating here.
but if I use useState here , it works. but that causes tremendous amount of component re-rendering. can you have any idea , how its happening?
Since the hook won't rerender you will only get the return value once. What you can do, is to create a useRef-const in the useScroll hook. The useScroll hook returns the reference of the useRef-const when the hook gets mounted. Because it's a reference you can write the changes in the useScroll hook to the useRef-const and read it's newest value in a component which implemented the hook. To reduce multiple event listeners you should implement the hook once in the parent component and pass the useRef-const reference to the child components. I made an example for you.
The hook:
import { useCallback, useEffect, useRef } from "react";
export const useScroll = () => {
const userScrollHeight = useRef(0);
const scroll = useCallback(() => {
userScrollHeight.current =
window.pageYOffset ||
(document.documentElement || document.body.parentNode || document.body)
.scrollTop;
}, []);
useEffect(() => {
window.addEventListener("scroll", scroll);
return () => {
window.removeEventListener("scroll", scroll);
};
}, []);
return userScrollHeight;
};
The parent component:
import { SomeChild, SomeOtherChild } from "./SomeChildren";
import { useScroll } from "./ScrollHook";
const App = () => {
const userScrollHeight = useScroll();
return (
<div>
<SomeChild userScrollHeight={userScrollHeight} />
<SomeOtherChild userScrollHeight={userScrollHeight} />
</div>
);
};
export default App;
The child components:
export const SomeChild = ({ userScrollHeight }) => {
const someButtonClickHandlerWhichPrintsUserScrollHeight = () => {
console.log("userScrollHeight from SomeChild", userScrollHeight.current);
};
return (
<div style={{
width: "100vw",
height: "100vh",
backgroundColor: "aqua"
}}>
<h1>SomeChild 1</h1>
<button onClick={() => someButtonClickHandlerWhichPrintsUserScrollHeight()}>Console.log userScrollHeight</button>
</div>
);
};
export const SomeOtherChild = ({ userScrollHeight }) => {
const someButtonClickHandlerWhichPrintsUserScrollHeight = () => {
console.log("userScrollHeight from SomeOtherChild", userScrollHeight.current);
};
return (
<div style={{
width: "100vw",
height: "100vh",
backgroundColor: "orange"
}}>
<h1>SomeOtherChild 1</h1>
<button onClick={() => someButtonClickHandlerWhichPrintsUserScrollHeight()}>Console.log userScrollHeight</button>
</div>
);
};
import { useRef } from 'react';
import throttle from 'lodash.throttle';
/**
* Hook to return the throttled function
* #param fn function to throttl
* #param delay throttl delay
*/
const useThrottle = (fn, delay = 500) => {
// https://stackoverflow.com/a/64856090/11667949
const throttledFn = useRef(throttle(fn, delay)).current;
return throttledFn;
};
export default useThrottle;
then, in your custom hook:
const scroll = () => {
scrollHeight.current =
window.pageYOffset ||
(document.documentElement || document.body.parentNode || document.body)
.scrollTop;
};
const throttledScroll = useThrottle(scroll)
Also, I like to point out that you are not clearing your effect. You should be:
useEffect(() => {
window.addEventListener("scroll", throttledScroll);
return () => {
window.removeEventListener("scroll", throttledScroll); // remove Listener
};
}, [throttledScroll]); // this will never change, but it is good to add it here. (We've also cleaned up effect)

How to control functions in certain media queries in React using hooks

I am working on a React project in that I have a button, for that button I have written one onClick function now what I need is when I click the button it only needs to change background color only to mobile screen from min(0px) to max(576px) in this screen only the function change has to apply.
This is my code
import React, { useState } from 'react';
import './App.css';
function App() {
const [color,setColor]=useState('red');
const [textColor,setTextColor]=useState('white');
const changeBackGround =() =>{
{setColor("black");setTextColor('red')}
}
return (
<div className="App">
<button style={{background:color,color:textColor}} onClick={changeBackGround} className='btn btn-primary'>Click here</button>
</div>
);
}
export default App
If you have any questions please let me know. Thank you
Have a state object that updates the the className on the button click. Update the className in the css media query.
import React, { useState } from 'react';
import './App.css';
function App() {
const [color,setColor]=useState('red');
const [textColor,setTextColor]=useState('white');
const [buttonClassName, setButtonClassName] = useState("");
const changeBackGround = () =>{
setColor("black");
setTextColor('red');
setButtonClassName("btn-update");
}
return (
<div className="App">
<button
style={{background:color,color:textColor}}
onClick={changeBackGround}
className={`btn btn-primary ${buttonClassName}`}>
Click here
</button>
</div>
);
}
export default App
#media screen and (max-width: 576px) {
.btn-update {
background-color: "green";
}
}
You can do this in 2 ways
check your window.innerWidth . But this will not work when you resize your window in the browser. To Test this what you can do is resize your browser window so the width is less than 576px and refresh your screen and click the button now .
const changeBackGround =() =>{
if(window.innerWidth < 576){
setColor("black");
setTextColor('red')}
} else {
...do something
}
}
Attach an event listener which listens for your resize event , now when you resize the window the width is maintained in state.
function App() {
const [deviceSize, changeDeviceSize] = useState(window.innerWidth);
const [color, setColor] = useState('red');
const [textColor, setTextColor] = useState('white');
useEffect(() => {
const handleResize = () => changeDeviceSize(window.innerWidth);
window.addEventListener('resize', handleResize);
// don't forget to remove the event listener on unmounting the component
return () => window.removeEventListener('resize', handleResize);
}, []);
const changeBackGround = () => {
if (deviceSize < 576) {
{
setColor('black');
setTextColor('red');
}
}
};
return (
<div className="App">
<button
style={{background: color, color: textColor}}
onClick={changeBackGround}
className="btn btn-primary"
>
Click here
</button>
</div>
);
}
If you want to trigger things dynamically, use custom hooks to get window size (generic) and another custom hook to check if it's valid for mobile (can be kept in a separate hooks folder).
useWindowSize.js
// Hook from https://usehooks.com/useWindowSize/
function useWindowSize() {
// Initialize state with undefined width/height so server and client renders match
// Learn more here: https://joshwcomeau.com/react/the-perils-of-rehydration/
const [windowSize, setWindowSize] = useState({
width: undefined,
height: undefined
});
useEffect(() => {
// Handler to call on window resize
function handleResize() {
// Set window width/height to state
setWindowSize({
width: window.innerWidth,
height: window.innerHeight
});
}
// Add event listener
window.addEventListener("resize", handleResize);
// Call handler right away so state gets updated with initial window size
handleResize();
// Remove event listener on cleanup
return () => window.removeEventListener("resize", handleResize);
}, []); // Empty array ensures that effect is only run on mount
return windowSize;
}
useIsMobile.js
const MAX_SIZE_FOR = { mobile: 576 };
const useIsMobile = () => {
const { width } = useWindowSize();
return width < MAX_SIZE_FOR;
};
yourComponent.js
import React, { useState } from "react";
import { useIsMobile } from './useIsMobile'
import "./App.css";
function App() {
const [style, setStyle] = useState({ background: "red", textColor: "white" });
const isMobile = useIsMobile();
const changeBackGround = () => {
if (isMobile) {
setStyle({ ...style, background: "black", textColor: "red" });
}
};
return (
<div className="App">
<button style={style} onClick={changeBackGround} className="btn btn-primary">
Click here
</button>
</div>
);
}
export default App;
You can even change the color on the fly via useEffect
const { width } = useWindowSize();
useEffect(changeBackGround, [width]);
You can create a state variable to update when the screen gets to a certain width. In a useEffect(), you can add a eventListener to the window that listens to the screen resizing. When the screen gets resized to a certain width, we update the state and use it to do conditional rendering in the return.
const [show, setShow] = useState(false); // state value for showing / hiding
useEffect(() => {
const handleResize = () => {
window.innerWidth < 576 ? setShow(true) : setShow(false); // set hide / show
}
window.addEventListener("resize", handleResize); // add event listener
}, []);
return ({
show ? <h1>show</h1>: <h1>hide</h1>
});

Maximum update depth exceeded with useLayoutEffect, useRef

I have the following component:
// component.js
import React from 'react';
import useComponentSize from 'useComponentSize';
const component = () => {
const [comSize, comRef] = useComponentSize();
return (
<div style={{width: '100%'}} ref={comRef}>
<p>hi</p>
</div>
);
};
which is using useComponentSize, a hook I've made:
// useComponentSize.js
import {
useRef,
useState,
useLayoutEffect,
} from 'react';
const useComponentSize = () => {
const [size, setSize] = useState({
width: 0,
height: 0,
});
const resizeRef = useRef();
useLayoutEffect(() => {
setSize(() => ({
width: resizeRef.current.clientWidth,
height: resizeRef.current.clientHeight,
}));
});
return [size, resizeRef];
};
export default useComponentSize;
but for a reason I cannot work out, it always exceeds the maximum update depth. I've tried having useLayoutEffect depend upon resizeRef, which I thought would work, but then it doesn't update again (which upon reflection is exactly how I should have expected a ref to work).
What should I do to make this work properly, and most importantly why does the above cause an infinite loop?
Edit: second attempt using event listeners, still failing. What concept am I missing here?
// component.js
import React, { useRef } from 'react';
import useComponentSize from 'useComponentSize';
const component = () => {
const ref = useRef();
const [comSize] = useComponentSize(ref);
return (
<div style={{width: '100%'}} ref={ref}>
<p>hi</p>
</div>
);
};
import {
useRef,
useState,
useLayoutEffect,
} from 'react';
const useComponentSize = (ref) => {
const [size, setSize] = useState();
useLayoutEffect(() => {
const updateSize = () => {
setSize(ref.current.clientWidth);
}
ref.current.addEventListener('resize', updateSize);
updateSize();
return () => window.removeEventListener('resize', updateSize);
}, []);
return [size];
};
export default useComponentSize;
That edit above is based upon this useWindowSize hook, which works great (I'm using it currently as a replacement, although I'd rather still get the above to work, and especially to know why it doesn't work).
A small explanation of what I'm trying to achieve as it wasn't made explicitly clear before: I want the state size to update whenever the size of the referenced component's size changes. That is, if the window resizes, and the component remains the same size, it should not update. But if the component size does change, then the size state should change value to reflect that.
Your code gets stuck in an infinite loop because you haven't passed the dependency array to useEffectLayout hook.
You actually don't need to use useEffectLayout hook at all. You can observe the changes to the DOM element using ResizeObserver API.
P.S: Although OP's problem has already been solved through a demo posted in one of the comments under the question, i am posting an answer for anyone who might look at this question in the future.
Example:
const useComponentSize = (comRef) => {
const [size, setSize] = React.useState({
width: 0,
height: 0
});
React.useEffect(() => {
const sizeObserver = new ResizeObserver((entries, observer) => {
entries.forEach(({ target }) => {
setSize({ width: target.clientWidth, height: target.clientHeight });
});
});
sizeObserver.observe(comRef.current);
return () => sizeObserver.disconnect();
}, [comRef]);
return [size];
};
function App() {
const comRef = React.useRef();
const [comSize] = useComponentSize(comRef);
return (
<div>
<textarea ref={comRef} placeholder="Change my size"></textarea>
<h1>{JSON.stringify(comSize)}</h1>
</div>
);
}
ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.1/umd/react-dom.production.min.js"></script>
<div id="root"></div>

Ref.current is undefined when component mount and it can not cause a re render when I need it

I want to make a draggable div so I created a ref to access the div so I could use it in Rxjs fromEvent operator
The problem is ref object will set after using it in fromEvent (the first time ref is not set yet) so I put it in an if statement so It only use ref when it's set.
Now dragRef.current is set but component never execute again
here is the code I'm using:
import React, { useRef, useEffect, useState } from "react";
import "./App.css";
import { fromEvent } from "rxjs";
import { map } from "rxjs/operators";
const App: React.FC = () => {
const dragRef = useRef<HTMLDivElement>(null);
const mouseMove = fromEvent(document, "mousedown");
if (dragRef.current != undefined) {
debugger;
const mouseDown = fromEvent(dragRef.current, "mousedown");
const mouseUp = fromEvent(dragRef.current, "mouseup");
const mouseDrag = mouseDown.pipe(
map((evt: MouseEvent | Event) => {
// let offsetX = evt.clientX - dragRef.current.offsetLeft;
// let offsetY = evt.clientY - dragRef.current.offsetTop;
console.log(evt);
return evt;
})
);
mouseDrag.subscribe(v => console.log(v));
}
return (
<div className="App">
<header className="App-header">
<div className="drag" ref={dragRef}></div>
</header>
</div>
);
};
export default App;
You can use the useEffect hook, which takes as a dependency an array of variables you want to be watching.
So in your case, when your ref changes, you then execute the function.
Something like
useEffect(() => {
if (dragRef.current != undefined) {
debugger;
const mouseDown = fromEvent(dragRef.current, "mousedown");
const mouseUp = fromEvent(dragRef.current, "mouseup");
const mouseDrag = mouseDown.pipe(
map((evt: MouseEvent | Event) => {
// let offsetX = evt.clientX - dragRef.current.offsetLeft;
// let offsetY = evt.clientY - dragRef.current.offsetTop;
console.log(evt);
return evt;
})
);
mouseDrag.subscribe(v => console.log(v));
}
}, [dragRef])
here is the final Code Which works perfectly for a draggable div in React
import React, { useRef, useEffect, useState } from "react";
import "./App.css";
import { fromEvent } from "rxjs";
import { map, takeUntil, mergeMap } from "rxjs/operators";
const App: React.FC = () => {
const dragRef = useRef<HTMLDivElement>(null);
const [position, setPosition] = useState({top: 100, left: 100})
useEffect(() => {
if (dragRef != null) {
const mouseMove = fromEvent(document, "mousemove");
//#ts-ignore
const mouseDown = fromEvent(dragRef.current, "mousedown");
//#ts-ignore
const mouseUp = fromEvent(dragRef.current, "mouseup");
const mouseDrag = mouseDown.pipe(
mergeMap((evt: any) => {
//#ts-ignore
let offsetX = evt.clientX - dragRef!.current.offsetLeft;
//#ts-ignore
let offsetY = evt.clientY - dragRef!.current.offsetTop;
return mouseMove.pipe(
map((e: any) => ({
top: e.clientY - offsetY,
left: e.clientX - offsetX
})),
takeUntil(mouseUp),
);
})
);
mouseDrag.subscribe((value: any) => {
console.log(value)
setPosition(value)
});
}
}, [dragRef]);
return (
<div className="App">
<header className="App-header">
<div className="drag" ref={dragRef} style={{top: position.top +'px', left: position.left +'px'}}></div>
</header>
</div>
);
};
export default App;

Getting undefined while accessing Context values

I keep on getting undefined while trying to access values from the the component.Here is my Provider file content :
import React from "react";
import { FlyToInterpolator } from "react-map-gl";
export const MapContext = React.createContext();
export function MapProvider(props) {
const [viewport, setViewport] = React.useState(INITIAL_STATE);
const onLoad = () => {
setViewport(DRC_MAP);
};
return (
<MapContext.Provider
value={{
viewport,
setViewport,
onLoad
}}
{...props}
/>
);
}
export const { Consumer: MapConsumer } = MapContext;
export const withMap = Component => props => {
return (
<MapConsumer>{value => <Component map={value} {...props} />}</MapConsumer>
);
};
// this is what state gets initialised as
const INITIAL_STATE = {
height: "100vh",
width: "100%",
longitude: 23.071374,
latitude: -3.6116245,
zoom: 1.33
};
const DRC_MAP = {
longitude: 23.656,
latitude: -2.88,
zoom: 4,
transitionDuration: 3000,
transitionInterpolator: new FlyToInterpolator(),
transitionEasing: t => t * (2 - t)
};
So when i try to use the viewport ot any other values defined i get undefined.Here is my Map component that is using the above code.
import React, { useContext } from "react";
import ReactMapGL from "react-map-gl";
import { MapContext } from "./contexts/MapProvider";
const MAPBOX_TOKEN ="secret"
const mapStyle = "mapbox://styles/jlmbaka/cjvf1uy761fo41fp8ksoil15x";
export default function Map() {
const { viewport, setViewport, onLoad } = useContext(MapContext);
return (
<ReactMapGL
mapboxApiAccessToken={MAPBOX_TOKEN}
mapStyle={mapStyle}
onViewportChange={nextViewport => setViewport(nextViewport)}
onLoad={onLoad}
ref={ref => (window.mapRef = ref && ref.getMap())}
{...viewport}
/>
);
}
I've read several problems which are similar to mine but,none of them are adapted for my case.Here they are :
Context value undefined in React
React context state property is undefined
You made a Context.Provider:
export function MapProvider({ children, ...props }) {
const [viewport, setViewport] = React.useState(INITIAL_STATE);
const onLoad = () => {
setViewport(DRC_MAP);
};
return (
<MapContext.Provider
value={{
viewport,
setViewport,
onLoad
}}
{...props}
>
{children} // <-- Children are consumers
</MapContext.Provider>
);
}
But you didn't consume the context:
// Somewhere in the code you need to consume its context
function Consumer() {
return (
<MapProvider>
<Map />
</MapProvider>
);
}
And then useContext will be valid:
export default function Map() {
// Child of MapContext.Provider,
// so it can consume the context.
const { viewport, setViewport, onLoad } = useContext(MapContext);
...
}

Resources