HomeAbout Me

React Hooks 1: Managing UI State

By Daniel Nguyen
Published in React JS
May 11, 2025
4 min read
React Hooks 1: Managing UI State

Managing UI State

Dynamic applications allow users to interact and make changes to what they see on the page and the application should respond by updating the UI based on what the user has done. We accomplish this by using state. When state changes (for example as the result of user interaction and input), we update the UI. Here’s how that works with React:

render --> set up the DOM --> user interacts --> state changes --> re-render --> update the DOM --> return to user interacts
render --> set up the DOM --> user interacts --> state changes --> re-render --> update the DOM --> return to user interacts

There’s a cycle of user interaction, state changes, and re-rendering. This is the core of how React works for interactive applications.

The render phase is what we’ve done so far with creating React elements. Handling user interactions is what we do with event listeners like onChange. Now we’re going to get into the state changes bit.

In React, you use special functions called “hooks” to do this. Common built-in hooks include:

  • useState
  • useRef
  • use
  • useReducer
  • useEffect

Each of these is a special function that you can call inside your custom React component function to store data (like state) or perform actions (or side-effects). There are a few more built-in hooks that have special use cases, but the ones above are what you’ll be using most of the time.

Each of the hooks has a unique API. Some return a value (like useRef and use), others return a pair of values (like useState and useReducer), and others return nothing at all (like useEffect).

Here’s an example of a component that uses the useState hook and an onClick event handler to update that state:

function Counter() {
const [count, setCount] = useState(0)
const increment = () => setCount(count + 1)
return <button onClick={increment}>{count}</button>
}

useState is a function that accepts a single argument. That argument is the initial state for the instance of the component. In our case, the state will start as 0.

useState returns a pair of values. It does this by returning an array with two elements (and we use destructuring syntax to assign each of those values to distinct variables). The first of the pair is the state value and the second is a function we can call to update the state. We can name these variables whatever we want. Common convention is to choose a name for the state variable, then prefix set in front of that for the updater function.

State can be defined as: data that changes over time. So how does this work over time? When the button is clicked, our increment function will be called at which time we update the count by calling setCount.

When we call setCount, that tells React to re-render our component. When it does this, the entire Counter function is re-run, so when useState is called this time, the value we get back is the value that we called setCount with. And it continues like that until Counter is unmounted (removed from the application), or the user closes the application.

Note that after the initial render, the argument passed to useState is ignored. The only time it’s used is when the component is first created. The only way to change the state value of the component while it’s around is to call the updater function returned by useState.

📜 To get a deeper dive on how React keeps track of hooks, read/watch this great post/talk by Shawn Wang: Getting Closure on Hooks

📜 And here’s a reference to the hooks official documentation.

Derive State

🦉 Often, it can be easy to think you need to keep track of two elements of state when you really only need one. For example, let’s say you have a counter that will display the number of times a user has clicked a button and also it will display whether that number is odd or even. You might be tempted to write the following code:

import { useState } from 'react'
export default function Counter() {
const [count, setCount] = useState(0)
const [isEven, setIsEven] = useState(true)
function handleClick() {
const newCount = count + 1
setCount(newCount)
setIsEven(newCount % 2 === 0)
}
return (
<div>
<p>{count}</p>
<p>{isEven ? 'Even' : 'Odd'}</p>
<button onClick={handleClick}>Increment</button>
</div>
)
}

This code works, but it’s not ideal because it’s keeping track of two pieces of state when it only needs to keep track of one. Imagine if we had multiple places where the count could be changed. We’d have to remember to update the isEven state in all of those places. This is a recipe for bugs.

Instead, we can derive the isEven state from the count state:

import { useState } from 'react'
export default function Counter() {
const [count, setCount] = useState(0)
function handleClick() {
const newCount = count + 1
setCount(newCount)
}
// this is the derived state
const isEven = count % 2 === 0
return (
<div>
<p>{count}</p>
<p>{isEven ? 'Even' : 'Odd'}</p>
<button onClick={handleClick}>Increment</button>
</div>
)
}

This is a much better solution because we only have to keep track of the count state. The isEven state is derived from the count state. This means we don’t have to worry about keeping the isEven state in sync with the count state.

👨‍💼 Thanks Olivia! So what we want to do in this step is derive the checkboxes’ checked state based on whether the query contains the word they represent.

Give that a shot.

Initialize State

👨‍💼 We want users to be able to share a link to this page with a prefilled search. For example: https://www.example.com/search?query=cat+dog.

