React: update state only once, when another state reaches certain value - reactjs

Here's my code https://codesandbox.io/s/heuristic-cannon-lvse23?file=/src/App.js
import { useEffect, useState } from "react";
import "./styles.css";
function useAsyncState() {
const [status, setStatus] = useState("idle");
function trigger(value) {
setStatus(`sending ${value}`);
setTimeout(function () {
setStatus("done");
}, 1500);
}
return [status, trigger];
}
export default function App() {
const [value, setValue] = useState(0);
const [status, trigger] = useAsyncState();
useEffect(() => {
if (status === "done") {
setValue(value - 1);
}
}, [status, value]);
return (
<div className="App">
<p>
status: {status}
<button onClick={() => trigger(value)}>send & decrement</button>
</p>
<p>
value: {value}
<button onClick={() => setValue(value + 1)}>increment</button>
</p>
</div>
);
}
I have some kind of async action wrapped into a custom hook useAsyncState (simplified version of some real hook from my dependencies).
The problem is, I want to update value only once, when async action turns into done state.
However, changing state rerenders the component and since status is still done, value changes again, leading to endless loop of decrementing.
Also, action can be triggered again and I want to decrement value, when it's done again.
If I had an original Promise, I could do it in then callback, but I have only a hook like this.
Also, using useEffect delays value update to the next render, so it takes two renders to show decremeneted value. It's not a big deal, but it would be nice to do it in single render.
Is it ever possible to do it nice with such a custom hook? Or using raw Promises is the only way?

You can use a Functional Update instead:
If the new state is computed using the previous state, you can pass a function to setState. The function will receive the previous value, and return an updated value.
useEffect(() => {
if (status === "done") {
setValue((prevValue) => prevValue - 1);
}
}, [status]);

Related

Unexpected behaviour of setInterval function (interval keeps on decreasing) [duplicate]

