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/
Related
Can anyone tell me why at button click, the value outputted to the console is always one unit smaller than displayed on the screen?
The values are not in sync as expected.
Example below in React
In Child:
import React, {useState } from "react";
export const ChildComp = ({getNumProps}) => {
const [num, setNum] = useState(0);
const onPlusClick = () => {
if (num< 12) {
setNum(num + 1);// num does not increase immediately after this line, except when focus reenters here on second method call
}
getNumProps(num);
}
return(
<div>
<button onClick={onPlusClick}>
Click to increment
</button>
{num}
</div>
);
}
In parent
import { ChildComp } from "./ChildComp"
export const ParentComp = () => {
const getNum= (num) => {
console.log(num);
}
return (<ChildComp getNumProps={getNum}/>)
}
The page initially shows 0
When I click once the number increments to 1, but console displays 0
When I click once the number increments to 2, but console displays 1
I should see in the console the same as the page display
Appreciate if you can leave a commen on how the question can be improved.
This is a child to parent communication example. Also, any objections about standards used, please let me know.
Thanks.
Update: I notice the values would be in sync if
instead of getNumProps(num);
I did getNumProps(num + 1); but that doesn't change the fact that previously on this line
setNum(num + 1);, as already pointed out in the comment, num does not increase immediately after this line, except when focus reenters here on second method call. Not sure why.
The prop function getNumProps is a side effect, and should be put into a hook, instead of inside of that onPlusClick function.
Instead, do this:
useEffect(() => {
getNumProp(num);
}, [num]);
Alternatively, to avoid the error: "React Hook useEffect has a missing dependency: 'getNumProps'. See this doc on using the useCallback hook
const callback = useCallback(() => {
getNumProp(num);
}, [num]);
function onPlusClick(...) {
...
callback();
}
The change to the state of num will cause a re-render of the child component, not the parent.
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.
My React app uses setTimeout() and setInterval(). Inside them, I need to access the state value. As we know, closures are bound to their context once created, so using state values in setTimeout() / setInterval() won't use the newest value.
Let's keep things simple and say my component is defined as such:
import { useState, useEffect, useRef } from 'react';
const Foo = () => {
const [number, setNumber] = useState(0);
const numberRef = useRef(number);
// Is this common? Any pitfalls? Can it be done better?
numberRef.current = number;
useEffect(
() => setInterval(
() => {
if (numberRef.current % 2 === 0) {
console.log('Yay!');
}
},
1000
),
[]
);
return (
<>
<button type="button" onClick={() => setNumber(n => n + 1)}>
Add one
</button>
<div>Number: {number}</div>
</>
);
};
In total I came up with 3 ideas how to achieve this, is any of them a recognized pattern?
Assigning state value to ref on every render, just like above:
numberRef.current = number;
The benefit is very simplistic code.
Using useEffect() to register changes of number:
useEffect(
() => numberRef.current = number,
[number]
);
This one looks more React-ish, but is it really necessary? Doesn't it actually downgrade the performance when a simple assignment from point #1 could be used?
Using custom setter:
const [number, setNumberState] = useState(0);
const numberRef = useRef(number);
const setNumber = value => {
setNumberState(value);
numberRef.current = value;
};
Is having the same value in the state and the ref a common pattern with React? And is any of these 3 ways more popular than others for any reason? What are the alternatives?
2021-10-17 EDIT:
Since this looks like a common scenario I wanted to wrap this whole logic into an intuitive
useInterval(
() => console.log(`latest number value is: ${number}`),
1000
)
where useInterval parameter can always "access" latest state.
After playing around for a bit in a CodeSandbox I've come to the realization that there is no way someone else hasn't already thought about a solution for this.
Lo and behold, the man himself, Dan Abramov has a blog post with a precise solution for our question https://overreacted.io/making-setinterval-declarative-with-react-hooks/
I highly recommend reading the full blog since it describes a general issue with the mismatch between declarative React programming and imperative APIs. Dan also explains his process (step by step) of developing a full solution with an ability to change interval delay when needed.
Here (CodeSandbox) you can test it in your particular case.
ORIGINAL answer:
1.
numberRef.current = number;
I would avoid this since we generally want to do state/ref updates in the useEffect instead of the render method.
In this particular case, it doesn't have much impact, however, if you were to add another state and modify it -> a render cycle would be triggered -> this code would also run and assign a value for no reason (number value wouldn't change).
2.
useEffect(
() => numberRef.current = number,
[number]
);
IMHO, this is the best way out of all the 3 ways you provided. This is a clean/declarative way of "syncing" managed state to the mutable ref object.
3.
const [number, setNumberState] = useState(0);
const numberRef = useRef(number);
const setNumber = value => {
setNumberState(value);
numberRef.current = value;
};
In my opinion, this is not ideal. Other developers are used to React API and might not see your custom setter and instead use a default setNumberState when adding more logic expecting it to be used as a "source of truth" -> setInterval will not get the latest data.
You have simply forgotten to clear interval. You have to clear the interval on rendering.
useEffect(() => {
const id = setInterval(() => {
if (numberRef.current % 2 === 0) {
console.log("Yay!");
}
}, 1000);
return () => clearInterval(id);
}, []);
If you won't clear, this will keep creating a new setInterval with every click. That can lead to unwanted behaviour.
Simplified code:
const Foo = () => {
const [number, setNumber] = useState(0);
useEffect(() => {
const id = setInterval(() => {
if (number % 2 === 0) {
console.log("Yay!");
}
}, 1000);
return () => clearInterval(id);
}, [number]);
return (
<div>
<button type="button" onClick={() => setNumber(number + 1)}>
Add one
</button>
<div>Number: {number}</div>
</div>
);
};
I am having a simple state, which defines a price of the coffee for employees, if the radio button is checked.
const [coffee, setCoffee] = useState(0);
const [checkedCoffee, setCheckedCoffee] = useState(true);
This is how I am setting up the new state:
useEffect(() => {
if (checkedCoffee) {
setCoffee(employees * 40);
} else {
setCoffee(0);
}
}, [coffee])
But I want to have another option, which will reduce the coffee price 50% and this is how I am trying to handle it:
const handleDivision = () => {
setCoffee(coffee / 2);
};
And then just calling handleDivision in onClick button.
<button onClick={handleDivision}>division button</button>
The result is just refreshing the price divided by 2 - so something is happening, but it never actually set up the 50% less price.
Where does my code conflicts?
You can see in the documentation for useEffect that it states:
The function passed to useEffect will run after the render is committed to the screen. Think of effects as an escape hatch from React’s purely functional world into the imperative world.
Based on the statement from the documentation, it looks like your useEffect function will execute again after you've clicked your button and refresh the value based on the useEffect function e.g.
if (checkedCoffee) {
setCoffee(employees * 40);
} else {
setCoffee(0);
}
In order to fix this you can remove the useEffect call in component and execute the above code inplace or as a seperate function call.
Add checkedCoffee to useEffect dependency list.
useEffect(() => {
if (checkedCoffee) {
setCoffee(employees * 40);
} else {
setCoffee(0);
}
}, [checkedCoffee]);
And then:
...
const handleDivision = () => {
setCoffee(coffee / 2);
};
return <button onClick={handleDivision}>division button</button>;
...
Look at the example here → https://codesandbox.io/s/brave-grothendieck-floly?file=/src/App.js
Try this :
<button
onClick={()=>{
handleDivision()
}}>
division button
</button>`
Instead of :
<button onClick={handleDivision}>division button</button>
Check out this sandbox:
https://codesandbox.io/s/optimistic-cache-4c9ud?file=/src/App.js
Try this:
const handleDivision = () => {
setCoffee(prevCoffePrice => prevCoffePrice / 2);
};
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>
);
}
}