Maximum update depth exceeded with useLayoutEffect, useRef - reactjs

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>

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)

React hook for page scroll position causing re-renders on scroll

I'm using a React hook to track the scroll position on a page. The hook code is as follows:
import { useLayoutEffect, useState } from 'react';
const useScrollPosition = () => {
const [scrollPosition, setScrollPosition] = useState(window.pageYOffset);
useLayoutEffect(() => {
const updatePosition = () => {
setScrollPosition(window.pageYOffset);
};
window.addEventListener('scroll', updatePosition);
return () => window.removeEventListener('scroll', updatePosition);
}, []);
return scrollPosition;
};
export default useScrollPosition;
I then use this in various ways, for example in this component where a class is applied to an element if the page has scrolled more than 10px:
const Component = () => {
const scrollPosition = useScrollPosition();
const [scrolled, setScrolled] = useState(false);
useEffect(() => {
const newScrolled = scrollPosition > 10;
if (newScrolled !== scrolled) {
setScrolled(newScrolled);
}
}, [scrollPosition]);
return (
<div
className={clsx(style.element, {
[style.elementScrolled]: scrolled,
})}
>
{children}
</div>
);
};
This all works and does what I'm trying to achieve, but the component re-renders continuously on every scroll of the page.
My understanding was that by using a hook to track the scroll position, and by using useState/useEffect to only update my variable "scrolled" in the event that the scroll position passes that 10px threshold, the component shouldn't be re-rendering continuously on scroll.
Is my assumption wrong? Is this behaviour expected? Or can I improve this somehow to prevent unnecessary re-rendering? Thanks
another idea is to have your hook react only if the scroll position is over 10pixel :
import { useEffect, useState, useRef } from 'react';
const useScrollPosition = () => {
const [ is10, setIs10] = useState(false)
useEffect(() => {
const updatePosition = () => {
if (window.pageYOffset > 10) {setIs10(true)}
};
window.addEventListener('scroll', updatePosition);
return () => window.removeEventListener('scroll', updatePosition);
}, []);
return is10;
};
export default useScrollPosition;
import React, { useEffect, useState } from "react";
import useScrollPosition from "./useScrollPosition";
const Test = ({children}) => {
const is10 = useScrollPosition();
useEffect(() => {
if (is10) {
console.log('10')
}
}, [is10]);
return (
<div
className=''
>
{children}
</div>
);
};
export default Test
so your component Test only renders when you reach that 10px threshold, you could even pass that threshold value as a parameter to your hook, just an idea...
Everytime there is useState, there will be a re-render. In your case you could try useRef to store the value instead of useState, as useRef will not trigger a new render
another idea if you want to stick to your early version is a compromise :have the children of your component memoized, say you pass a children named NestedTest :
import React from 'react'
const NestedTest = () => {
console.log('hit nested')
return (
<div>nested</div>
)
}
export default React.memo(NestedTest)
you will see that the 'hit nested' does not show in the console.
But that might not be what you are expecting in the first place. May be you should try utilizing useRef in your hook instead

React + fabric.js

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;

TypeError: Cannot read property 'onMonthSelect' of undefined while using "useRef"

i am trying to use useRef in my react functional component, but when i try to access it I am getting error
"TypeError: Cannot read property 'onMonthSelect' of undefined while using "useRef" " .
Here is the code below for this
import React, { useRef, useState, useEffect } from "react";
import moment from "moment";
import "react-dates/initialize";
import "react-dates/lib/css/_datepicker.css";
import { SingleDatePicker } from "react-dates";
const SingleDatePickerComponent = () => {
const monthController = useRef();
const [createdAt, setCreatedAt] = useState(moment());
const onDateChange = (createdAt) => {
console.log(createdAt);
setCreatedAt(createdAt);
};
useEffect(() => {
console.log(monthController);
// TODO: check if month is visible before moving
monthController.current.onMonthSelect(
monthController.current.month,
createdAt.format("M")
);
//In this useEffect i am getting the error
}, [createdAt]);
return (
<div>
<div style={{ marginLeft: "200px" }}>
</div>
<SingleDatePicker
date={createdAt}
startDateId="MyDatePicker"
onDateChange={onDateChange}
renderMonthElement={(...args) => {
// console.log(args)
monthController.current = {
month: args[0].month,
onMonthSelect: args[0].onMonthSelect,
};
// console.log(monthController)
return args[0].month.format("MMMM");
}}
id="SDP"
/>
</div>
);
};
export default SingleDatePickerComponent;
The ref value won't be set yet on the initial render. Use a guard clause or Optional Chaining operator on the access.
useEffect(() => {
// TODO: check if month is visible before moving
monthController.current && monthController.current.onMonthSelect(
monthController.current.month,
createdAt.format("M")
);
}, [createdAt]);
or
useEffect(() => {
// TODO: check if month is visible before moving
monthController.current?.onMonthSelect(
monthController.current.month,
createdAt.format("M")
);
}, [createdAt]);
It may also help to provide a defined initial ref value.
const monthController = useRef({
onMonthSelect: () => {},
});

lodash debounce in React functional component not working

