React useState hook. What is it, and how can it help you manage client state?
React useState hook is a function that allows you to add React state into your functional components. In this short tutorial, I'll show you how to use it and its use cases.
Table of Contents
- TL;DR
- Function signature
- How does it work?
- How can we use React useState?
- What's the purpose of these state types (client vs. server)?
- Conclusion
TL;DR
The useState hook helps you manage state in your functional components, the state can be declared by calling useState with the optional value of any type - const [count, setCount] = React.useState(0). The main use case is client state management.
If you are not familiar with hooks yet, you might check out hooks intro on the official ReactJS site to get familiar with hooks.
Function signature
function useState<S>(initialState: S | (() => S)): [S, Dispatch<SetStateAction<S>>]
Okay, this looks a bit complicated if you don't know TS, so let's break it into two parts.
Function params
function useState(initialState?)// Expects only one param "initialState" that is optional// A value for "initialState" can be of any type:// primitive type (string, number, boolean, etc.)// array (indexed collection)// object// function - keep in mind that the function has to return value
Return value
function useState(...): [state, callback]// -> We get back an Array with 2 values (state & callback)// State - "initialState" we passed or undefined// Callback - dispatch callback to set state
As stated in code block comments, function expects one optional parameter. It returns an Array with 2 values, first one is state and the second one is a function (callback to change state value).
How does it work?
import React from 'react'export default function ClickCounter() {const [count, setCount] = React.useState(0)return (<div><span>Clicks count: {count}</span><button onClick={() => setCount(count + 1)}>Click</button></div>)}
We created a primitive functional component in the example above that counts clicks. It consists of text and a button. As you can see, we called React.useState and passed 0 as an initial parameter.
const [count, setCount] = React.useState(0)// It's just a shorthand ofconst arr = React.useState(0)const state = arr[0]const setCount = arr[1]
If you're not familiar with this syntax, const [value1, value2] = randomArray, it is called destructuring assignment and helps you unpack values from arrays or properties from objects.
Going back to the example we defined above, the onClick handler is executed whenever the user clicks on a button. In our example, we call setCount and pass value, which is the current count plus one - setCount(count + 1)
Let's make it more interesting, and add a reset button.
import React from 'react'export default function ClickCounter() {const [count, setCount] = React.useState(0)return (<div><span>Clicks count: {count}</span><button onClick={() => setCount(count + 1)}>Click</button><button onClick={() => setCount(0)}>Reset</button></div>)}
As you can see, we have just added a button to execute handler on click and call setCount with 0. This sets the state back to 0. Pretty cool.
How can we use React useState?
In general, the primary use case for useState is client state management. Client state represents any non-async data, i.e., data that we don't have to fetch from a server.
What can the client state represent?
- Theme
- Popups (modals) and popovers
- Input data
- Success / Warning / Error states
Theme (dark mode) example:
import React from 'react'export default function App() {const [isDark, setDark] = React.useState(false)return (<div style={{ background: isDark ? 'black' : 'white' }}><button onClick={() => setDark(!isDark)}>Switch dark mode</button><span style={{ color: isDark ? 'white' : 'black' }}>App with dark mode</span></div>)}
Input data with an error state example:
import React from 'react'import axios from 'axios'export default function UserForm() {const [name, setName] = React.useState('')const [error, setError] = React.useState(false)const handleSubmit = () => {axios.post('/users', { name }).catch(() => {setError(true)})}return (<div><input value={name} onChange={e => setName(e.target.value)} />{error && (<span>An error occured!</span>)}<button onClick={handleSubmit}>Submit form</button></div>)}
In the example above, we used axios to help us send post request on /users endpoint. If we catch the error, we set the error to be true and then conditionally render the message above the button.
Server state handled with React useState
We can use React useState to store async data from a server.
Storing async data (server state) example:
import React from 'react'import axios from 'axios'export default function UsersList() {const [users, setUsers] = React.useState([])React.useEffect(() => {axios.get('/users').then(res => {setUsers(res.data)})}, [])return (<div><span>Users list:</span>{users.map(user => (<div key={user.id}>{user.name}</div>))}</div>)}
In our example, we do get request to /users endpoint and expect data to be an array. In case of success, we set a state and pass the response data.
Bear in mind that this example ignores errors, undefined response data, and so on. It's omitted for brevity.
What's the purpose of these state types (client vs. server)?
So far, I've only talked about the client state and server state.
Let's make the distinction between client and server state even more explicit:
- Client state - You don't have to request a server, and you're dealing with synchronous data.
- Server state - You have to request the server, i.e., you have to call the endpoint; thus, you're dealing with asynchronous data.
Now that it's clear, we can further divide both of these state types into two categories:
- Local state - Is scoped to specific components (it can be scoped even for an entire screen).
- Global state - Is available app-wide.
Local state
To make it clear, if we take our example with UserForm (input data with error state), it is a good example of a local state. The name and error state variables are very specific to the UserForm, and if we define children components, we can pass these down as props.
Outside of the UserForm though, the state is not accessible. That is a good thing since we encapsulated logic in a specific component or set of components.
Global state
There is no pure example with the global state above, though if you look at the theme (dark mode) example, we defined state in App, which is a root component of our app. It's available for all components, but we still have to pass the desired props down, perhaps even through multiple layers.
If you already know a bit about React and its ecosystem, you might think of an obvious solution, such as using a state management library (Redux) that helps you manage the state globally.
While redux is cool and solves the issues above, I suggest keeping a rigorous line between client and server state. For that, check out React Query or SWR.
Going back to our issue, we can solve this by using React Context. It's native to React and works without extra dependency or boilerplate, but more in another lesson.
Nonetheless, if we would wrap the App in Context provider, the state would be available for all components and accessible without passing props. That is an example of a global state.
In general, though, if you find yourself moving a local state declaration upward a lot, it might as well be because your parent components are too smart, i.e., having too much responsibility. There are patterns to avoid this, and I'll gladly share them.
Conclusion
React useState is a native way to handle a client state in your app. It helps you manage state in specific components (local state), and combined with useContext and other hooks, it can serve well beyond that.
If you don't use any 3rd party library to handle your server state, you might also use useState for storing the async data, although it's good to have something proper in place for that.
That's all for today! I hope you learned something new. Don't forget to share this post if you find it useful.