React: useState or useRef? - reactjs

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).

Related

Why is my useState variable initialized every time my React components is rendered?

While working on some custom hooks in React I have observed the following behavior: Every time my component renders, my useState variables are initialized. More specifically, I have observed this by using an auxiliary function to dynamically create data for testing.
In order to demonstrate this behavior of React, I have created a super simple "TestComponent" (please refer to the code below). Every time my component renders, it logs "testData called!" in the console.
What did I do wrong? How can I prevent React from calling my "test-data-generating-function" testData every time my component renders?
PS: I tried to wrap my function testData into a useCallback hook. But that does not work since useCallback cannot be used at "top level". Also, instead of finding a "workaround", I would really like to understand the root cause of my problem.
import React, { useEffect, useState } from 'react';
const testData = (): number[] => {
console.log('testData called!');
const returnValues: number[] = [];
for (let i = 1; i <= 10; i++) {
returnValues.push(i);
}
return returnValues;
};
const TestComponent = () => {
// useState hooks
const [data, setData] = useState<number[]>(testData());
const [counter, setCounter] = useState<number>(1);
// useEffect hook
useEffect(() => {
const increaseCounter = () => {
setCounter((counter) => counter + 1);
setTimeout(() => increaseCounter(), 1000);
};
increaseCounter();
}, []);
return (
<div>
{data.map((item) => (
<p key={item}>{item}</p>
))}
<p>{counter}</p>
</div>
);
};
export default TestComponent;
I believe what happens here is that your function to retrieve the data (testData()) is being called for each render, but not necessarily updates the state.
A react component is essentialy a function, and for each render the whole function is executed (including the call to testData()).
If you use the callback signature of useState, it will be called only on the first render:
const [data, setData] = useState(() => testData());

How to prevent set sate in unmounted component in React?

I received a warning: "Can't perform a React state update on an unmounted component", so I try to determine when my component is unmounted, like below:
function ListStock() {
let mounted = true;
const [data, setData] = useState([]);
const [search, setSearch] = useState();
useEffect(() => {
async function fetchData() {
const {start_date, end_date} = search;
const result = await getDataStock(start_date, end_date);
if (result && mounted) {
setData(result.data); // only set a state when mounted = true
}
}
fetchData();
return () => {
mounted = false; // set false on clean up
}
}, [search])
const handleSearch = () => {
...
setSearch({
start_date: moment().subtract(1, 'month').format('YYYY-MM-DD'),
end_date: moment().format('YYYY-MM-DD')
});
}
return (
<div>
<input type="text" id="keyword">
<input type="button" onlick={handleSearch} value="Search">
{data}
</div>
)
}
By this way, it can resolve that warning message, however it shows another one:
"Assignments to the 'mounted' variable from inside React Hook
useEffect will be lost after each render. To preserve the value over
time, store it in a useRef Hook and keep the mutable value in the
'.current' property. Otherwise, you can move this variable directly
inside useEffect"
When I store the 'mounted' variable in a useRef hook, I cannot search anymore, since the 'mounted' is always set to "false".
My questions are:
Why a clean up code runs when User click a search button? I though it runs only when a component is unmounted?
What is the right way to implement a searching job with a remote api?
Is it fine if I config ESLint to ignore all this kind of warning messages?
Thanks all.
The problem is that you are doing the whole mounted/unmounted thing wrong. Here is a proper implementation:
const mounted = useRef(false);
useEffect(() => {
mounted.current = true;
return () => {
mounted.current = false;
};
}, []); // Notice lack of dependencies
Before I go on, I should probably refer you to the awesome react-use library, which already comes with a useMountedState hook
Now back to your questions
Why a clean up code runs when User click a search button? I though it
runs only when a component is unmounted?
I didn't realize this was a thing until I read the docs:
When exactly does React clean up an effect? React performs the cleanup
when the component unmounts. However, as we learned earlier, effects
run for every render and not just once. This is why React also cleans
up effects from the previous render before running the effects next
time...
So there you have it: The cleanup function is run after every render which happens after state changes, thus when search changes, a re-render is required.
What is the right way to implement a searching job with a remote api?
The way you are doing it is fine, but if you are going to be checking for unmounted state every time, you might as well use the library I mentioned.
Is it fine if I config ESLint to ignore all this kind of warning
messages?
Nah. Just fix it. It is very easy
Instead of using a variable, you need to store mounted = true; in a useRef hook. UseRef can hold values and it won't re-render the page when the value changes.
function ListStock() {
const mounted = useRef(true);
const [data, setData] = useState([]);
const [search, setSearch] = useState();
useEffect(() => {
async function fetchData() {
const {start_date, end_date} = search;
const result = await getDataStock(start_date, end_date);
if (result && mounted,current) {
setData(result.data); // only set a state when mounted = true
}
}
fetchData();
return () => {
mounted.current = false; // set false on clean up
}
}, [search])
const handleSearch = () => {
...
setSearch({
start_date: moment().subtract(1, 'month').format('YYYY-MM-DD'),
end_date: moment().format('YYYY-MM-DD')
});
}
return (
<div>
<input type="text" id="keyword">
<input type="button" onlick={handleSearch} value="Search">
{data}
</div>
)
}
Hopefully, questions 1 and 2 will be solved by the above code. 3rd question, I would say it's better to keep it as it shows what's going wrong.
in this case, put mounted inside useEffect is better,
once search changed, previous request should be cancel,
const [data, setData] = useState([]);
const [search, setSearch] = useState();
useEffect(() => {
let cancel = true;
async function fetchData() {
const {start_date, end_date} = search;
const result = await getDataStock(start_date, end_date);
if (result && !cancel) {
setData(result.data); // only set a state when not canceled
}
}
fetchData();
return () => {
cancel = true; // to cancel setState
}
}, [search])

