All you need to know about useState in 2025

All you need to know about React useState hook in 2025

Introduction

I have used the useState hook like a million times through out my career and to me it felt like a very basic thing that every web developer should know but whenever i talk to an entry level developer, they are still confused about the do’s and don’t’s (and sometimes even confused about what it is) of using useState. So here is yet another article about useState hook where i try and explain the hook based on my experience.

To be honest, this article wont give you any new information which isn’t already available on the offical react docs. The main purpose of this article is to refresh my knowledge about useState and maybe give a fresh perspective of how a senior software engineer think and uses it in their day to day work.

What is a useState hook?

Official definition:

useState is a React Hook that lets you add a state variable to your component.

My understanding:

Every time in you component code you need to save a variable and make sure that React library safely keeps track of its updates, you need to use react useState.

Why use useState?

Then you may ask, why not just use vanilla JS var or let keyword?

Read the emphasised phrase in the above definition. Keeps track! thats the main distinction. Every time there is a change, the entire component re-renders (i.e. the code reruns which the state snapshot react holds).

If we just you the vanilla var or let, it will be re initialised on every render cycle of that react component. This means, any updates to that variable in the component code won’t persist in react’s memory and will be discarded.

Lets have a look at a basic using of let/var in react component code

function Counter() {
let count = 0;
const handleClick = () => {
count++;
}
return (
<>
<button onClick={handleClick}>Click me!</button>
<span>{count}</span> // count will be always 0
</>
)
}

In the above example, no matter how many times you click on the button, the variable wont update. This happens because every time you click that button in the output, react understands that something happened on the page (in this case, a click event) and triggers a re-render and re-runs everything inside the function Todo. This results in reinitialising the count variable to 0 and even though you are trying to update the count, the UI wont show the updated value as it doesn’t persist in react’s memory.

How to use useState?

Now that you know why we should use useState hook in react, let me show you how to use it for various cases

Case 1: Basic usage (primary data types i.e number, string, boolean)

Here is a refactored code of the above example with useState hook:

function Counter () {
const [count, updateCount] = useState<number>(0);
const handleClick = () => {
updateCount((previousCount) => previousCount+1);
// updateCount(count+1);
// updateCount(count++);
}
return (
<>
<button onClick={handleClick}>Click me!</button>
<span>{count}</span> // count will be always 0
</>
)
}

Case 2: Objects and Arrays

When dealing with objects and arrays, we need to make sure we have access to the previous version of that variable in react’s memory and then update on top of it when updating data of type object.

Have a look at the example below where i am updating a new todo into a todos state which holds all the existing todos data:

interface ITodo {
label: string;
completed: boolean;
}

function Todos () {
const [input, updateInput] = useState<string>('');
const [todos, updateTodos] = useState<ITodo[]>([]);
const generateTodo = (value: string) => {
return {
label: value,
completed: false,
}
}
const handleClick = (value: string) => {
updateTodos((prevTodos) => [
...prevTodos,
generateTodo(value)
]);
updateInput("");
}
return (
<>
<input type="text" onChange={(e) => updateInput(e.target.value)}/>
<button disabled={input.length === 0} onClick={handleClick}>Add Todo</button>
{
todos.map(({ label, completed }) => <Todo key={label} label={label} completed={completed} />)
}
</>
)
}

now, lets have a look at similar example for Javascript object where an object holds all the todos and you need add another todo into it:

interface ITodo {
label: string;
completed: boolean;
}

function Todos () {
const [input, updateInput] = useState<string>('');
const [todos, updateTodos] = useState<ITodoMap>({});
const generateTodo = (value: string) => {
return {
label: value,
completed: false,
}
}
const handleClick = (value: string) => {
const uniqueId = uuid(); // generates unique id using uuid library
updateTodos((prevTodos) => {
...prevTodos,
[uniqueId] : generateTodo(value)
});
updateInput("");
}
return (
<>
<input type="text" onChange={(e) => updateInput(e.target.value)}/>
<button disabled={input.length === 0} onClick={handleClick}>Add Todo</button>
{
Object.entries().map(([id, {label, completed}]) => <Todo key={id} label={label} completed={completed} />)
}
</>
)
}

I hope this clears an doubts around updating a state when dealing with objects/arrays. If you have any more doubts, feel free to post it in the comments below.

Case 3: Functions

Lets be clear, i am not a fan of holding functions in react state and i don’t recommend it. As a reviewer, I don’t accept PR’s if a dev does this, unless there is a strong reason (like async code injection etc.) for doing this and there are no alternatives (like use refs).

Lets have a look at an example (i am going to continue from our previous example) where you might have to hold function in state because it’s dynamic:


function Todos () {
const [input, updateInput] = useState<string>('');
const [todos, updateTodos] = useState<ITodoMap>({});
const [onComplete, updateOnComplete] = useState<Function>(() => {console.log("initial Render")})
const generateTodo = (value: string) => {
return {
label: value,
completed: false,
}
}

/* lets assume, there is a remote module which serves
the onComplete fn and our component fetches it and
uses it in the onClick event handler
*/
useEffect(() => {
const fetchRemoteOnComplete = async () => {
console.log("Fetching remote onComplete logic...");
// Simulate remote response with function body
const response = await fetch('/api/remote-on-complete');
const result = await response.json();

const dynamicFn: () => void; = new Function(result.args, result.body);

setOnComplete(() => dynamicFn);
console.log("Injected remote onComplete function");
};

fetchRemoteOnComplete();
}, []);

const handleClick = (value: string) => {
const uniqueId = uuid(); // generates unique id using uuid library
updateTodos((prevTodos) => {
...prevTodos,
[uniqueId] : generateTodo(value)
});
updateInput("");
onComplete() // the dynamic fn if fetched properly, will be executed here.
}
return (
<>
<input type="text" onChange={(e) => updateInput(e.target.value)}/>
<button disabled={input.length === 0} onClick={handleClick}>Add Todo</button>
{
Object.entries().map(([id, {label, completed}]) => <Todo key={id} label={label} completed={completed} />)
}
</>
)
}

I hope the above example give you an understanding of when to save function (if it’s a dynamic one, only then) into react state using useState hook.

Conclusion

I hope this article gives you more clarity about React useState hook and helps you in your journey of becoming a better Web Engineer by the day.

Feel free to put your thoughts/comments/doubts in the comment section below and we can have a healthy discussion about it and learn more from each other.

I am on a journey to learn all i can about the topics i am planning to blog about. This will help me improve my knowledge and stay udpated (with you guys) in the process.

Subscribe to my website if you like my content and learn more about web/app design/development topics.

Have a great day 🙂

Leave a Comment

Your email address will not be published. Required fields are marked *