React useEffect dependency or useCallback seem to cause re-renders - reactjs

I have a useEffect function that takes an image, resizes it for display and makes it ready for upload. While this works I get the React warning to list the function in the useEffect dependency list. But when I do this it causes continuous re-renders even when moving the function to a useCallback. The code is as follows ...
import React, { useEffect, useRef, useCallback } from 'react'
const Canvas = (props) => {
const { width = 180, height = 220, img, onChange = null, setFile = null} = props
const canvas = useRef(null)
const setBlobFile = useCallback(
(blob) => {
setFile(blob)
},
[setFile]
)
useEffect(() => {
if (img) {
console.log("Canvas useEffect ...", img)
var image = new Image()
image.src = img
image.onload = () => {
canvas.current.getContext("2d")
canvas.current.getContext("2d").clearRect(0, 0, width, height);
canvas.current.getContext("2d").drawImage(image, 0, 0, image.width, image.height, 0, 0, width, height)
canvas.current.toBlob((blob) => {
setBlobFile(blob)
})
}
}
}, [img, width, height, setBlobFile])
const onFileSelect = (e) => {
const objectURL = URL.createObjectURL(e.target.files[0])
onChange(objectURL)
}
return (
<div onClick={onClick}>
<label htmlFor='upload'>
<canvas ref={canvas} width={width} height={height} />
<input type='file' id='upload' onChange={(e) => onFileSelect(e)} style={{ display: 'none' }} />
</label>
</div>
)
}
export { Canvas as default }
Parent Component provides the setFile routine to set the file for upload
...
const [selectedFile, setSelectedFile] = useState(new File([""], ""))
const setFile = (aBlob) => {
var img = new Image()
img = aBlob
setSelectedFile(new File([aBlob], "image.png", {
type: 'image/png',
}))
}
...
Any ideas why I'm seeing this behaviour? I thought the useCallback() approach was supposed to stop the re-renders but it seems to create them. How can I avoid the warning but also stop the re-rendering behaviour? Thanks for taking a look.

You have to use useCallback where the function is created, not where it is used.
const setBlobFile = useCallback(
(blob) => {
setFile(blob)
},
[setFile]
)
in Canvas is useless for preventing rerenders when setFile changes because setBlobFile will still change when setFile changes, since setFile is passed as a dependency.
Instead you need to to use useCallback in the parent component where setFile is created in the first place:
const [selectedFile, setSelectedFile] = useState(new File([""], ""))
const setFile = useCallback((aBlob) => {
var img = new Image()
img = aBlob
setSelectedFile(new File([aBlob], "image.png", {
type: 'image/png',
}))
}, [setSelectedFile]);
This is will work as expected because useState guarantees that the setter function (setSelectedFile) will never change between renders and therefore setFile will never change between renders.

Related

window.visualViewport.height doenst update useEffect when a dependency

This ugly code works. Every second viewportHeight is set to the value of window.visualViewport.height
const [viewportHeight, setViewportHeight] = React.useState(0);
React.useEffect(() => {
setInterval(() => {
setViewportHeight(window.visualViewport.height);
}, 1000);
}, []);
However this doesn't work. viewportHeight is set on page load but not when the height changes.
React.useEffect(() => {
setViewportHeight(window.visualViewport.height);
}, [window.visualViewport.height]);
Additional context: I need the page's height in state and I need the virtual keyboard's height to be subtracted from this on Mobile iOS.
You can only use state variables managed by React as dependencies - so a change in window.visualViewport.height will not trigger your effect.
You can instead create a div that spans the whole screen space and use a resize observer to trigger effects when its size changes:
import React from "react";
import useResizeObserver from "use-resize-observer";
const App = () => {
const { ref, width = 0, height = 0 } = useResizeObserver();
const [viewportHeight, setViewportHeight] = React.useState(height);
React.useEffect(() => {
setViewportHeight(window.visualViewport.height);
}, [height]);
return (
<div ref={ref} style={{ width: "100vw", height: "100vh" }}>
// ...
</div>
);
};
This custom hook works:
function useVisualViewportHeight() {
const [viewportHeight, setViewportHeight] = useState(undefined);
useEffect(() => {
function handleResize() {
setViewportHeight(window.visualViewport.height);
}
window.visualViewport.addEventListener('resize', handleResize);
handleResize();
return () => window.visualViewport.removeEventListener('resize', handleResize);
}, []);
return viewportHeight;
}

useRef type on React Draggable