I have a functional component built around the React Table component that uses the Apollo GraphQL client for server-side pagination and searching. I am trying to implement debouncing for the searching so that only one query is executed against the server once the user stops typing with that value. I have tried the lodash debounce and awesome debounce promise solutions but still a query gets executed against the server for every character typed in the search field.
Here is my component (with irrelevant info redacted):
import React, {useEffect, useState} from 'react';
import ReactTable from "react-table";
import _ from 'lodash';
import classnames from 'classnames';
import "react-table/react-table.css";
import PaginationComponent from "./PaginationComponent";
import LoadingComponent from "./LoadingComponent";
import {Button, Icon} from "../../elements";
import PropTypes from 'prop-types';
import Card from "../card/Card";
import './data-table.css';
import debounce from 'lodash/debounce';
function DataTable(props) {
const [searchText, setSearchText] = useState('');
const [showSearchBar, setShowSearchBar] = useState(false);
const handleFilterChange = (e) => {
let searchText = e.target.value;
setSearchText(searchText);
if (searchText) {
debounceLoadData({
columns: searchableColumns,
value: searchText
});
}
};
const loadData = (filter) => {
// grab one extra record to see if we need a 'next' button
const limit = pageSize + 1;
const offset = pageSize * page;
if (props.loadData) {
props.loadData({
variables: {
hideLoader: true,
opts: {
offset,
limit,
orderBy,
filter,
includeCnt: props.totalCnt > 0
}
},
updateQuery: (prev, {fetchMoreResult}) => {
if (!fetchMoreResult) return prev;
return Object.assign({}, prev, {
[props.propName]: [...fetchMoreResult[props.propName]]
});
}
}).catch(function (error) {
console.error(error);
})
}
};
const debounceLoadData = debounce((filter) => {
loadData(filter);
}, 1000);
return (
<div>
<Card style={{
border: props.noCardBorder ? 'none' : ''
}}>
{showSearchBar ? (
<span className="card-header-icon"><Icon className='magnify'/></span>
<input
autoFocus={true}
type="text"
className="form-control"
onChange={handleFilterChange}
value={searchText}
/>
<a href="javascript:void(0)"><Icon className='close' clickable
onClick={() => {
setShowSearchBar(false);
setSearchText('');
}}/></a>
) : (
<div>
{visibleData.length > 0 && (
<li className="icon-action"><a
href="javascript:void(0)"><Icon className='magnify' onClick= {() => {
setShowSearchBar(true);
setSearchText('');
}}/></a>
</li>
)}
</div>
)
)}
<Card.Body className='flush'>
<ReactTable
columns={columns}
data={visibleData}
/>
</Card.Body>
</Card>
</div>
);
}
export default DataTable
... and this is the outcome: link
debounceLoadData will be a new function for every render. You can use the useCallback hook to make sure that the same function is being persisted between renders and it will work as expected.
useCallback(debounce(loadData, 1000), []);
const { useState, useCallback } = React;
const { debounce } = _;
function App() {
const [filter, setFilter] = useState("");
const debounceLoadData = useCallback(debounce(console.log, 1000), []);
function handleFilterChange(event) {
const { value } = event.target;
setFilter(value);
debounceLoadData(value);
}
return <input value={filter} onChange={handleFilterChange} />;
}
ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.min.js"></script>
<script src="https://unpkg.com/react#16/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom#16/umd/react-dom.development.js"></script>
<div id="root"></div>
To add onto Tholle's answer: if you want to make full use of hooks, you can use the useEffect hook to watch for changes in the filter and run the debouncedLoadData function when that happens:
const { useState, useCallback, useEffect } = React;
const { debounce } = _;
function App() {
const [filter, setFilter] = useState("");
const debounceLoadData = useCallback(debounce(fetchData, 1000), []);
useEffect(() => {
debounceLoadData(filter);
}, [filter]);
function fetchData(filter) {
console.log(filter);
}
return <input value={filter} onChange={event => setFilter(event.target.value)} />;
}
ReactDOM.render(<App />, document.getElementById("root"));
You must remember the debounced function between renders.
However, you should not use useCallback to remember a debounced (or throttled) function as suggested in other answers. useCallback is designed for inline functions!
Instead use useMemo to remember the debounced function between renders:
useMemo(() => debounce(loadData, 1000), []);
I hope this post will get you to the solution ,
You don't have to use external library for Debouncing you can create your own custom hook follow my steps
step(1):- Create the custom hook of Debouncing
import { useEffect ,useState} from 'react';
export const UseDebounce = (value,delay)=>{
const [debouncedValue,setDebouncedValue]= useState();
useEffect(()=>{
let timer = setTimeout(()=>setDebouncedValue(value),delay)
return ()=> clearTimeout(timer);
},[value])
return debouncedValue
}
step(2) :- Now create the file in which you want to add throttle
import React from 'react'
import { useEffect } from 'react';
import { useState } from 'react';
import {UseDebounce} from "./UseDebounce";
function Test() {
const [input, setInput] = useState("");
const debouncedValue = UseDebounce(input,1000);
const handleChange = (e)=>{
setInput(e.target.value)
}
useEffect(()=>{
UseDebounce&& console.log("UseDebounce",UseDebounce)
},[UseDebounce])
return (
<div>
<input type="text" onChange={handleChange} value={input}/>
{UseDebounce}
</div>
)
}
export default Test;
NOTE:- To test this file first create react app then embrace my files in it
Hope this solution worthwhile to you

Resources