React Hooks, how to handle dependency If i want to run only when one of them changes?

I wanted to ask if there's a proper way of handling a situation when we have an useEffect that we only care it runs when one variable of the dependency array is changed.
We have a table that has pagination and inputs to filter the content
with a button.
When the user changes the action (inputs) we update this state and
when the user press search we fetch from the api.
When we have results paginated, we then hook on the page and if it
changes we then fetch the corresponding page.
I solved the issue by having the ref of action and checking if the previous value was different from the current value. Though I don't know if this is the best practice
I did something like this.
const FunctionView = () => {
const actionRef = useRef({})
// action object have query params for the api
const fetchData = useCallback((page) => {
// call and api and sets local values
}, [action])
// this hook handle page next or previous
useEffect(() => {
let didCancel = false
if (isEqual(Object.values(action), Object.values(actionRef.current))) {
if (!didCancel) fetchData(page + 1)
}
actionRef.current = action
return () => {
didCancel = true
}
}, [page, fetchData, action])
return (
<>
Components here that changes {action} object, dates and category
<Button onClick={() => fetchData(page)}>Search</Button>
<Table>...</Table>
</>
)
}
I know I can set the dependency array to only page but react lint plugin complains about this so I ended up with warnings.
A common mistake is trying to directly wire the state of forms up to the data they represent. In your example, you DO want to perform the search when action changes. What you're actually trying to avoid is updating action every time one of the inputs changes prior to submit.
Forms are, by nature, transient. The state of the form is simply the current state of the inputs. Unless you really want things to update on every single keystroke, the storage of these values should be distinct.
import { useCallback, useEffect, useState } from 'react'
const Search = () => {
const [action, setAction] = useState({})
const loadData = useCallback(() => {
// call api with values from `action`
}, [action])
useEffect(loadData, [loadData])
return (
<>
<SearchForm setRealData={setAction} />
<Table>...</Table>
</>
)
}
const SearchForm = ({ setRealData }) => {
// don't want to redo the search on every keystroke, so
// this just saves the value of the input
const [firstValue, setFirstValue] = useState('')
const [secondValue, setSecondValue] = useState('')
const handleFirstChange = (event) => setFirstValue(event.target.value)
const handleSecondChange = (event) => setSecondValue(event.target.value)
// now that we have finished updating the form, update the state
const handleSubmit = (event) => {
event.preventDefault()
setRealData({ first: firstValue, second: secondValue })
}
return (
<form onSubmit={handleSubmit}>
<input value={firstValue} onChange={handleFirstChange} />
<input value={secondValue} onChange={handleSecondChange} />
<button type="submit">Search</button>
</form>
)
}

storing value from text input into redux store cause re-rendering react component