Are there ways to simulate componentDidMount in React functional components via hooks?
For the stable version of hooks (React Version 16.8.0+)
For componentDidMount
useEffect(() => {
// Your code here
}, []);
For componentDidUpdate
useEffect(() => {
// Your code here
}, [yourDependency]);
For componentWillUnmount
useEffect(() => {
// componentWillUnmount
return () => {
// Your code here
}
}, [yourDependency]);
So in this situation, you need to pass your dependency into this array. Let's assume you have a state like this
const [count, setCount] = useState(0);
And whenever count increases you want to re-render your function component. Then your useEffect should look like this
useEffect(() => {
// <div>{count}</div>
}, [count]);
This way whenever your count updates your component will re-render. Hopefully this will help a bit.
There is no exact equivalent for componentDidMount in react hooks.
In my experience, react hooks requires a different mindset when developing it and generally speaking you should not compare it to the class methods like componentDidMount.
With that said, there are ways in which you can use hooks to produce a similar effect to componentDidMount.
Solution 1:
useEffect(() => {
console.log("I have been mounted")
}, [])
Solution 2:
const num = 5
useEffect(() => {
console.log("I will only run if my deps change: ", num)
}, [num])
Solution 3 (With function):
useEffect(() => {
const someFunc = () => {
console.log("Function being run after/on mount")
}
someFunc()
}, [])
Solution 4 (useCallback):
const msg = "some message"
const myFunc = useCallback(() => {
console.log(msg)
}, [msg])
useEffect(() => {
myFunc()
}, [myFunc])
Solution 5 (Getting creative):
export default function useDidMountHook(callback) {
const didMount = useRef(null)
useEffect(() => {
if (callback && !didMount.current) {
didMount.current = true
callback()
}
})
}
It is worth noting that solution 5 should only really be used if none of the other solutions work for your use case. If you do decide you need solution 5 then I recommend using this pre-made hook use-did-mount.
Source (With more detail): Using componentDidMount in react hooks
There's no componentDidMount on functional components, but React Hooks provide a way you can emulate the behavior by using the useEffect hook.
Pass an empty array as the second argument to useEffect() to run only the callback on mount only.
Please read the documentation on useEffect.
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>
useEffect() hook allows us to achieve the functionality of componentDidMount, componentDidUpdate componentWillUnMount functionalities.
Different syntaxes of useEffect() allows to achieve each of the above methods.
i) componentDidMount
useEffect(() => {
//code here
}, []);
ii) componentDidUpdate
useEffect(() => {
//code here
}, [x,y,z]);
//where x,y,z are state variables on whose update, this method should get triggered
iii) componentDidUnmount
useEffect(() => {
//code here
return function() {
//code to be run during unmount phase
}
}, []);
You can check the official react site for more info. Official React Page on Hooks
Although accepted answer works, it is not recommended. When you have more than one state and you use it with useEffect, it will give you warning about adding it to dependency array or not using it at all.
It sometimes causes the problem which might give you unpredictable output. So I suggest that you take a little effort to rewrite your function as class. There are very little changes, and you can have some components as class and some as function. You're not obligated to use only one convention.
Take this for example
function App() {
const [appointments, setAppointments] = useState([]);
const [aptId, setAptId] = useState(1);
useEffect(() => {
fetch('./data.json')
.then(response => response.json())
.then(result => {
const apts = result.map(item => {
item.aptId = aptId;
console.log(aptId);
setAptId(aptId + 1);
return item;
})
setAppointments(apts);
});
}, []);
return(...);
}
and
class App extends Component {
constructor() {
super();
this.state = {
appointments: [],
aptId: 1,
}
}
componentDidMount() {
fetch('./data.json')
.then(response => response.json())
.then(result => {
const apts = result.map(item => {
item.aptId = this.state.aptId;
this.setState({aptId: this.state.aptId + 1});
console.log(this.state.aptId);
return item;
});
this.setState({appointments: apts});
});
}
render(...);
}
This is only for example. so lets not talk about best practices or potential issues with the code. Both of this has same logic but the later only works as expected. You might get componentDidMount functionality with useEffect running for this time, but as your app grows, there are chances that you MAY face some issues. So, rather than rewriting at that phase, it's better to do this at early stage.
Besides, OOP is not that bad, if Procedure-Oriented Programming was enough, we would never have had Object-Oriented Programming. It's painful sometimes, but better (technically. personal issues aside).
import React, { useState, useEffect } from 'react';
function Example() {
const [count, setCount] = useState(0);
// Similar to componentDidMount and componentDidUpdate:
useEffect(() => {
// Update the document title using the browser API
document.title = `You clicked ${count} times`;
});
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
Please visit this official docs. Very easy to understand the latest way.
https://reactjs.org/docs/hooks-effect.html
Info about async functions inside the hook:
Effect callbacks are synchronous to prevent race conditions. Put the async function inside:
useEffect(() => {
async function fetchData() {
// You can await here
const response = await MyAPI.getData(someId);
// ...
}
fetchData();
}, [someId]); // Or [] if effect doesn't need props or state
useLayoutEffect hook is the best alternative to ComponentDidMount in React Hooks.
useLayoutEffect hook executes before Rendering UI and useEffect hook executes after rendering UI. Use it depend on your needs.
Sample Code:
import { useLayoutEffect, useEffect } from "react";
export default function App() {
useEffect(() => {
console.log("useEffect Statements");
}, []);
useLayoutEffect(() => {
console.log("useLayoutEffect Statements");
}, []);
return (
<div>
<h1>Hello Guys</h1>
</div>
);
}
Yes, there is a way to SIMULATE a componentDidMount in a React functional component
DISCLAIMER: The real problem here is that you need to change from "component life cycle mindset" to a "mindset of useEffect"
A React component is still a javascript function, so, if you want something to be executed BEFORE some other thing you must simply need to execute it first from top to bottom, if you think about it a function it's still a funtion like for example:
const myFunction = () => console.log('a')
const mySecondFunction = () => console.log('b)
mySecondFunction()
myFunction()
/* Result:
'b'
'a'
*/
That is really simple isn't it?
const MyComponent = () => {
const someCleverFunction = () => {...}
someCleverFunction() /* there I can execute it BEFORE
the first render (componentWillMount)*/
useEffect(()=> {
someCleverFunction() /* there I can execute it AFTER the first render */
},[]) /*I lie to react saying "hey, there are not external data (dependencies) that needs to be mapped here, trust me, I will leave this in blank.*/
return (
<div>
<h1>Hi!</h1>
</div>
)}
And in this specific case it's true. But what happens if I do something like that:
const MyComponent = () => {
const someCleverFunction = () => {...}
someCleverFunction() /* there I can execute it BEFORE
the first render (componentWillMount)*/
useEffect(()=> {
someCleverFunction() /* there I can execute it AFTER the first render */
},[]) /*I lie to react saying "hey, there are not external data (dependencies) that needs to be maped here, trust me, I will leave this in blank.*/
return (
<div>
<h1>Hi!</h1>
</div>
)}
This "cleverFunction" we are defining it's not the same in every re-render of the component.
This lead to some nasty bugs and, in some cases to unnecessary re-renders of components or infinite re-render loops.
The real problem with that is that a React functional component is a function that "executes itself" several times depending on your state thanks to the useEffect hook (among others).
In short useEffect it's a hook designed specifically to synchronize your data with whatever you are seeing on the screen. If your data changes, your useEffect hook needs to be aware of that, always. That includes your methods, for that it's the array dependencies.
Leaving that undefined leaves you open to hard-to-find bugs.
Because of that it's important to know how this work, and what you can do to get what you want in the "react" way.
const initialState = {
count: 0,
step: 1,
done: false
};
function reducer(state, action) {
const { count, step } = state;
if (action.type === 'doSomething') {
if(state.done === true) return state;
return { ...state, count: state.count + state.step, state.done:true };
} else if (action.type === 'step') {
return { ...state, step: action.step };
} else {
throw new Error();
}
}
const MyComponent = () => {
const [state, dispatch] = useReducer(reducer, initialState);
const { count, step } = state;
useEffect(() => {
dispatch({ type: 'doSomething' });
}, [dispatch]);
return (
<div>
<h1>Hi!</h1>
</div>
)}
useReducer's dispatch method it's static so it means it will be the same method no matter the amount of times your component is re-rendered. So if you want to execute something just once and you want it rigth after the component is mounted, you can do something like the above example. This is a declarative way of do it right.
Source: The Complete Guide to useEffect - By Dan Abramov
That being said if you like to experiment with things and want to know how to do it "the imperative wat" you can use a useRef() with a counter or a boolean to check if that ref stores a defined reference or not, this is an imperative approach and it's recommended to avoid it if you're not familiar with what happen with react behind curtains.
That is because useRef() is a hook that saves the argument passed to it regardless of the amount of renders (I am keeping it simple because it's not the focus of the problem here, you can read this amazing article about useRef ). So it's the best approach to known when the first render of the component happened.
I leave an example showing 3 different ways of synchronise an "outside" effect (like an external function) with the "inner" component state.
You can run this snippet right here to see the logs and understand when these 3 functions are executed.
const { useRef, useState, useEffect, useCallback } = React
// External functions outside react component (like a data fetch)
function renderOnce(count) {
console.log(`renderOnce: I executed ${count} times because my default state is: undefined by default!`);
}
function renderOnFirstReRender(count) {
console.log(`renderOnUpdate: I executed just ${count} times!`);
}
function renderOnEveryUpdate(count) {
console.log(`renderOnEveryUpdate: I executed ${count ? count + 1 : 1} times!`);
}
const MyComponent = () => {
const [count, setCount] = useState(undefined);
const mounted = useRef(0);
// useCallback is used just to avoid warnings in console.log
const renderOnEveryUpdateCallBack = useCallback(count => {
renderOnEveryUpdate(count);
}, []);
if (mounted.current === 0) {
renderOnce(count);
}
if (mounted.current === 1) renderOnFirstReRender(count);
useEffect(() => {
mounted.current = mounted.current + 1;
renderOnEveryUpdateCallBack(count);
}, [count, renderOnEveryUpdateCallBack]);
return (
<div>
<h1>{count}</h1>
<button onClick={() => setCount(prevState => (prevState ? prevState + 1 : 1))}>TouchMe</button>
</div>
);
};
class App extends React.Component {
render() {
return (
<div>
<h1>hI!</h1>
</div>
);
}
}
ReactDOM.createRoot(
document.getElementById("root")
).render(
<MyComponent/>
);
<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>
If you execute it you will see something like this:
You want to use useEffect(), which, depending on how you use the function, can act just like componentDidMount().
Eg. you could use a custom loaded state property which is initially set to false, and switch it to true on render, and only fire the effect when this value changes.
Documentation
the exact equivalent hook for componentDidMount() is
useEffect(()=>{},[]);
hope this helpful :)

How to deal with stale state values inside of a useEffect closure?

The following example is of a Timer component that has a button (to start the timer), and two tags that display the number of elapsed seconds, and the number of elapsed seconds times 2.
However, it does not work (CodeSandbox Demo)
The Code
import React, { useState, useEffect } from "react";
const Timer = () => {
const [doubleSeconds, setDoubleSeconds] = useState(0);
const [seconds, setSeconds] = useState(0);
const [isActive, setIsActive] = useState(false);
useEffect(() => {
let interval = null;
if (isActive) {
interval = setInterval(() => {
console.log("Creating Interval");
setSeconds((prev) => prev + 1);
setDoubleSeconds(seconds * 2);
}, 1000);
} else {
clearInterval(interval);
}
return () => {
console.log("Destroying Interval");
clearInterval(interval);
};
}, [isActive]);
return (
<div className="app">
<button onClick={() => setIsActive((prev) => !prev)} type="button">
{isActive ? "Pause Timer" : "Play Timer"}
</button>
<h3>Seconds: {seconds}</h3>
<h3>Seconds x2: {doubleSeconds}</h3>
</div>
);
};
export { Timer as default };
The Problem
Inside the useEffect call, the "seconds" value will always be equal to the its value when the useEffect block was last rendered (when isActive last changed). This will result in the setDoubleSeconds(seconds * 2) statement to fail. The React Hooks ESLint plugin gives me a warning regarding this problem that reads:
React Hook useEffect has a missing dependency: 'seconds'. Either include it or remove the dependency array. You can also replace
multiple useState variables with useReducer if 'setDoubleSeconds'
needs the current value of 'seconds'.
(react-hooks/exhaustive-deps)eslint
And correctly so, adding "seconds" to the dependency array (and changing setDoubleSeconds(seconds * 2) to setDoubleSeconds((seconds + 1) * ) will render the correct results. However, this has a nasty side effect of causing the interval to be created and destroyed on every render (the console.log("Destroying Interval") fires on every render).
So now I am looking at the other recommendation from the ESLint warning "You can also replace multiple useState variables with useReducer if 'setDoubleSeconds' needs the current value of 'seconds'".
I do not understand this recommendation. If I create a reducer and use it like so:
import React, { useState, useEffect, useReducer } from "react";
const reducer = (state, action) => {
switch (action.type) {
case "SET": {
return action.seconds;
}
default: {
return state;
}
}
};
const Timer = () => {
const [doubleSeconds, dispatch] = useReducer(reducer, 0);
const [seconds, setSeconds] = useState(0);
const [isActive, setIsActive] = useState(false);
useEffect(() => {
let interval = null;
if (isActive) {
interval = setInterval(() => {
console.log("Creating Interval");
setSeconds((prev) => prev + 1);
dispatch({ type: "SET", seconds });
}, 1000);
} else {
clearInterval(interval);
}
return () => {
console.log("Destroying Interval");
clearInterval(interval);
};
}, [isActive]);
return (
<div className="app">
<button onClick={() => setIsActive((prev) => !prev)} type="button">
{isActive ? "Pause Timer" : "Play Timer"}
</button>
<h3>Seconds: {seconds}</h3>
<h3>Seconds x2: {doubleSeconds}</h3>
</div>
);
};
export { Timer as default };
The problem of stale values will still exist (CodeSandbox Demo (using Reducers)).
The Question(s)
So what is the recommendation for this scenario? Do I take the performance hit and simply add "seconds" to the dependency array? Do I create another useEffect block that depends on "seconds" and call "setDoubleSeconds()" in there? Do I merge "seconds" and "doubleSeconds" into a single state object? Do I use refs?
Also, you might be thinking "Why don't you simply change <h3>Seconds x2: {doubleSeconds}</h3>" to <h3>Seconds x2: {seconds * 2}</h3> and remove the 'doubleSeconds' state?". In my real application doubleSeconds is passed to a Child component and I do not want the Child component to know how seconds is mapped to doubleSeconds as it makes the Child less re-usable.
Thanks!
You can access a value inside an effect callback without adding it as a dep in a few ways.
setState. You can tap the up-to-date value of a state variable through its setter.
setSeconds(seconds => (setDoubleSeconds(seconds * 2), seconds));
Ref. You can pass a ref as a dependency and it'll never change. You need to manually keep it up to date, though.
const secondsRef = useRef(0);
const [seconds, setSeconds] = useReducer((_state, action) => (secondsRef.current = action), 0);
You can then use secondsRef.current to access seconds in a block of code without having it trigger deps changes.
setDoubleSeconds(secondsRef.current * 2);
In my opinion you should never omit a dependency from the deps array. Use a hack like the above to make sure your values are up-to-date if you need the deps not to change.
Always first consider if there's some more elegant way to write your code than hacking a value into a callback. In your example doubleSeconds can be expressed as a derivative of seconds.
const [seconds, setSeconds] = useState(0);
const doubleSeconds = seconds * 2;
Sometimes applications aren't that simple so you may need to use the hacks described above.
Do I take the performance hit and simply add "seconds" to the dependency array?
Do I create another useEffect block that depends on "seconds" and call "setDoubleSeconds()" in there?
Do I merge "seconds" and "doubleSeconds" into a single state object?
Do I use refs?
All of them work correctly, although personally I would rather choose the second approach:
useEffect(() => {
setDoubleSeconds(seconds * 2);
}, [seconds]);
However:
In my real application doubleSeconds is passed to a Child component and I do not want the Child component to know how seconds is mapped to doubleSeconds as it makes the Child less re-usable
That is questionable. Child component might be implemented like the following:
const Child = ({second}) => (
<p>Seconds: {second}s</p>
);
And parent component should look like the following:
const [seconds, setSeconds] = useState(0);
useEffect(() => {
// change seconds
}, []);
return (
<React.Fragment>
<Child seconds={second} />
<Child seconds={second * 2} />
</React.Fragment>
);
This would be a more clear and concise way.

useState hook: update a state based on another just updated state

I'm currently looking in to React's funtion Component and the useState hook.
const [stateA, setStateA] = useState(somevalue)
const [stateB, setStateB] = useState(somevalue)
function onClick(value){
setStateA(stateA => transformedValue) //a is now transformedValue
//Use transformedValue to update state B
setStateB(...)
}
My question is how to use the newly updated stateA(transformedValue) to updateB after that.
So far i could only access the previous value of State A, not the transformedValue.
I know i could calculate transformedValue outside of the setStateA function, then use that to setStateB. But is it the correct way?
I know can also merge stateA and stateB into one State too.
What is the correct approach? Is there another more preferred way?
The best approach is using useEffect as mentioned.
useEffect(() => {
setCounter2(count + 1);
}, [count]);
Because of you obsessed with rendering once, try this:
function App() {
const [count, setCount] = useState(0);
const [dummy, setDummy] = useState(0);
useEffect(() => {
console.log(`Renderderd ${count} times`);
});
function handleClick() {
setCount(prevCounter => {
setDummy(prevCounter + 1);
return prevCounter + 1;
});
}
return (
<>
<div>You clicked count {count} times!</div>
<div>Dummy= {dummy} !</div>
<button onClick={handleClick}>Click me for rendering!</button>
</>
);
}
As the method setState is asynchronous.
Your stateA won't update immediately when setState is called.
A way to implement this idea is that
to "watch" a stateA by using useEffect method to ensure stateA is updated.
code seems like this:
useEffect(()=>{
setStateB(stateA); // or do anything with updated stateA
},[stateA]);

React: useState or useRef?

I am reading about React useState() and useRef() at "Hooks FAQ" and I got confused about some of the use cases that seem to have a solution with useRef and useState at the same time, and I'm not sure which way it the right way.
From the "Hooks FAQ" about useRef():
"The useRef() Hook isn’t just for DOM refs. The “ref” object is a generic container whose current property is mutable and can hold any value, similar to an instance property on a class."
With useRef():
function Timer() {
const intervalRef = useRef();
useEffect(() => {
const id = setInterval(() => {
// ...
});
intervalRef.current = id;
return () => {
clearInterval(intervalRef.current);
};
});
// ...
}
With useState():
function Timer() {
const [intervalId, setIntervalId] = useState(null);
useEffect(() => {
const id = setInterval(() => {
// ...
});
setIntervalId(id);
return () => {
clearInterval(intervalId);
};
});
// ...
}
Both examples will have the same result, but which one it better - and why?
The main difference between both is :
useState causes re-render, useRef does not.
The common between them is, both useState and useRef can remember their data after re-renders. So if your variable is something that decides a view layer render, go with useState. Else use useRef
I would suggest reading this article.
useRef is useful when you want to track value change, but don't want to trigger re-render or useEffect by it.
Most use case is when you have a function that depends on value, but the value needs to be updated by the function result itself.
For example, let's assume you want to paginate some API result:
const [filter, setFilter] = useState({});
const [rows, setRows] = useState([]);
const [currentPage, setCurrentPage] = useState(1);
const fetchData = useCallback(async () => {
const nextPage = currentPage + 1;
const response = await fetchApi({...filter, page: nextPage});
setRows(response.data);
if (response.data.length) {
setCurrentPage(nextPage);
}
}, [filter, currentPage]);
fetchData is using currentPage state, but it needs to update currentPage after successful response. This is inevitable process, but it is prone to cause infinite loop aka Maximum update depth exceeded error in React. For example, if you want to fetch rows when component is loaded, you want to do something like this:
useEffect(() => {
fetchData();
}, [fetchData]);
This is buggy because we use state and update it in the same function.
We want to track currentPage but don't want to trigger useCallback or useEffect by its change.
We can solve this problem easily with useRef:
const currentPageRef = useRef(0);
const fetchData = useCallback(async () => {
const nextPage = currentPageRef.current + 1;
const response = await fetchApi({...filter, page: nextPage});
setRows(response.data);
if (response.data.length) {
currentPageRef.current = nextPage;
}
}, [filter]);
We can remove currentPage dependency from useCallback deps array with the help of useRef, so our component is saved from infinite loop.
The main difference between useState and useRef are -
The value of the reference is persisted (stays the same) between component re-rendering,
Updating a reference using useRefdoesn't trigger component re-rendering.
However, updating a state causes component re-rendering
The reference update is synchronous, the updated referenced value is immediately available, but the state update is asynchronous - the value is updated after re-rendering.
To view using codes:
import { useState } from 'react';
function LogButtonClicks() {
const [count, setCount] = useState(0);
const handle = () => {
const updatedCount = count + 1;
console.log(`Clicked ${updatedCount} times`);
setCount(updatedCount);
};
console.log('I rendered!');
return <button onClick={handle}>Click me</button>;
}
Each time you click the button, it will show I rendered!
However, with useRef
import { useRef } from 'react';
function LogButtonClicks() {
const countRef = useRef(0);
const handle = () => {
countRef.current++;
console.log(`Clicked ${countRef.current} times`);
};
console.log('I rendered!');
return <button onClick={handle}>Click me</button>;
}
I am rendered will be console logged just once.
Basically, We use UseState in those cases, in which the value of state should be updated with re-rendering.
when you want your information persists for the lifetime of the component you will go with UseRef because it's just not for work with re-rendering.
If you store the interval id, the only thing you can do is end the interval. What's better is to store the state timerActive, so you can stop/start the timer when needed.
function Timer() {
const [timerActive, setTimerActive] = useState(true);
useEffect(() => {
if (!timerActive) return;
const id = setInterval(() => {
// ...
});
return () => {
clearInterval(intervalId);
};
}, [timerActive]);
// ...
}
If you want the callback to change on every render, you can use a ref to update an inner callback on each render.
function Timer() {
const [timerActive, setTimerActive] = useState(true);
const callbackRef = useRef();
useEffect(() => {
callbackRef.current = () => {
// Will always be up to date
};
});
useEffect(() => {
if (!timerActive) return;
const id = setInterval(() => {
callbackRef.current()
});
return () => {
clearInterval(intervalId);
};
}, [timerActive]);
// ...
}
Counter App to see useRef does not rerender
If you create a simple counter app using useRef to store the state:
import { useRef } from "react";
const App = () => {
const count = useRef(0);
return (
<div>
<h2>count: {count.current}</h2>
<button
onClick={() => {
count.current = count.current + 1;
console.log(count.current);
}}
>
increase count
</button>
</div>
);
};
If you click on the button, <h2>count: {count.current}</h2> this value will not change because component is NOT RE-RENDERING. If you check the console console.log(count.current), you will see that value is actually increasing but since the component is not rerendering, UI does not get updated.
If you set the state with useState, clicking on the button would rerender the component so UI would get updated.
Prevent unnecessary re-renderings while typing into input.
Rerendering is an expensive operation. In some cases, you do not want to keep rerendering the app. For example, when you store the input value in the state to create a controlled component. In this case for each keystroke, you would rerender the app. If you use the ref to get a reference to the DOM element, with useState you would rerender the component only once:
import { useState, useRef } from "react";
const App = () => {
const [value, setValue] = useState("");
const valueRef = useRef();
const handleClick = () => {
console.log(valueRef);
setValue(valueRef.current.value);
};
return (
<div>
<h4>Input Value: {value}</h4>
<input ref={valueRef} />
<button onClick={handleClick}>click</button>
</div>
);
};
Prevent the infinite loop inside useEffect
to create a simple flipping animation, we need to 2 state values. one is a boolean value to flip or not in an interval, another one is to clear the subscription when we leave the component:
const [isFlipping, setIsFlipping] = useState(false);
let flipInterval = useRef<ReturnType<typeof setInterval>>();
useEffect(() => {
startAnimation();
return () => flipInterval.current && clearInterval(flipInterval.current);
}, []);
const startAnimation = () => {
flipInterval.current = setInterval(() => {
setIsFlipping((prevFlipping) => !prevFlipping);
}, 10000);
};
setInterval returns an id and we pass it to clearInterval to end the subscription when we leave the component. flipInterval.current is either null or this id. If we did not use ref here, everytime we switched from null to id or from id to null, this component would rerender and this would create an infinite loop.
If you do not need to update UI, use useRef to store state variables.
Let's say in react native app, we set the sound for certain actions which have no effect on UI. For one state variable it might not be that much performance savings but If you play a game and you need to set different sound based on game status.
const popSoundRef = useRef<Audio.Sound | null>(null);
const pop2SoundRef = useRef<Audio.Sound | null>(null);
const winSoundRef = useRef<Audio.Sound | null>(null);
const lossSoundRef = useRef<Audio.Sound | null>(null);
const drawSoundRef = useRef<Audio.Sound | null>(null);
If I used useState, I would keep rerendering every time I change a state value.
You can also use useRef to ref a dom element (default HTML attribute)
eg: assigning a button to focus on the input field.
whereas useState only updates the value and re-renders the component.
It really depends mostly on what you are using the timer for, which is not clear since you didn't show what the component renders.
If you want to show the value of your timer in the rendering of your component, you need to use useState. Otherwise, the changing value of your ref will not cause a re-render and the timer will not update on the screen.
If something else must happen which should change the UI visually at each tick of the timer, you use useState and either put the timer variable in the dependency array of a useEffect hook (where you do whatever is needed for the UI updates), or do your logic in the render method (component return value) based on the timer value.
SetState calls will cause a re-render and then call your useEffect hooks (depending on the dependency array).
With a ref, no updates will happen, and no useEffect will be called.
If you only want to use the timer internally, you could use useRef instead. Whenever something must happen which should cause a re-render (ie. after a certain time has passed), you could then call another state variable with setState from within your setInterval callback. This will then cause the component to re-render.
Using refs for local state should be done only when really necessary (ie. in case of a flow or performance issue) as it doesn't follow "the React way".
useRef() only updates the value not re-render your UI if you want to re-render UI then you have to use useState() instead of useRe. let me know if any correction needed.
As noted in many different places useState updates trigger a render of the component while useRef updates do not.
For the most part having a few guiding principles would help:.
for useState
anything used with input / TextInput should have a state that gets updated with the value that you are setting.
when you need a trigger to recompute values that are in useMemo or trigger effects using useEffect
when you need data that would be consumed by a render that is only available after an async operation done on a useEffect or other event handler. E.g. FlatList data that would need to be provided.
for useRef
use these to store data that would not be visible to the user such as event subscribers.
for contexts or custom hooks, use this to pass props that are updated by useMemo or useEffect that are triggered by useState/useReducer. The mistake I tend to make is placing something like authState as a state and then when I update that it triggers a whole rerender when that state is actually the final result of a chain.
when you need to pass a ref
The difference is that useState returns the current state and has an updater function that updates the state. While useRef returns an object, doesn’t cause components to re-render, and it’s used to reference DOM elements.
Therefore,
If you want to have state in your components, which triggers a rerendered view when changed, useState or useReducer. Go with useRef if you don't want state to trigger a render.
look at this example,
import { useEffect, useRef } from "react";
import { Form } from "./FormStyle";
const ExampleDemoUseRef = () => {
const emailRef = useRef("");
const passwordRef = useRef("");
useEffect(() => {
emailRef.current.focus();
}, []);
useEffect(() => {
console.log("render everytime.");
});
const handleSubmit = (event) => {
event.preventDefault();
const email = emailRef.current.value;
const password = passwordRef.current.value;
console.log({ email, password });
};
return (
<div>
<h1>useRef</h1>
<Form onSubmit={handleSubmit}>
<label htmlFor="email">Email: </label>
<input type="email" name="email" ref={emailRef} />
<label htmlFor="password">Password: </label>
<input type="password" name="password" ref={passwordRef} />
<button>Submit</button>
</Form>
</div>
);
};
export default ExampleDemoUseRef;
and this useState example,
import { useEffect, useState, useRef } from "react";
import { Form } from "./FormStyle";
const ExampleDemoUseState = () => {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const emailRef = useRef("");
useEffect(() => {
console.log("render everytime.");
});
useEffect(() => {
emailRef.current.focus();
}, []);
const onChange = (e) => {
const { type, value } = e.target;
switch (type) {
case "email":
setEmail(value);
break;
case "password":
setPassword(value);
break;
default:
break;
}
};
const handleSubmit = (event) => {
event.preventDefault();
console.log({ email, password });
};
return (
<div>
<h1>useState</h1>
<Form onSubmit={handleSubmit}>
<label htmlFor="email">Email: </label>
<input type="email" name="email" onChange={onChange} ref={emailRef} />
<label htmlFor="password">Password: </label>
<input type="password" name="password" onChange={onChange} />
<button>Submit</button>
</Form>
</div>
);
};
export default ExampleDemoUseState;
so basically,
UseRef is an alternative to useState if you do not want to update DOM elements and want to get a value (having a state in component).

Simple counter doesn't stop incrementing inside functional component with useEffect hook

I trying to use rxjs inside a functional component's useEffect Hook.
I believe useEffect can be used just like componentDidMount and componentDidUpdate.
const Counter = () => {
const [state, setState] = useState({counter: 0});
useEffect(() => {
subscriber.subscribe((value) => {
let { counter } = state;
counter += value;
setState({ counter });
});
});
return (
<div>
<span>Counter: {state.counter}</span>
<Crementer />
</div>
);
The subscriber is a simple rxjs subject
const subscriber = new BehaviorSubject(0);
There's also a component to increment / decrement the counter
const Crementer = () => {
return(
<>
<button onClick={() => subscriber.next(1)}>+</button>
<button onClick={() => subscriber.next(-1)}>-</button>
</>
)
};
(I tried counterService.send(1) as well)
The problem is as soon as I click + or - the counter increments or decrements continuously.
You can see the behavior here: https://stackblitz.com/edit/react-9auz4d
Maybe because useEffect runs also on update? It will probably work in a class component with a componentDidMount() ?
Hooks are invoked on every update: https://reactjs.org/docs/hooks-effect.html#explanation-why-effects-run-on-each-update
So you could store the subscription returned form subscribe call and then keep it in another useState. On every effect invocation you'd just check whether it's set or not. Btw, you should also return a teardown function from this effect so that's where you'd need the subscription as well.
Or eventually you can use the second parameter to useEffect to run it just once (How to call loading function with React useEffect only once):
useEffect(() => {
const subscription = observable.subscribe((value) => {
let { counter } = state;
counter += value;
setState({ counter });
});
return () => subscription.unsubscribe();
}, []);

Resources