Right now, this won’t work, because we don’t have a way to initialize the state of the search box from the URL. Let’s fix that.

The useState hook supports an initial value. Right now we’re just passing '' as the initial value, but we can use the URLSearchParams API to get the query string from the URL and use that as the initial value.

const params = new URLSearchParams(window.location.search)
const initialQuery = params.get('query') ?? ''

Then you can pass that initialQuery to useState. Give that a shot and then the link above should work!

Init Callback

🦉 There’s one more thing you should know about useState initialization and that is a small performance optimization. useState can accept a function.

You may recall from earlier we mentioned that the first argument to useState is only used during the initial render. It’s not used on subsequent renders. This is because the initial value is only used when the component is first rendered. After that, the value is managed by React and you use the updater function to update it.

But imagine a situation where calculating that initial value were computationally expensive. It would be a waste to compute the initial value for all but the initial render right? That’s where the function form of useState comes in.

Let’s imagine we have a function that calculates the initial value and it’s computationally expensive:

const [val, setVal] = useState(calculateInitialValue())

This will work just fine, but it’s not ideal. The calculateInitialValue will be called on every render, even though it’s only needed for the initial render. So instead of calling the function, we can just pass it:

const [val, setVal] = useState(calculateInitialValue)

Typically doing this is unnecessary, but it’s good to know about in case you need it.

So

// This will call getQueryParam on every render, undermining our optimization! 😵
const [query, setQuery] = useState(getQueryParam())
// This will _only_ call getQueryParam on init. Great! ✅
const [query, setQuery] = useState(getQueryParam)

You’re going to be making the getQueryParam function. Got it? Great, let’s go!

🚨 Note, we can't reasonably test whether you're doing this right so the tests are passing from the get-go, but you'll know you didn't break anything if the tests are still working when you're finished.
import { useState } from 'react'
import { createRoot } from 'react-dom/client'
import { generateGradient, getMatchingPosts } from '#shared/blog-posts'
function getQueryParam() {
const params = new URLSearchParams(window.location.search)
return params.get('query') ?? ''
}
function App() {
const [query, setQuery] = useState(getQueryParam)
const words = query.split(' ')
const dogChecked = words.includes('dog')
const catChecked = words.includes('cat')
const caterpillarChecked = words.includes('caterpillar')
function handleCheck(tag: string, checked: boolean) {
const newWords = checked ? [...words, tag] : words.filter(w => w !== tag)
setQuery(newWords.filter(Boolean).join(' ').trim())
}
return (
<div className="app">
<form>
<div>
<label htmlFor="searchInput">Search:</label>
<input
id="searchInput"
name="query"
type="search"
value={query}
onChange={e => setQuery(e.currentTarget.value)}
/>
</div>
<div>
<label>
<input
type="checkbox"
checked={dogChecked}
onChange={e => handleCheck('dog', e.currentTarget.checked)}
/>{' '}
🐶 dog
</label>
<label>
<input
type="checkbox"
checked={catChecked}
onChange={e => handleCheck('cat', e.currentTarget.checked)}
/>{' '}
🐱 cat
</label>
<label>
<input
type="checkbox"
checked={caterpillarChecked}
onChange={e =>
handleCheck('caterpillar', e.currentTarget.checked)
}
/>{' '}
🐛 caterpillar
</label>
</div>
<button type="submit">Submit</button>
</form>
<MatchingPosts query={query} />
</div>
)
}
function MatchingPosts({ query }: { query: string }) {
const matchingPosts = getMatchingPosts(query)
return (
<ul className="post-list">
{matchingPosts.map(post => (
<li key={post.id}>
<div
className="post-image"
style={{ background: generateGradient(post.id) }}
/>
<a
href={post.id}
onClick={event => {
event.preventDefault()
alert(`Great! Let's go to ${post.id}!`)
}}
>
<h2>{post.title}</h2>
<p>{post.description}</p>
</a>
</li>
))}
</ul>
)
}
const rootEl = document.createElement('div')
document.body.append(rootEl)
createRoot(rootEl).render(<App />)

Tags

#ReactHooks

Share

Previous Article
React Fundamental Section 10: Rendering Arrays

Table Of Contents

1
Managing UI State
2
Derive State
3
Initialize State
4
Init Callback

Related Posts

React Hook Section 6: Tic Tac Toe
May 16, 2025
2 min
© 2025, All Rights Reserved.
Powered By

Quick Links

About Me

Legal Stuff

Social Media