I have a functional component managing several states, amongst other things a state, which stores the index, with which I am rendering a type of table, when clicking a suitable button. OnClick that button calls a callback function, which runs a click handler. That click handler changes the index state, to the same 'index' as the array entry, in which I store an object with information for the rendering of a child component.
I would expect, that onClick the state would change before the rendering happens, so the component could render correctly. Yet it only happens a render later.
I already tried calling the useEffect-hook, to re-render, when that index state changes, but that didn't help neither.
Here is a shortened version of the code:
export const someComponent = () => {
[index, setIndex] = useState(-1);
const handleClick = (id) => {
setIndex(id);
// This is a function, I use to render the table
buildNewComponent(index);
}
}
Further 'down' in the code, I got the function, which is rendering the table entries. There I pass the onClick prop in the child component of the table as following:
<SomeEntryComponent
onClick={() => handleClick(arrayEntry.id)}
>
// some code which is not affecting my problem
</SomeEntryComponent>
So as told: when that onClick fires, it first renders the component when one presses it the second time, because first then the state changes.
Could anyone tell me why that happens like that and how I could fix it to work properly?
As other have stated, it is a synchronicity issue, where index is being updated after buildComponent has been invoked.
But from a design standpoint, it would be better to assert index existence by its value, as opposed to flagging it in a handler. I don't know the details behind buildComponent, but you can turn it into a conditional render of the component.
Your component rendering becomes derived from its data, as opposed to manual creation.
export const someComponent = () => {
[index, setIndex] = useState(-1);
const handleClick = (id) => setIndex(id);
const indexHasBeenSelected = index !== -1
return (
<div>
{indexHasBeenSelected && <NewComponent index={index} />}
</div>
)
}
When calling buildNewComponent the index is not yet updated. You just called setState, there is no guarantee that the value is updated immediately after that. You could use id here or call buildNewComponent within a useEffect that has index as its dependency.
I believe that you can have the correct behavior if you use useEffect and monitor index changes.
export const someComponent = () => {
[index, setIndex] = useState(-1);
useEffect(() => {
// This is a function, I use to render the table
buildNewComponent(index);
}, [buildNewComponent, index])
const handleClick = (id) => {
setIndex(id);
}
}
The process will be:
When the use clicks and call handleClick it will dispatch setIndex with a new id
The index will change to the same id
The useEffect will see the change in index and will call buildNewComponent.
One important thing is to wrap buildNewComponent with useCallback to avoid unexpected behavior.
Related
When a component re-renders as a result of calling the state setter function returned by useState, does the entire function component get called again or only parts of the function component get called?
function MyComponent() {
let [count, setCount] = useState(0);
const handleChange = () {
setCount((prevCount) => prevCount+1)
}
return (
<p onClick={handleChange}>
Count = {count}
</p>
)
}
In the above example, when we click the paragraph element, handleChange() gets called which in turn calls the state setter function setCount(). This causes our component to re-render. But when the component re-renders( or the function MyComponent gets called), the useState(0) function gets called again. This should set the initial value of count to 0 again. So shouldn't the counter be stuck at 0 instead of progressing to 1, 2, 3 and so on?
Whole functional component will be rerendered. If the props value which you are passing to the child component is not changing then you can prevent the rerendering of child component by using React.memo
Let say your component tree has parent and has children called childOne and childtwo.
if you change the state of the ChildOne;Only the ChildOne get rendered, Not Parent and childtwo get rendered. But if you change the state of the parent, whole component tree get retendered
This is the idea of hooks. They use closures.
If it was let count = 0, then yes, if would reset to 0, this is why it is let [count] = useState(0)
The whole thing gets re-render and everything is getting re-calculated.
The argument you pass in useState / useRef is used only once, when the component mounts. If it is expensive operation, it recalculate on each render (there are ways to improve this), but the value of it is used only once. Then when you call setState it updates the value.
They work like this:
let ref = {};
const useRef = (defaultValue) => {
if (!ref.current) {
ref.current = defaultValue
}
return ref
}
The hook argument is used only once.
Short story
I want to know if this is ok to do:
const onClick = useCallback(() => {
setHoveredItem((hovered)=>{
setSelectedData(hovered);
return hovered;
});
},[]);
Just using the useState setter to get the value, 'removing' the useCallback's dependency to avoid re-renders. Notice the empty dependency array.
Is that an acceptable way to do things?
Long story
I am working on an application with an chart. Re-renders to the chart are really bad (they basically break). So I used React.memo. All good until I needed to pass event listeners:
<MemoChart
{...{ data, onHover, onClick }}>
</MemoChart>
On click, I want to store the hovered item.
Initially, this was my onClick function:
const onClick = () => {
setSelectedData(hoveredItem);
}
But of course, every hover event re-renders the parent, which causes the chart to re-render. Use Callback on onClick would work, except that if I add a dependency on hoveredItem, it will do nothing.
So this is what I did. It works, but I have never seen it done and I wonder if it is ok to do:
const onClick = useCallback(() => {
setHoveredItem((hovered)=>{
setSelectedData(hovered);
return hovered;
});
},[]);
UPDATED
For your case, I think you can use useRef to keep hovered data instead of a state.
const hoveredRef = React.useRef()
const onHover = useCallback((hovered) => {
hoveredRef.current = hovered //won't make re-rendering
}, [])
const onClick = useCallback(() => {
setSelectedData(hoveredRef.current)
}, [])
OLD ANSWER
<MemoChart
{...{ data, onHover, onClick }}>
</MemoChart>
Your problem is these events onHover and onClick will be initialized newly every time which will cause unexpected renderings. If you have a complex computation on your component, it will be laggy on renderings.
In your onClick case with useCallback, it works but it will create more trouble in state handlers because it's lacking dependencies which means it's considering that function will be the same all the time, but in fact, your function is relying on hovered state should be changed on events.
I'd propose that you should pass dependencies to align with your used states in functions.
const onHover = useCallback((hovered) => {
setHoveredItem(hovered);
}, []) //only initialize this function once after the component mounted
const onClick = useCallback(() => {
setSelectedData(hovered);
},[hovered]); //initialize this function again when `hovered` state changes
const onClick = useCallback(() => {
setHoveredItem((hovered)=>{
setSelectedData(hovered);
return hovered;
});
},[]);
This is not wrong, but it's a hack that helps you getting the fresh state if you have closures issues.
In your case there is no reason since you are not working with closures, and the problem is just that you set an empty deps array.
Now you say that this does not work:
const onClick = useCallback(() => {
setSelectedData(hovered);
},[hovered]);
But this MUST work, unless you are doing something wrong when you call setHovereditem.
I am trying to build an ecommerce website, and I hit a problem I cannot seem to resolve. I am very new to react and JS so have some patience please :)
I declared 4 useStates in my app.js:
const [elementeDinState, setElementeDinState] = useState([]);
const [currentCategorie, setCurrentCategorie] = useState("Acasa");
const [subCategorie, setSubcategorie] = useState([]);
const [cartContents, setCartContents] = useState([]);
const fetchData = useCallback(async () => {
const data = await getCategories();
setElementeDinState(data);
}, []);
useEffect(() => {
fetchData().catch(console.error);
}, [fetchData]);
const changeHeader = (dataFromMenuItem) => {
setCurrentCategorie(dataFromMenuItem);
};
const changeCopiiContent = (data1FromThere) => {
setSubcategorie(data1FromThere);
};
const changeCart = (dataFromCart) => {
setCartContents(dataFromCart);
};
I am passing the functions to change those states to different child components as props. my problem is, when I add items to cart it triggers a re render of my component (products listing component) that should not be affected by cartContents and that resets the state of said component to the initial value that changes the items being shown. does useState hook create a single global state comprised of all those states?
If these useState are defined in the app.js and then passed down, when a child will use them chasing the state will happen in the app.js so all the children of <App /> will be re-rendered.
I guess that your app.js looks similar:
function App() {
const [elementeDinState, setElementeDinState] = useState([]);
// ...and the other hooks and methods
return (
<cartContents setElementDinState={setElementeDinState} />
<ProductList />
)
}
In this case the state is in the component so when <CartContents /> changes it, it will trigger a re-render of the and all its children <ProductList /> included.
To avoid this problem think better when each piece of state needs to be and put the state as near as possibile to that component. For example, if the state of the cart does not influence the Product list. Move the useState in the <Cart /> component.
From what I understand, your problem is that you're simply resetting the cartContents state every time you call the changeCart function, correct?
What you probably want, is to add (or remove ?) the item to the cart, like this?
const changeCart = (dataFromCart) => {
setCartContents(oldContents => [...oldContents, dataFromCart]);
};
Here is a description of useState from the oficial site:
useState is a Hook (...). We call it inside a function component to add some local state to it
So it creates just a local state.
About your problem, We need more information, but I believe that some parent component of that widget is trying to render other component instead of your the component that you wanted (let's call it "ProblemComponent") and rendering you ProblemComponent from scratch again, before you can see it.
it's something like that:
function ParentComponent(props: any) {
const isLoading = useState(false);
// Some logic...
if(isLoading) {
return <LoadingComponent/>;
}
return <ProblemComponent/>;
}
If that doesn't work you can also try to use React.memo() to prevent the ProblemComponent to update when it props change.
well, seems like I wanted to change the way react works so I figured out a work around, based on what you guys told me. I declared the state of the productsComponent in the parent component and adding to cart now doesn't force a refresh of the items being shown. thank you!
I have a situation that an item outside the component might influence the item in the backend. ex: change the value of one of the properties that are persisted, let's say the item status moves from Pending to Completed.
I know when it happens but since it is outside of a component I need to tell to the component that it is out of sync and re-fetch the data. But from outside. I know you can pass props calling the render method again. But the problem is I have a reducer and the state will pick up the last state and if I use an prop to trigger an effect I get into a loop.
Here is what I did:
useEffect(() => {
if (props.effect && !state.effect) { //this runs when the prop changes
return dispatch({ type: props.effect, });
}
if (state.effect) { // but then I get here and then back up and so on
return ModelEffect[state.effect](state?.data?.item)}, [state.status, state.effect, props.effect,]);
In short since I can't get rid of the prop the I get the first one then the second and so on in an infinite loop.
I render the root component passing the id:
render(html`<${Panel} id=${id}/>`,
document.getElementById('Panel'));
Then the idea was that I could do this to sync it:
render(html`<${Panel} id=${id} effect="RELOAD"/>`,
document.getElementById('Panel'));
any better ways to solve this?
Thanks a lot
I resolved it by passing the initialized dispatch function to a global.
function Panel (props) {
//...
const [state, dispatch,] = useReducer(RequestReducer, initialData);
//...
useEffect(() => {
//...
window.GlobalDispatch = dispatch;
//...
}, [state.status, state.effect,]);
with that I can do:
window.GlobalDispatch({type:'RELOAD'});
Is it possible to reload a component in react without using the help of a function like onClick. I want my component to reload again after it just got loaded.
I tried using
window.location.reload(false); in the constructor. I even tried using useEffect() but they make the page reload infinitely.
Is there any work around?
I have seen people accomplish this by setting a dummy setState method that is strictly used to trigger a refresh. Consider this component:
export const Proof = () => {
console.log("Component re-rendered")
const [dummyState,rerender] = React.useState(1);
const onClick = () => {
rerender(dummyState + 1);
}
React.useEffect( () => {
console.log("dummyState's state has updated to: " + dummyState)
}, [dummyState])
return(
<div>
<button onClick={onClick}>reRender</button>
</div>
)
}
Whenever you want to rerender the component you just need to trigger a change in the dummyState. Clicking the button will cause the components state to change, which will cause the component to rerender (i used a console.log() for proof). It is worth noting that simply calling a method that instantly changes the state of the component will result in an infinite loop.
From reading your comment above, it sounds like you are passing a value as a prop to a child component and you would like the value to be recalculated as soon as any interaction with that component occurs. Ideally, the interaction itself should cause the recalculation. But if you would just like to quickly recalculate the value and rerender the component as soon as it renders then i think this would work:
export const Proof = () => {
console.log("Component re-rendered")
const [dummyState,rerender] = React.useState(1);
//the empty brackets will cause this useEffect
//statement to only execute once.
React.useEffect( () => {
rerender(dummyState + 1);
}, [])
return(
<div>
<p>dummyState</p>
</div>
)
}
You could also recalculate the value in the useEffect method, as it will get called as soon as the component initially renders but not on any subsequent rerenders (due to the empty brackets as the second parameter to the useEffect method)
Have you tried to enter some dependecies to your useEffect hook ?
Like :
useEffect(() => {
console.log('hello');
}, [isLoad];
Here you're component will re-render only if isLoad is changing.