I am relatively new to coding and especially to Typescript. I am trying to create a React Draggable modal that changes its bounds when the window gets resized and move the modal with it, so it never gets out of the window. I have created a function that does that but I am struggling to use the correct type for the ref draggableRef that is used on the actual draggable. What type is the draggableRef? Whatever I change it to there is problem with the .state.x; on the modalOffsetLeft that it doesn't exist on it.
Or, is there other way to do it? Thanks!
here is the code:
export const DraggableModal: React.FC<DraggableModalProps> = ({
draggableProps,
onClose,
}) => {
const [bounds, setBounds] = useState({
width: 0,
height: 0,
});
const ref = useRef<HTMLDivElement>(null);
const draggableRef = useRef<DraggableCore>(null);
const rootWindow = document.getElementById('root') as HTMLDivElement;
const getValues = () => {
if (ref.current !== null && draggableRef.current !== null) {
const modalWidth = ref?.current?.clientWidth;
const draggableWindowWidth = rootWindow?.clientWidth - modalWidth;
const modalOffsetLeft = draggableRef?.current?.state.x;
setBounds({
width: draggableWindowWidth,
height: rootWindow?.clientHeight - ref.current.offsetHeight,
});
if (modalOffsetLeft > draggableWindowWidth) {
draggableRef.current.state.x = draggableWindowWidth;
}
}
};
useEffect(() => {
getValues();
}, []);
parent.onresize = getValues;
return (
<Draggable
ref={draggableRef}
bounds={{
left: 0,
top: 0,
right: bounds.width,
bottom: bounds.height,
}}
{...draggableProps}
handle="#handle"
>
<DraggableWrapper>
...
</DraggableWrapper>
....
instead of updating its x,y using ref try to create a state {x,y} and then update this x and y.
then pass x and y to position props in Draggable.
by the way you can run the function get values in the more clearing way by adding the function to a listner that runs when the window is changes.

React state always returning previous (or initial) state inside callback from fabricjs

The code below is my minimal issue reproduce component. It initializes fabric canvas, and handles "mode" state. Mode state determines whether canvas can be edited and a simple button controls that state.
The problem is that even if mode,setMode works correctly (meaning - components profiler shows correct state after button click, also text inside button shows correct state), the state returned from mode hook inside fabric event callback, still returns initial state.
I suppose that the problem is because of the function passed as callback to fabric event. It seems like the callback is "cached" somehow, so that inside that callback, all the states have initial values, or values that were in state before passing that callback.
How to make this work properly? I would like to have access to proper, current state inside fabric callback.
const [canvas, setCanvas] = useState<fabric.Canvas | null>(null);
const [mode, setMode] = useState("freerun");
const canvasRef = React.useRef<HTMLCanvasElement>(null);
const modes = ["freerun", "edit"];
React.useEffect(() => {
const canvas = new fabric.Canvas(canvasRef.current, {
height: 800,
width: 800,
backgroundColor: 'yellow'
});
canvas.on('mouse:down', function (this: typeof canvas, opt: fabric.IEvent) {
const evt = opt.e as any;
console.log("currentMode", mode) // Not UPDATING - even though components profiler shows that "mode" state is now "edit", it still returns initial state - "freerun".
if (mode === "edit") {
console.log("edit mode, allow to scroll, etc...");
}
});
setCanvas(canvas);
return () => canvas.dispose();
}, [canvasRef])
const setNextMode = () => {
const index = modes.findIndex(elem => elem === mode);
const nextIndex = index + 1;
if (nextIndex >= modes.length) {
setMode(modes[0])
} else {
setMode(modes[nextIndex]);
}
}
return (
<>
<div>
<button onClick={setNextMode}>Current mode: { mode }</button>
</div>
{`Current width: ${width}`}
<div id="fabric-canvas-wrapper">
<canvas ref={canvasRef} />
</div>
</>
)
The problem is that mode is read and it's value saved inside the callback during the callback's creation and, from there, never update again.
In order to solve this you have to add mode on the useEffect dependencies. In this way each time that mode changes React will run again the useEffect and the callback will receive the updated (and correct) value.
That's true! It worked now, thanks Marco.
Now to not run setCanvas on each mode change, I ended up creating another useEffect hook to hold attaching canvas events only. The final code looks similar to this:
const [canvas, setCanvas] = useState<fabric.Canvas | null>(null);
const [mode, setMode] = useState("freerun");
const canvasRef = React.useRef<HTMLCanvasElement>(null);
const modes = ["freerun", "edit"];
React.useEffect(() => {
if (!canvas) {
return;
}
// hook for attaching canvas events
fabric.Image.fromURL(gd, (img) => {
if (canvas) {
canvas.add(img)
disableImageEdition(img);
}
});
canvas.on('mouse:down', function (this: typeof canvas, opt: fabric.IEvent) {
const evt = opt.e as any;
console.log("currentMode", mode) // works correctly now
if (mode === "edit") {
console.log("edit mode, allow to scroll, etc...");
}
});
}, [canvas, mode])
React.useEffect(() => {
const canvas = new fabric.Canvas(canvasRef.current, {
height: 800,
width: 800,
backgroundColor: 'yellow'
});
setCanvas(canvas);
return () => canvas.dispose();
}, [canvasRef])
const setNextMode = () => {
const index = modes.findIndex(elem => elem === mode);
const nextIndex = index + 1;
if (nextIndex >= modes.length) {
setMode(modes[0])
} else {
setMode(modes[nextIndex]);
}
}
return (
<>
<div>
<button onClick={setNextMode}>Current mode: { mode }</button>
</div>
{`Current width: ${width}`}
<div id="fabric-canvas-wrapper">
<canvas ref={canvasRef} />
</div>
</>
)
I also wonder if there are more ways to solve that - is it possible to solve this using useCallback hook?

useEffect triggers function several times with proper dependencies

i've got Tabs component, it has children Tab components. Upon mount it calculates meta data of Tabs and selected Tab. And then sets styles for tab indicator. For some reason function updateIndicatorState triggers several times in useEffect hook every time active tab changes, and it should trigger only once. Can somebody explain me what I'm doing wrong here? If I remove from deps of 2nd useEffect hook function itself and add a value prop as dep. It triggers correctly only once. But as far as I've read docs of react - I should not cheat useEffect dependency array and there are much better solutions to avoid that.
import React, { useRef, useEffect, useState, useCallback } from 'react';
import PropTypes from 'prop-types';
import { defProperty } from 'helpers';
const Tabs = ({ children, value, orientation, onChange }) => {
console.log(value);
const indicatorRef = useRef(null);
const tabsRef = useRef(null);
const childrenWrapperRef = useRef(null);
const valueToIndex = new Map();
const vertical = orientation === 'vertical';
const start = vertical ? 'top' : 'left';
const size = vertical ? 'height' : 'width';
const [mounted, setMounted] = useState(false);
const [indicatorStyle, setIndicatorStyle] = useState({});
const [transition, setTransition] = useState('none');
const getTabsMeta = useCallback(() => {
console.log('getTabsMeta');
const tabsNode = tabsRef.current;
let tabsMeta;
if (tabsNode) {
const rect = tabsNode.getBoundingClientRect();
tabsMeta = {
clientWidth: tabsNode.clientWidth,
scrollLeft: tabsNode.scrollLeft,
scrollTop: tabsNode.scrollTop,
scrollWidth: tabsNode.scrollWidth,
top: rect.top,
bottom: rect.bottom,
left: rect.left,
right: rect.right,
};
}
let tabMeta;
if (tabsNode && value !== false) {
const wrapperChildren = childrenWrapperRef.current.children;
if (wrapperChildren.length > 0) {
const tab = wrapperChildren[valueToIndex.get(value)];
tabMeta = tab ? tab.getBoundingClientRect() : null;
}
}
return {
tabsMeta,
tabMeta,
};
}, [value, valueToIndex]);
const updateIndicatorState = useCallback(() => {
console.log('updateIndicatorState');
let _newIndicatorStyle;
const { tabsMeta, tabMeta } = getTabsMeta();
let startValue;
if (tabMeta && tabsMeta) {
if (vertical) {
startValue = tabMeta.top - tabsMeta.top + tabsMeta.scrollTop;
} else {
startValue = tabMeta.left - tabsMeta.left;
}
}
const newIndicatorStyle =
((_newIndicatorStyle = {}),
defProperty(_newIndicatorStyle, start, startValue),
defProperty(_newIndicatorStyle, size, tabMeta ? tabMeta[size] : 0),
_newIndicatorStyle);
if (isNaN(indicatorStyle[start]) || isNaN(indicatorStyle[size])) {
setIndicatorStyle(newIndicatorStyle);
} else {
const dStart = Math.abs(indicatorStyle[start] - newIndicatorStyle[start]);
const dSize = Math.abs(indicatorStyle[size] - newIndicatorStyle[size]);
if (dStart >= 1 || dSize >= 1) {
setIndicatorStyle(newIndicatorStyle);
if (transition === 'none') {
setTransition(`${[start]} 0.3s ease-in-out`);
}
}
}
}, [getTabsMeta, indicatorStyle, size, start, transition, vertical]);
useEffect(() => {
const timeout = setTimeout(() => {
setMounted(true);
}, 350);
return () => {
clearTimeout(timeout);
};
}, []);
useEffect(() => {
if (mounted) {
console.log('1st call mounted');
updateIndicatorState();
}
}, [mounted, updateIndicatorState]);
let childIndex = 0;
const childrenItems = React.Children.map(children, child => {
const childValue = child.props.value === undefined ? childIndex : child.props.value;
valueToIndex.set(childValue, childIndex);
const selected = childValue === value;
childIndex += 1;
return React.cloneElement(child, {
selected,
indicator: selected && !mounted,
value: childValue,
onChange,
});
});
const styles = {
[size]: `${indicatorStyle[size]}px`,
[start]: `${indicatorStyle[start]}px`,
transition,
};
console.log(styles);
return (
<>
{value !== 2 ? (
<div className={`tabs tabs--${orientation}`} ref={tabsRef}>
<span className="tab__indicator-wrapper">
<span className="tab__indicator" ref={indicatorRef} style={styles} />
</span>
<div className="tabs__wrapper" ref={childrenWrapperRef}>
{childrenItems}
</div>
</div>
) : null}
</>
);
};
Tabs.defaultProps = {
orientation: 'horizontal',
};
Tabs.propTypes = {
children: PropTypes.node.isRequired,
value: PropTypes.number.isRequired,
orientation: PropTypes.oneOf(['horizontal', 'vertical']),
onChange: PropTypes.func.isRequired,
};
export default Tabs;
useEffect(() => {
if (mounted) {
console.log('1st call mounted');
updateIndicatorState();
}
}, [mounted, updateIndicatorState]);
This effect will trigger whenever the value of mounted or updateIndicatorState changes.
const updateIndicatorState = useCallback(() => {
...
}, [getTabsMeta, indicatorStyle, size, start, transition, vertical]);
The value of updateIndicatorState will change if any of the values in its dep array change, namely getTabsMeta.
const getTabsMeta = useCallback(() => {
...
}, [value, valueToIndex]);
The value of getTabsMeta will change whenever value or valueToIndex changes. From what I'm gathering from your code, value is the value of the selected tab, and valueToIndex is a Map that is re-defined on every single render of this component. So I would expect the value of getTabsMeta to be redefined on every render as well, which will result in the useEffect containing updateIndicatorState to run on every render.

When to use hooks? is worth it that example?

I have write a hook to check if browser is IE, so that I can reutilize the logic instead of write it in each component..
const useIsIE = () => {
const [isIE, setIsIE] = useState(false);
useEffect(() => {
const ua = navigator.userAgent;
const isIe = ua.indexOf("MSIE ") > -1 || ua.indexOf("Trident/") > -1;
setIsIE(isIe);
}, []);
return isIE;
}
export default useIsIE;
Is it worth it to use that hook?
Im not sure if is good idea because that way, Im storing a state and a effect for each hook call (bad performane?) when I can simply use a function like that:
export default () => ua.indexOf("MSIE ") > -1 || ua.indexOf("Trident/") > -1;
What do you think? is worth it use that hook or not?
If not, when should I use hooks and when not?
ty
No. Not worth using the hook.
You'd need to use a hook when you need to tab into React's underlying state or lifecycle mechanisms.
Your browser will probably NEVER change during a session so just creating a simple utility function/module would suffice.
I would recommend to set your browser checks in constants and not functions, your browser will never change.
...
export const isChrome = /Chrome/.test(userAgent) && /Google Inc/.test(navigator.vendor);
export const isIOSChrome = /CriOS/.test(userAgent);
export const isMac = (navigator.platform.toUpperCase().indexOf('MAC') >= 0);
export const isIOS = /iphone|ipad|ipod/.test(userAgent.toLowerCase());
...
This is a simple hook that checks if a element has been scrolled a certain amount of pixels
const useTop = (scrollable) => {
const [show, set] = useState(false);
useEffect(() => {
const scroll = () => {
const { scrollTop } = scrollable;
set(scrollTop >= 50);
};
const throttledScroll = throttle(scroll, 200);
scrollable.addEventListener('scroll', throttledScroll, false);
return () => {
scrollable.removeEventListener('scroll', throttledScroll, false);
};
}, [show]);
return show;
};
Then you can use it in a 'To Top' button to make it visible
...
import { tween } from 'shifty';
import useTop from '../../hooks/useTop';
// scrollRef is your scrollable container ref (getElementById)
const Top = ({ scrollRef }) => {
const t = scrollRef ? useTop(scrollRef) : false;
return (
<div
className={`to-top ${t ? 'show' : ''}`}
onClick={() => {
const { scrollTop } = scrollRef;
tween({
from: { x: scrollTop },
to: { x: 0 },
duration: 800,
easing: 'easeInOutQuart',
step: (state) => {
scrollRef.scrollTop = state.x;
},
});
}}
role="button"
>
<span><ChevronUp size={18} /></span>
</div>
);
};

Resources