How to prevent useEffect to be fired up in some events? - javascript

Halo guys, I would like to ask a frustating problem for me :'(
What if we don't want to trigger useEffect in some events for the same state. For example lets say I have a state called "myState" and I have put myState in useEffect's dependency. In button1 I want to update myState which will trigger useEffect, in button2 I want also update myState but I don't want to trigger the useEffect
Is it possible to prevent useEffect to be fired up such I just described?
Thank you

Write UseEffects separately, you can keep on stacking them on one above others if you want to run them independantly

in your state, you should have some kind of a breaker for useEffect. in my example i named it stopUseEffect. if i don't want the useEffect to run, i will set stopUseEffect to true. then in useEffect, first thing to do is to check the breaker, does breaker allow it to run or not (if(myState.stopUseEffect) return;):
const { useState, useEffect } = React
const Test = () => {
const [myState, setMyState] = useState({content: 0, stopUseEffect: false});
useEffect(() => {
if(myState.stopUseEffect) return;
console.log("useEffect ran! myState's content:", myState.content)
}, [myState])
return <div>
<button onClick={() => setMyState(p => ({...p, content: p.content + 1, stopUseEffect: false}))}>btn 1, this button will trigger useEffect.</button> <br/>
<button onClick={() => setMyState(p => ({...p, content: p.content + 1, stopUseEffect: true}))}>btn 2, this button will NOT trigger useEffect.</button>
</div>
}
ReactDOM.createRoot(
document.getElementById("root")
).render(
<Test />
);
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.1.0/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.1.0/umd/react-dom.development.js"></script>

Related

Replicate element in React