I'm trying to store value from text input into the redux store but problem is that my react component is still re-rendering.
After dispatching action using debounce action, the component Autocomplete rerender and I get initial state. So input become empty after 300ms because of debounce func.
I tried to use useCallback to avoid it but without any result.
How Could I save input value into redux store and prevent re-render react component ?
const AutoComplete = () => {
const [focus, setFocus] = useState(false);
const [blur, setBlur] = useState(false);
const radarTrends = useSelector(selectedRadarsTrends);
const dispatch = useDispatch();
const filteredValue = useSelector(selectedFilteredValue);
const debounceFilteredValue = (value) => {
dispatch(setFilteredValue(value));
}
const debounce = useCallback(_.debounce(debounceFilteredValue, 300), []);
const handleOnChange = (event) => {
debounce(event.target.value);
};
const handleOnFocus = () => {
setBlur(false);
setFocus(true);
};
const handleOnBlur = () => {
setFocus(false);
setBlur(true);
};
return (
<StyledAutoComplete hasFocus={focus} hasBlur={blur}>
<StyledMagnifier>
<Magnifier width={20} height={20} />
</StyledMagnifier>
<Input
placeholder="type trend name"
type="text"
label=""
name="autocomplete"
onFocus={handleOnFocus}
onChange={handleOnChange}
onBlur={handleOnBlur}
/>
</StyledAutoComplete>
);
};
Thanks for help.
I believe re-render is caused by useSelector after dispatch.
When an action is dispatched, useSelector() will do a reference comparison of
the previous selector result value and the current result value.
If they are different, the component will be forced to re-render.
If they are the same, the component will not re-render.
See the docs: https://react-redux.js.org/next/api/hooks#useselector

How to wait for multiple state updates in multiple hooks?

Example
In my scenario I have a sidebar with filters.. each filter is created by a hook:
const filters = {
customerNoFilter: useFilterForMultiCreatable(),
dateOfOrderFilter: useFilterForDate(),
requestedDevliveryDateFilter: useFilterForDate(),
deliveryCountryFilter: useFilterForCodeStable()
//.... these custom hooks are reused for like 10 more filters
}
Among other things the custom hooks return currently selected values, a reset() and handlers like onChange, onRemove. (So it's not just a simple useState hidden behind the custom hooks, just keep that in mind)
Basically the reset() functions looks like this:
I also implemented a function to clear all filters which is calling the reset() function for each filter:
const clearFilters = () => {
const filterValues = Object.values(filters);
for (const filter of filterValues) {
filter.reset();
}
};
The reset() function is triggering a state update (which is of course async) in each filter to reset all the selected filters.
// setSelected is the setter comming from the return value of a useState statement
const reset = () => setSelected(initialSelected);
Right after the resetting I want to do stuff with the reseted/updated values and NOT with the values before the state update, e.g. calling API with reseted filters:
clearFilters();
callAPI();
In this case the API is called with the old values (before the update in the reset())
So how can i wait for all filters to finish there state updated? Is my code just badly structured? Am i overseeing something?
For single state updates I could simply use useEffect but this would be really cumbersome when waiting for multiple state updates..
Please don't take the example to serious as I face this issue quite often in quite different scenarios..
So I came up with a solution by implementing a custom hook named useStateWithPromise:
import { SetStateAction, useEffect, useRef, useState } from "react";
export const useStateWithPromise = <T>(initialState: T):
[T, (stateAction: SetStateAction<T>) => Promise<T>] => {
const [state, setState] = useState(initialState);
const readyPromiseResolverRef = useRef<((currentState: T) => void) | null>(
null
);
useEffect(() => {
if (readyPromiseResolverRef.current) {
readyPromiseResolverRef.current(state);
readyPromiseResolverRef.current = null;
}
/**
* The ref dependency here is mandatory! Why?
* Because the useEffect would never be called if the new state value
* would be the same as the current one, thus the promise would never be resolved
*/
}, [readyPromiseResolverRef.current, state]);
const handleSetState = (stateAction: SetStateAction<T>) => {
setState(stateAction);
return new Promise(resolve => {
readyPromiseResolverRef.current = resolve;
}) as Promise<T>;
};
return [state, handleSetState];
};
This hook will allow to await state updates:
const [selected, setSelected] = useStateWithPromise<MyFilterType>();
// setSelected will now return a promise
const reset = () => setSelected(undefined);
const clearFilters = () => {
const promises = Object.values(filters).map(
filter => filter.reset()
);
return Promise.all(promises);
};
await clearFilters();
callAPI();
Yey, I can wait on state updates! Unfortunatly that's not all if callAPI() is relying on updated state values ..
const [filtersToApply, setFiltersToApply] = useState(/* ... */);
//...
const callAPI = () => {
// filtersToApply will still contain old state here, although clearFilters() was "awaited"
endpoint.getItems(filtersToApply);
}
This happens because the executed callAPI function after await clearFilters(); is is not rerendered thus it points to old state. But there is a trick which requires an additional useRef to force rerender after filters were cleared:
useEffect(() => {
if (filtersCleared) {
callAPI();
setFiltersCleared(false);
}
// eslint-disable-next-line
}, [filtersCleared]);
//...
const handleClearFiltersClick = async () => {
await orderFiltersContext.clearFilters();
setFiltersCleared(true);
};
This will ensure that callAPI was rerendered before it is executed.
That's it! IMHO a bit messy but it works.
If you want to read a bit more about this topic, feel free to checkout my blog post.

Resources