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:
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.
🦉 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 + 1setCount(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 + 1setCount(newCount)}// this is the derived stateconst isEven = count % 2 === 0return (<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.
👨💼 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!
🦉 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!
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><inputid="searchInput"name="query"type="search"value={query}onChange={e => setQuery(e.currentTarget.value)}/></div><div><label><inputtype="checkbox"checked={dogChecked}onChange={e => handleCheck('dog', e.currentTarget.checked)}/>{' '}🐶 dog</label><label><inputtype="checkbox"checked={catChecked}onChange={e => handleCheck('cat', e.currentTarget.checked)}/>{' '}🐱 cat</label><label><inputtype="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}><divclassName="post-image"style={{ background: generateGradient(post.id) }}/><ahref={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 />)
Quick Links
Legal Stuff
Social Media