Hello thanks for the help, with the following code in react I want to replicate the click button every time I click on it. Currently nothing happens, but if the console shows that the array grows. Here the sandbox:
https://codesandbox.io/s/charming-glitter-0ntxmj?file=/src/App.js
const { useEffect, useState } = React;
function App() {
const btn = [
<button
onClick={() => {
btn.push(btn[0]);
console.log(btn);
}}
>
Click
</button>
];
/* useEffect(()=>{
btn
},[btn])*/
return (
<div className="App">
<div>
{btn.map((e) => {
return e;
})}
</div>
</div>
);
}
ReactDOM.render(<App />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>
Updating a local variable in a function component doesn't work that way. It behaves just like a normal function, btn only exists during the execution of App().
In order to persist values across renders you need to use state. However, updates to state and props are the only things that cause rerenders in the first place, so App is probably only being rendered one time at the beginning.
If you convert this directly to use state, you will run into the anti-pattern of storing components in state. To avoid that, we should modify the logic to only store some generic items in the state array so that our rendering logic can us it to determine how many buttons to render.
Consider the following option:
const { useState } = React;
function App() {
const [btns, setBtns] = useState(['value'])
function handleAdd() {
setBtns((prev) => ([...btns, 'value']));
}
return (
<div className="App">
<div>
{btns.map((e) => (
<button
onClick={handleAdd}
>
Click
</button>
))}
</div>
</div>
);
}
ReactDOM.render(<App />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>
You can simplify even more by only storing the count of buttons in your state:
const { useState } = React;
function App() {
const [btnCount, setBtnCount] = useState(1)
function handleAdd() {
setBtnCount(btnCount + 1);
}
return (
<div className="App">
<div>
{Array.from({ length: btnCount}).map((e) => (
<button
onClick={handleAdd}
>
Click
</button>
))}
</div>
</div>
);
}
ReactDOM.render(<App />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>
Yes, changes are made to your component, and yes you can see them in the console, but
App function is just a function and each time it run it will create the array and it will always have the same value (one element).
React will not update unless state or props has changed for a component.
even if your App function update, in react an update means React will re-run the same function again (you endup always with one element in the array).
So to solve this issue you can use state, even thow App is still a function, the useState will garenty that if the value of the array change, React will remeber the changes for you, and React also will re-render your component when state changes.
Keep in mind also that you can change the value of the state only using the provided function setBtns and mutating the btns will not update your component, and mutation of state is always source of bugs.
const [btns, setBtns] = useState([
<button
onClick={() => setBtns((prev) => [...prev, prev[0]])}
>
Click
</button>
]);

function declared with const uses the old value of useState

I have a React component (functional) that contains a child component modifying the state of the parent component. I am using the hook useState for this.
After the state change, there is a "Next" button in the parent component that executes a function referencing the updated state. The problem is this next function uses the old state from before the state was modified by the child component.
I can't use useEffect here as the function needs to execute on the click of the "Next" button and not immediately after the state change. I did some digging about JavaScript closures, but none of the answers address my specific case.
Here's the code
const ParentComponent = () => {
const [myState, setMyState] = useState(0);
const handleNext = () => {
console.log(myState); // prints 0 which is the old value
}
return (
<ChildComponent modifyState = {setMyState} />
<Button onClick={handleNext} > Next </Button>
)
}
export default ParentComponent;
BTW there are no errors.
It's a little difficult to understand without your ChildComponent code. setMyState suggests that you need to update the increase the state by one when you click the next button, but you can't do that without also passing in the state itself.
An easier (imo) solution is to pass a handler down to the child component that is called when the next button is clicked. The handler then sets the state.
const {useState} = React;
function ChildComponent({ handleUpdate }) {
function handleClick() {
handleUpdate();
}
return <button onClick={handleClick}>Click</button>
}
function Example() {
const [myState, setMyState] = useState(0);
function handleUpdate() {
setMyState(myState + 1);
}
function handleNext() {
console.log(myState);
}
return (
<div>
<ChildComponent handleUpdate={handleUpdate} />
<button onClick={handleNext}>Next </button>
</div>
)
}
// Render it
ReactDOM.render(
<Example />,
document.getElementById("react")
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>
<div id="react"></div>
try to modify like this
<ChildComponent modifyState={(value) => setMyState(value)} />

Logic of rerendering in react function components

I have a simple App component
export default function App() {
const [count, changeCount] = useState(0)
const onIncreaseClick = useCallback(() => {
changeCount(count + 1)
}, [count])
const onPress = useCallback(() => {
alert('pressed')
}, [])
return (<>
<button onClick={onIncreaseClick}>Increase</button>
<ButtonPressMe onClick={onPress} />
</>);
}
I expect that onPress variable contains always the same link since parameters never change
And i expect that my ButtonPressMe component will be rendered just once - with the first App component rendering... because it has just one prop and value of this prop never change... therefore no need to rerender component. Correct?
Inside my ButtonPressMe component i check it with console.log
const ButtonPressMe = ({ onClick }) => {
console.log('Button press Me render')
return <button onClick={onClick}>Press me</button>
}
And against my expectations it rerenders each time when parent component rerenders after Increase button is pressed.
Did i misunderstood something?
sandbox to check
And against my expectations it rerenders each time when parent component rerenders after Increase button is pressed.
Did i misunderstood something?
That's the default behavior in react: when a component renders, all of its children render too. If you want the component to compare its old and new props and skip rendering if they didn't change, you need to add React.memo to the child:
const ButtonPressMe = React.memo(({ onClick }) => {
return <button onClick={onClick}>Press me</button>
})
By default, when a parent component re-renders, all of its child components re-render too.
useCallback hook will preserve the identity of the onPress function but that won't prevent a re-render of the ButtonPressMe component. To prevent a re-render, React.memo() is used. useCallback hook is used to avoid passing a new reference to a function, as a prop to a child component, each time a parent component re-renders.
In your case, combination of React.memo and useCallback hook will prevent a re-render of ButtonPressMe component.
function App() {
const [count, changeCount] = React.useState(0);
const onIncreaseClick = React.useCallback(() => {
changeCount(count + 1);
}, [count]);
const onPress = React.useCallback(() => {
alert("pressed");
}, []);
return (
<div>
<button onClick={onIncreaseClick}>Increase</button>
<ButtonPressMe onClick={onPress} />
</div>
);
}
const ButtonPressMe = React.memo(({ onClick }) => {
console.log("Button press Me render");
return <button onClick={onClick}>Press me</button>;
});
ReactDOM.render(<App/>, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>
<div id="root"></div>
The default behavior in React is to change everything in the App when anything changes, in your case you're changing the state of the parent of your custom button, therefore React re-renders everything including your button.
You can find an explanation on how React decides to re-render components here:
https://lucybain.com/blog/2017/react-js-when-to-rerender/#:~:text=A%20re%2Drender%20can%20only,should%20re%2Drender%20the%20component.

Using useEffect with event listeners

The issue I'm having is that when I set up an event listener, the value the event listener sees doesn't update with the state. It's as if it's bound to the initial state.
What is the correct way to do this?
Simple example:
import React, { useState, useEffect } from "react";
import ReactDOM from "react-dom";
const App = () => {
const [name, setName] = useState("Colin");
const [nameFromEventHandler, setNameFromEventHandler] = useState("");
useEffect(() => {
document.getElementById("name").addEventListener("click", handleClick);
}, []);
const handleButton = () => {
setName("Ricardo");
};
const handleClick = () => {
setNameFromEventHandler(name);
};
return (
<React.Fragment>
<h1 id="name">name: {name}</h1>
<h2>name when clicked: {nameFromEventHandler}</h2>
<button onClick={handleButton}>change name</button>
</React.Fragment>
);
};
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
Gif below, since SO code snippet doesn't work for some reason.
So your problem is that you pass an empty array as the second argument to your effect so the effect will never be cleaned up and fired again. This means that handleClick will only ever be closed over the default state. You've essentially written: setNameFromEventHandler("Colin"); for the entire life of this component.
Try removing the second argument all together so the effect will be cleaned up and fired whenever the state changes. When the effect refires, the function that will be handling the click event that will be closed over the most recent version of your state. Also, return a function from your useEffect that will remove your event listener.
E.g.
useEffect(() => {
document.getElementById("name").addEventListener("click", handleClick);
return () => {
document.getElementById("name").removeEventListener("click", handleClick);
}
});
I think correct solution should be this: codesanbox. We are telling to the effect to take care about its dependency, which is the callback. Whenever it is changed we should do another binding with correct value in closure.
I believe the correct solution would be something like this:
useEffect(() => {
document.getElementById("name").addEventListener("click", handleClick);
}, [handleClick]);
const handleButton = () => {
setName("Ricardo");
};
const handleClick = useCallback(() => {
setNameFromEventHandler(name)
}, [name])
The useEffect should have handleClick as part of its dependency array otherwise it will suffer from what is known as a 'stale closure' i.e. having stale state.
To ensure the useEffect is not running on every render, move the handleClick inside a useCallback. This will return a memoized version of the callback that only changes if one of the dependencies has changed which in this case is 'name'.

React Hooks and Component Lifecycle Equivalent

What are the equivalents of the componentDidMount, componentDidUpdate, and componentWillUnmount lifecycle hooks using React hooks like useEffect?
componentDidMount
Pass an empty array as the second argument to useEffect() to run only the callback on mount only.
function ComponentDidMount() {
const [count, setCount] = React.useState(0);
React.useEffect(() => {
console.log('componentDidMount');
}, []);
return (
<div>
<p>componentDidMount: {count} times</p>
<button
onClick={() => {
setCount(count + 1);
}}
>
Click Me
</button>
</div>
);
}
ReactDOM.render(
<div>
<ComponentDidMount />
</div>,
document.querySelector("#app")
);
<script src="https://unpkg.com/react#16.7.0-alpha.0/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom#16.7.0-alpha.0/umd/react-dom.development.js"></script>
<div id="app"></div>
componentDidUpdate
componentDidUpdate() is invoked immediately after updating occurs. This method is not called for the initial render. useEffect runs on every render including the first. So if you want to have a strict equivalent as componentDidUpdate, you have to use useRef to determine if the component has been mounted once. If you want to be even stricter, use useLayoutEffect(), but it fires synchronously. In most cases, useEffect() should be sufficient.
This answer is inspired by Tholle, all credit goes to him.
function ComponentDidUpdate() {
const [count, setCount] = React.useState(0);
const isFirstUpdate = React.useRef(true);
React.useEffect(() => {
if (isFirstUpdate.current) {
isFirstUpdate.current = false;
return;
}
console.log('componentDidUpdate');
});
return (
<div>
<p>componentDidUpdate: {count} times</p>
<button
onClick={() => {
setCount(count + 1);
}}
>
Click Me
</button>
</div>
);
}
ReactDOM.render(
<ComponentDidUpdate />,
document.getElementById("app")
);
<script src="https://unpkg.com/react#16.7.0-alpha.0/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom#16.7.0-alpha.0/umd/react-dom.development.js"></script>
<div id="app"></div>
componentWillUnmount
Return a callback in useEffect's callback argument and it will be called before unmounting.
function ComponentWillUnmount() {
function ComponentWillUnmountInner(props) {
React.useEffect(() => {
return () => {
console.log('componentWillUnmount');
};
}, []);
return (
<div>
<p>componentWillUnmount</p>
</div>
);
}
const [count, setCount] = React.useState(0);
return (
<div>
{count % 2 === 0 ? (
<ComponentWillUnmountInner count={count} />
) : (
<p>No component</p>
)}
<button
onClick={() => {
setCount(count + 1);
}}
>
Click Me
</button>
</div>
);
}
ReactDOM.render(
<div>
<ComponentWillUnmount />
</div>,
document.querySelector("#app")
);
<script src="https://unpkg.com/react#16.7.0-alpha.0/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom#16.7.0-alpha.0/umd/react-dom.development.js"></script>
<div id="app"></div>
From React docs:
If you’re familiar with React class lifecycle methods, you can think
of useEffect Hook as componentDidMount, componentDidUpdate, and
componentWillUnmount combined.
By that saying they mean:
componentDidMount is sort of useEffect(callback, [])
componentDidUpdate is sort of useEffect(callback, [dep1, dep2, ...]) - the array of deps tell React: "if one of the deps is change, run the callback after rendering".
componentDidMount + componentDidUpdate is sort of useEffect(callback)
componentWillUnmount is sort of the returned function from the callback:
useEffect(() => {
/* some code */
return () => {
/* some code to run when rerender or unmount */
}
)
With the help of Dan Abramov's phrasing from his blog, and some additions of my own:
While you can use those hooks, it’s not an exact equivalent. Unlike componentDidMount and componentDidUpdate, it will capture props and state. So even inside the callbacks, you’ll see the props and state of the specific render (which means in componentDidMount the initial props and state). If you want to see “latest” something, you can write it to a ref. But there’s usually a simpler way to structure the code so that you don’t have to.
The returned function which supposes to be alternative to componentWillUnmount also is not an exact equivalent, since the function will run every time the component will re-render and when the component will unmount.
Keep in mind that the mental model for effects is different from component lifecycles, and trying to find their exact equivalents may confuse you more than help. To get productive, you need to “think in effects”, and their mental model is closer to implementing synchronization than to responding to lifecycle events.
Example from Dan's blog:
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
setTimeout(() => {
console.log(`You clicked ${count} times`);
}, 3000);
});
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
If we use the class implementation:
componentDidUpdate() {
setTimeout(() => {
console.log(`You clicked ${this.state.count} times`);
}, 3000);
}
this.state.count always points at the latest count rather than the one belonging to a particular render.
For Simple Explanation, I would like to show a visual reference
As we can see simply in the above picture that for -
componentDidMount :
useEffect(() => {
console.log('componentWillMount');
}, []);
componentDidUpdate :
useEffect(() => {
console.log('componentWillUpdate- runs on every update');
});
useEffect(() => {
console.log('componentWillUpdate - runs if dependency value changes ');
},[Dependencies]);
componentwillUnmount :
useEffect(() => {
return () => {
console.log('componentWillUnmount');
};
}, []);
Here is a good summary from the React Hooks FAQ listing Hooks equivalents for class lifecycle methods:
constructor: Function components don’t need a constructor. You can initialize the state in the useState call. If computing the initial state is expensive, you can pass a function to useState.
getDerivedStateFromProps: Schedule an update while rendering instead.
shouldComponentUpdate: See React.memo below.
render: This is the function component body itself.
componentDidMount, componentDidUpdate, componentWillUnmount: The useEffect Hook can express all combinations of these (including less common cases).
componentDidCatch and getDerivedStateFromError: There are no Hook equivalents for these methods yet, but they will be added soon.
componentDidMount
useEffect(() => { /*effect code*/ }, []);
[] will make the effect only run once at mount time. Usually you better specify your dependencies. To have the same layout timings as componentDidMount, have a look at useLayoutEffect (not needed in most cases).
componentWillUnmount
useEffect(() => { /*effect code*/ ; return ()=> { /*cleanup code*/ } }, [deps]);
componentWillUnmount corresponds to an effect with cleanup.
componentDidUpdate
const mounted = useRef();
useEffect(() => {
if (!mounted.current) mounted.current = true;
else {
// ... on componentDidUpdate
}
});
To have the same layout timings as componentDidUpdate, have a look at useLayoutEffect (not needed in most cases). See also this post for a more detailed look at componentDidUpdate hook equivalents.

Categories