React Page Scrolls on top after re render by useEffect - javascript

My problem - whenever i use <Tabs> component the onChange method calls the handleTabChange function. The component gets called again and the after repainting the useEffect gets called. This causes my page to scroll on top.
How do i make sure the handleChange and useEffect does not cause the page to scroll on top?
export function VarianceGraph({ variancesGraphData }) {
if (variancesGraphData === undefined) {
return null;
}
const [currentProject, setCurrentProject] = useState('All');
const [currentTab, setCurrentTab] = useState('ABCD');
const projectCodes = prepareProjectCodesForConsumption(variancesGraphData);
let data = {};
function handleProjectChange(event) {
const { value } = event.target;
setCurrentProject(value);
}
function handleTabChange(event, tabValue) {
setCurrentTab(tabValue);
}
data = prepareProjectDataForRechartsConsumption(currentProject, variancesGraphData);
const hideABCD = data.ABCD.length < 1;
const hideBCDF = data['BCDF'].length < 1;
useEffect(() => {
if (hideABCD && hideBCDF) {
setCurrentTab('None');
} else if (hideABCD) {
setCurrentTab('BCDF');
} else if (hideBCDF) {
setCurrentTab('ABCD');
}
});
return (
<Grid container direction="row" justify="flex-start" alignItems="flex-start">
<Grid item xs={12}>
I have tried -
I need to have the setCurrentTab in some function so as to not render infinitely.
I have tried using the useLayoutEffect but it has the same behavior.
I can write the component code further if required. Please help. Thanks in advance.

If I am understanding right, you only want your useEffect code run once after VarianceGraph render.
Try to add a second argument [] to your useEffect, otherwise the code in useEffect will run after any state change.

I went ahead and searched the problem and found the below solutions
Keep scroll-y after rerender
But the problem is the getSnapshotBeforeUpdate is not available in hooks
as can be seen here
getSnapshotBeforeUpdate using react hooks
So, the best option is to convert the component to a class component and follow the example here : https://reactjs.org/docs/react-component.html#getsnapshotbeforeupdate
Sample code, in your case, you might have to bind the document.body.scrollHeight
class ScrollingList extends React.Component {
constructor(props) {
super(props);
this.listRef = React.createRef();
}
getSnapshotBeforeUpdate(prevProps, prevState) {
// Are we adding new items to the list?
// Capture the scroll position so we can adjust scroll later.
if (prevProps.list.length < this.props.list.length) {
const list = this.listRef.current;
return list.scrollHeight - list.scrollTop;
}
return null;
}
componentDidUpdate(prevProps, prevState, snapshot) {
// If we have a snapshot value, we've just added new items.
// Adjust scroll so these new items don't push the old ones out of view.
// (snapshot here is the value returned from getSnapshotBeforeUpdate)
if (snapshot !== null) {
const list = this.listRef.current;
list.scrollTop = list.scrollHeight - snapshot;
}
}
render() {
return (
<div ref={this.listRef}>{/* ...contents... */}</div>
);
}
}

Related

How to update an index inside an array with hooks without hitting "Error: Too many re-renders."

I have an array of cells that I need to update based on some user input from a socket server. But whenever I try to update an index using useState(), react complains about "Error: Too many re-renders.". I tried adding a state to indicate that the grid has changed, but that still produces the same error. Please help me :<
const GameUI = ({ height, width }) => {
const [grid, setGrid] = React.useState([]);
const [didUpdate, setDidUpdate] = React.useState(0);
React.useEffect(() => {
for (let index = 0; index < 64; index++) {
const gridItem = <GridSquare key={`${index}`} color="" />;
setGrid((oldArray) => [...oldArray, gridItem]);
}
}, []);
//not re-rendering the modified component
const handleChange = () => {
let board = [...grid]
board[1] = <GridSquare key={`${0}${1}`} color="1" />;
setGrid(board)
// let count = didUpdate;
// count += 1
// setDidUpdate(count)
};
// handleChange();
return (
<div className="App">
<GridBoard grid={grid} />
<ScoreBoard />
<Players />
{handleChange()}
{/* <MessagePopup /> */}
</div>
);
};
Every time you change a state provided from the useState hook, it re-renders your component. You are calling handleChange on every render, which is calling your setGrid state hook. Therefore, you are rendering your component infinitely.
When do you actually need to call handleChange? Every animation frame? Every action event of some kind? Create an appropriate useEffect or useCallback hook (that includes used dependencies [array] as the second parameter), apply the appropriate event to your method and trigger it accordingly. If you go with a useEffect, don't forget to return a function that disables the event handling when the component is eventually unmounted or else you will have duplicate events triggering every time it gets remounted in the same application.

How to implement progress bar in React JS?

I am trying to implement a progress bar in my react js project, but It's not working properly.
Code for child component where I have implemented progress bar -
export const Child = (props: ChildProps): React.ReactElement => {
const { text, showProgress, typeId } = props;
const [progress, setProgress] = React.useState<number>(0);
React.useEffect(() => {
const timer = setInterval(() => {
setProgress((oldProgress) => {
if (oldProgress === 100) {
return 0;
}
const diff = Math.random() * 100;
return Math.min(oldProgress + diff, 100);
});
}, 500);
return () => {
if(progress === 100){
clearInterval(timer);
}
};
}, []);
return (
<Grid container>
<Grid container item direction="column">
<Label>{text}</Label>
<Select
options={myOptions}
value={mySelectedValue}
onChange={(selectedOption: Select<string, string>) =>
onChange('TYPE', selectedOption.value, typeId)
}
/>
</Grid>
{showProgress && (<ProgressBarContainer>
<ProgressBar color={'Blue'} height={'Small'} progress={progress} />
</ProgressBarContainer>)
}
</Grid>
);
}
I have defined a callback for onChange function in parent component. Everytime I call onChange event, it should display the progress.
Code for parent component -
const [showProgress , setShowProgress] = React.useState<boolean>(false);
const [isApiCalled , setIsApiCalled] = React.useState<boolean>(false);
const [myData , setMyData] = React.useState<any>([]);
const onChange = (type: string, value: string, typeid: number): void => {
if (type === 'TYPE') {
setShowProgress(true);
setIsApiCalled(true);
}
setMyData(updateData); // I haven't written the code for it here.
}
const saveData = (isSave: boolean): void => {
callAPI(saveRequestData); // calling API
setShowProgress(false);
}
React.useEffect(()=> {
saveData(true)
setIsApiCalled(false);
}, [isApiCalled])
return (
<Child myData={myData} onChange={onChange} showProgress={showProgress} />;
);
Everytime I change the data in Child, it should call progress as I am setting the progress to be true. It is calling the progress but not in a proper way. Like progress should start from 0 everytime I do call onChange and should increase continue. But first time it starts from 0 but it never continue increasing and afterwards it never starts from 0. I don't know what I am doing wrong. Can anyone help ?
In my app I have included a progress bar too, more specifically a circular progress bar. I have used an already developed package. You can follow the instructions here: https://www.npmjs.com/package/react-circular-progressbar
A simple implementation:
import { CircularProgressbar } from "react-circular-progressbar";
const MyComponent: React.FC = () => {
const points = 30000 //only of testing
const percentage = (points / 10000) * 100; //calculate the percentage
return (
<div>
<CircularProgressbar value={percentage} text={points} />
</div>
);
}
export default MyComponent;
For custom styling and CSS, you can copy and modify the code here: https://github.com/kevinsqi/react-circular-progressbar/blob/HEAD/src/styles.css
It seems because of closure, useEffect() captures the initial value there on mount and never updates it. Since Child is already mounted and useEffect() is not called after mount setInterval always has initial value of progress that is 0 and progress is not updated.
Similar problem is explained by Dan Abramov in his blog:
Excerpt from his blog about this problem
The problem is that useEffect captures the count from the first
render. It is equal to 0. We never re-apply the effect so the closure
in setInterval always references the count from the first render, and
count + 1 is always 1. Oops!
To solve this, you can use useRef hook to capture callback inside interval and on re-render just update it with new reference of the callback. So that setInterval points to updated callback after every interval. Something like:
useEffect(() => {
savedCallback.current = callback;
});
You can refer Dan's blog for clear understanding
https://overreacted.io/making-setinterval-declarative-with-react-hooks/

Update state with setState ReactJs

I'm trying to update state variable when button click.but my issue is,it's update once with correct data then again it updated with constructor defined data.
constructor(props) {
super(props);
this.state = {
popupshow: [{ check: false, id: '' }]
}
}
componentDidUpdate(prevProps, prevState) {
console.log("this.state.popupshow",this.state.popupshow)
}
Details(type){
this.state.popupshow[i].id = type
this.state.popupshow[i].check = true;
this.setState({ popupshow: this.state.popupshow });
}
render() {
return (
<a onClick={() => this.Details("Tv Series")}>Update </>
)
}
my console.log is like bellow
You should not update React state directly. You should always update/set React state via setState method.
These lines are against React principal
this.state.popupshow[i].id = type
this.state.popupshow[i].check = true;
Update your Details as follows
Details(type){
let { popupshow } = this.state;
let i = 0;
popupshow[i].id = type
popupshow[i].check = true;
this.setState({ popupshow });
}
Note I dont have idea of variable i so assumed that as 0
I think you should rewrite details functions like :
Details(type, i){
const popupDetail = Object.assign([], this.state.popupshow);
popupDetail[i].id = type
popupDetail[i].check = true;
this.setState({ popupshow: popupDetail });
}
you are setting popupshow: this.state.popupshow this is causing forceupdate which re renders the component hence its value gets reset.
I totally agree with the other answers have given for the question, however there are few things worth noting is you might wanna add the function to the context.
The argument in favour of adding these lines to the constructor is so that the new bound functions are only created once per instance of the class. You could also use
onClick={this.Details.bind(this, "Tv Series")}
or (ES6):
onClick={() => this.Details("Tv Series")}
but either of these methods will create a new function every time the component is re-rendered.
Then change the function to arrow fucntion too like
Details(type, i){
const popupDetail = Object.assign([], this.state.popupshow);
popupDetail[i].id = type
popupDetail[i].check = true;
this.setState({ popupshow: popupDetail });
}

React componentWillReceiveProps not updating state

I've got this React parent component here. The children components at this point are just returning dropdown menus. I expected that componentWillReceiveProps would update the state here, which in turn should be passed to StopList as props. However, when state.selectedSub is changed through handleSubSelect, nothing happens and StopList doesn't receive any props.
Is my mistake with the asynchronous nature of componentWillReceiveProps? Is it in the wrong place in my code? Am I using the wrong lifecycle method?
// We're controlling all of our state here and using children
// components only to return lists and handle AJAX calls.
import React, { Component } from 'react';
import SubList from './SubList';
import StopList from './StopList';
class SubCheck extends Component {
constructor (props) {
super(props);
this.state = {
selectedSub: '--',
selectedStop: null,
stops: ['--'],
};
this.handleSubSelect.bind(this);
this.handleStopSelect.bind(this);
}
// We want the user to be able to select their specific subway
// stop, so obviously a different array of stops needs to be
// loaded for each subway. We're getting those from utils/stops.json.
componentWillReceiveProps(nextProps) {
var stopData = require('../utils/stops');
var stopsArray = [];
var newSub = nextProps.selectedSub
for(var i = 0; i < stopData.length; i++) {
var stop = stopData[i];
if (stop.stop_id.charAt(0) === this.state.selectedSub) {
stopsArray.push(stop.stop_name);
}
}
if (stopsArray.length !== 0 && newSub !== this.state.selectedSub) {
this.setState({stops: stopsArray});
}
}
handleSubSelect(event) {
this.setState({selectedSub:event.target.selectedSub});
}
handleStopSelect(event) {
this.setState({selectedStop:event.target.selectedStop})
}
render() {
return (
<div>
<SubList onSubSelect={this.handleSubSelect.bind(this)}/>
<StopList stops={this.state.stops} onStopSelect={this.handleStopSelect.bind(this)}/>
</div>
);
}
}
export default SubCheck;
You are duplicating data, and causing yourself headaches that aren't necessary.
Both selectedSub and selectedStop are being stored as props and as state attributes. You need to decide where this data lives and put it in a singular location.
The problem you are encountering entirely revolves round the fact that you are changing the state attribute and expecting this to trigger a change to your props. Just because they share a name does not mean they are the same value.

How to detect overflow of React component without ReactDOM?

Basically i want to be able to detect if a react component has children which are overflowing. Just as in this question. I have found that the same thing is possible using ReactDOM, however i cannot/should not use ReactDOM. I don't see anything on the suggested alternative,ref, that is equivalent.
So what i need to know is if it is possible to detect overflow within a react component under these conditions. And to the same point, is it possible to detect width at all?
In addition to #jered's excellent answer, i'd like to mention the qualifier that a ref will only return an element that directly has access to the various properties of regular DOM elements if the ref is placed directly on a DOM element. That is to say, it does not behave in this way with Components.
So if you are like me and have the following:
var MyComponent = React.createClass({
render: function(){
return <SomeComponent id="my-component" ref={(el) => {this.element = el}}/>
}
})
and when you attempt to access DOM properties of this.element (probably in componentDidMount or componentDidUpdate) and you are not seeing said properties, the following may be an alternative that works for you
var MyComponent = React.createClass({
render: function(){
return <div ref={(el) => {this.element = el}}>
<SomeComponent id="my-component"/>
</div>
}
})
Now you can do something like the following:
componentDidUpdate() {
const element = this.element;
// Things involving accessing DOM properties on element
// In the case of what this question actually asks:
const hasOverflowingChildren = element.offsetHeight < element.scrollHeight ||
element.offsetWidth < element.scrollWidth;
},
The implementation of the solution proposed by #Jemar Jones:
export default class OverflowText extends Component {
constructor(props) {
super(props);
this.state = {
overflowActive: false
};
}
isEllipsisActive(e) {
return e.offsetHeight < e.scrollHeight || e.offsetWidth < e.scrollWidth;
}
componentDidMount() {
this.setState({ overflowActive: this.isEllipsisActive(this.span) });
}
render() {
return (
<div
style={{
width: "145px",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
overflow: "hidden"
}}
ref={ref => (this.span = ref)}
>
<div>{"Triggered: " + this.state.overflowActive}</div>
<span>This is a long text that activates ellipsis</span>
</div>
);
}
}
Yep, you can use ref.
Read more about how ref works in the official documentation: https://facebook.github.io/react/docs/refs-and-the-dom.html
Basically, ref is just a callback that is run when a component renders for the first time, immediately before componentDidMount is called. The parameter in the callback is the DOM element that is calling the ref function. So if you have something like this:
var MyComponent = React.createClass({
render: function(){
return <div id="my-component" ref={(el) => {this.domElement = el}}>Hello World</div>
}
})
When MyComponent mounts it will call the ref function that sets this.domElement to the DOM element #my-component.
With that, it's fairly easy to use something like getBoundingClientRect() to measure your DOM elements after they render and determine if the children overflow the parent:
https://jsbin.com/lexonoyamu/edit?js,console,output
Keep in mind there is no way to measure the size/overflow of DOM elements before they render because by definition they don't exist yet. You can't measure the width/height of something until you render it to the screen.
I needed to achieve this in React TypeScript, as such here is the updated solution in TypeScript using React Hooks. This solution will return true if there are at least 4 lines of text.
We declare the necessary state variables:
const [overflowActive, setOverflowActive] = useState<boolean>(false);
const [showMore, setShowMore] = useState<boolean>(false);
We declare the necessary ref using useRef:
const overflowingText = useRef<HTMLSpanElement | null>(null);
We create a function that checks for overflow:
const checkOverflow = (textContainer: HTMLSpanElement | null): boolean => {
if (textContainer)
return (
textContainer.offsetHeight < textContainer.scrollHeight || textContainer.offsetWidth < textContainer.scrollWidth
);
return false;
};
Lets build a useEffect that will be called when overflowActive changes and will check our current ref object to determine whether the object is overflowing:
useEffect(() => {
if (checkOverflow(overflowingText.current)) {
setOverflowActive(true);
return;
}
setOverflowActive(false);
}, [overflowActive]);
In our component's return statement, we need to bind the ref to an appropriate element. I am using Material UI coupled with styled-components so the element in this example will be StyledTypography:
<StyledTypography ref={overflowingText}>{message}</StyledTypography>
Styling the component in styled-components:
const StyledTypography = styled(Typography)({
display: '-webkit-box',
'-webkit-line-clamp': '4',
'-webkit-box-orient': 'vertical',
overflow: 'hidden',
textOverflow: 'ellipsis',
});
The same could be achieved using React hooks:
The first thing you need would be a state which holds boolean values for text open and overflow active:
const [textOpen, setTextOpen] = useState(false);
const [overflowActive, setOverflowActive] = useState(false);
Next, you need a ref on the element you want to check for overflowing:
const textRef = useRef();
<p ref={textRef}>
Some huuuuge text
</p>
The next thing is a function that checks if the element is overflowing:
function isOverflowActive(event) {
return event.offsetHeight < event.scrollHeight || event.offsetWidth < event.scrollWidth;
}
Then you need a useEffect hook that checks if the overflow exists with the above function:
useEffect(() => {
if (isOverflowActive(textRef.current)) {
setOverflowActive(true);
return;
}
setOverflowActive(false);
}, [isOverflowActive]);
And now with those two states and a function that checks the existence of an overflowing element, you can conditionally render some element (eg. Show more button):
{!textOpen && !overflowActive ? null : (
<button>{textOpen ? 'Show less' : 'Show more'}</button>
)}
To anyone who wonder how it can be done with hooks and useRef:
// This is custom effect that calls onResize when page load and on window resize
const useResizeEffect = (onResize, deps = []) => {
useEffect(() => {
onResize();
window.addEventListener("resize", onResize);
return () => window.removeEventListener("resize", onResize);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [...deps, onResize]);
};
const App = () => {
const [isScrollable, setIsScrollable] = useState(false);
const [container, setContainer] = useState(null);
// this has to be done by ref so when window event resize listener will trigger - we will get the current element
const containerRef = useRef(container);
containerRef.current = container;
const setScrollableOnResize = useCallback(() => {
if (!containerRef.current) return;
const { clientWidth, scrollWidth } = containerRef.current;
setIsScrollable(scrollWidth > clientWidth);
}, [containerRef]);
useResizeEffect(setScrollableOnResize, [containerRef]);
return (
<div
className={"container" + (isScrollable ? " scrollable" : "")}
ref={(element) => {
if (!element) return;
setContainer(element);
const { clientWidth, scrollWidth } = element;
setIsScrollable(scrollWidth > clientWidth);
}}
>
<div className="content">
<div>some conetnt</div>
</div>
</div>
);
};

Categories