Hello there👋
This is the first article of the React Hooks Unboxed Series. In this article, we would be discussing the useState hook with practical examples.
An Introduction
The useState hook is used to manage state in React. State is simply data that can change over time. The useState hook lets us create a state variable, initialize it with data and also gives us access to a setter function that lets us update this state.
This is especially important in React which, just like the name suggests, is a library that is designed to efficiently update and react to changes in the user interface. The library efficiently updates the DOM when the application state changes, providing a reactive programming model.
The ability to update state is essential in the creation of modern, responsive web applications, and can be useful to execute the following operations:
Dynamic UI Updates: State allows React to monitor and re-render components and update the UI based on changes in data or user interactions.
User Input Handling: State is often used to manage user input, such as form data. When users interact with an application, the state can capture and reflect the current input values.
Component Communication: Parent components can pass down state as props to child components, enabling data flow and communication between components and synchronization.
Asynchronous Operations: State is essential for handling asynchronous operations, such as fetching data from an API. It helps to manage loading states, error states, and the display of fetched data.
Conditional Rendering: Components can use state to conditionally render different parts of the UI based on certain conditions. This way we can show or hide elements dynamically.
Create A State Variable
Here's how we can create a state variable using the useState hook.
const [state, setState] = useState(initialValue)
state
is the state variable.
setState
is the setter function that lets us update the state variable, triggering React to re-render the component.
initialValue
is the value we use to initialize the state variable.
A simple example is if we want to monitor and display the name provided by a user.
import { useState } from "react";
export default function App() {
const [name, setName] = useState("");
return (
<div className="App">
<input
type="text"
placeholder="Enter name"
value={name}
onChange={(e) => setName(e.target.value)}
/>
<p>Hello {name}</p>
</div>
);
}
In this example, we initialize the name
state variable with an empty string, ""
, and monitor input changes using the event handler, onChange
. Once a user updates the text in the input element, the setter function, setName
, is run and the argument passed into it, i.e. the current value in the input element, e.target.value
, is stored in the name
state. Once the state has been updated, React re-renders the component and updates the UI, letting us display the updated name.
You can also pass a function instead of a variable as the initial value. This function is called the initializer function and it runs only once, at the first render of that component. It should be pure and and should return a value, which is used to initialize the variable.
You should pass the function and not call the function when initializing with the useState hook. Passing a function ensures it is only called on the first render of that component and consequently ignored.
function getName () {
return 'Paul'
}
// wrong👎
const [name, setName] = useState(getName());
// right 👍
const [name, setName] = useState(getName);
Another way of updating state using the setter function is to pass a pure function into it instead of a value. This pure function has access to the current state which you can use as a parameter in your function to calculate the next state. This pure function that updates state is called an updater function.
const [name, setName] = useState('TITO');
setName(currentName => currentName.toLowerCase());
// console.log(name) => 'tito'
Here, our setter function accesses the current name
, 'TITO', and updates it to lowercase, 'tito'.
Do not try to set state in this function. The updater function must be pure and should take only the current state as an argument.
An Example
Now, we will create a login form to authenticate the users of our app. What do we need this login form to do:
Collect email and password user input. We can collect user input using the form element.
Monitor and store these values in state as they change e.g. when a user is entering input or changing it
Submit this form using user provided data
Display feedback after authentication
import { useState } from "react"; export default function App() { const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); const [success, setSuccess] = useState(false); const [error, setError] = useState(false); function handleSubmit(e) { e.preventDefault(); // submit email and password via post request for server authentication if (email === "dev@gmail.com" && password === "password") { // setTimeout is used to simulate server delay of 2s setTimeout(() => { // simulate message pop up setSuccess(true); setTimeout(() => setSuccess(false), 2000); }, 2000); } else { setTimeout(() => { // simulate message pop up setError(true); setTimeout(() => setError(false), 2000); }, 2000); } } return ( <div className="App"> <form onSubmit={handleSubmit}> <input type="email" placeholder="Enter email" value={email} onChange={(e) => setEmail(e.target.value)} required /> <br /> <input type="password" placeholder="Enter password" value={password} onChange={(e) => setPassword(e.target.value)} required /> <br /> <button type="submit">Log in</button> </form> <!-- display notification messages --> {success && <p>Welcome back!</p>} {error && <p>Are you a hacker or just forgetful? Try again😁</p>} </div> ); }
In this example, we have been able to show the power of useState as stated at the beginning of the article. We handled user input by monitoring changes and storing them in the email and password states, conditionally rendered UI (notification messages) and dynamically updated the UI with a message based on the success
and error
states, and handled updated states asynchronously.
Typically, the handleSubmit()
function makes an asynchronous POST API request to a server which is either successful or not depending on if the user submitted the right data. We can update the success and error states based on the server response.
// ...
try {
// make POST API request
// await response
// if status 200, setSuccess(true)
// if status 400, throw Error
} catch(err) {
// catch error, setError(true)
}
What if we want to add interactivity for better UX? Typically, response after an API request may be delayed due to varying factors. So, we can display a loading message while our user waits. Our form can be in two different states in terms of submission, loading and not-loading. We can use the useState hook to manage this state!
import { useState } from "react";
export default function App() {
// ...
const [loading, setLoading] = useState(false);
function handleSubmit(e) {
e.preventDefault();
// simulate loading
setLoading(true);
// submit email and password via post request for server authentication
if (email === "dev@gmail.com" && password === "password") {
setTimeout(() => {
setSuccess(true);
// stop loading after success
setLoading(false);
// simulate message pop up
setTimeout(() => setSuccess(false), 2000);
}, 2000);
} else {
setTimeout(() => {
setError(true);
// stop loading after error
setLoading(false);
// simulate message pop up
setTimeout(() => setError(false), 2000);
}, 2000);
}
}
return (
<div className="App">
<form onSubmit={handleSubmit}>
<!-- ... -->
<button type="submit">{loading ? "Loading..." : "Log in"}</button>
</form>
<!-- ... -->
</div>
);
}
Libraries like react-query help provide abstraction, managing and giving us access to our loading, error, success and response(data) states, so our code is not so verbose.
Updating arrays and objects
One thing that stumps beginners is updating array and object states. When managing array and object state, one should aim to replace with a new value instead of mutate the existing one. For instance, if we have a user state variable that stores the following properties; name, age and sex, we can update the name
property by using the spread syntax(...
) to copy existing data and then override the name
property with the new value.
const [user, setUser] = useState({
name: 'tito',
age: 22,
sex: female
});
// wrong👎
user.name = 'esther';
// right👍
setUser({...user, name: 'esther'})
One should also aim to treat state arrays as immutable and read-only, utilizing methods that return a copy of the array instead of mutating the existing one.
Examples of such non-mutating methods include filter, concat, slice and map.
const [names, setNames] = useState([]);
// wrong👎
names.push('john');
// right👍
setNames([...names, 'john']) // adds 'john' to end of list
setNames(['john', ...names]) // adds 'john' to beginning of list
setNames(names.filter(name => name.startsWith('e')) ); // deletes 'esther'
The last line creates a new array of names that contain elements that do not start with an 'e', effectively deleting 'esther' from the array.
Component Communication
State can be passed as props from a parent to a child component.
import { useState } from "react";
export default function App() {
const [email, setEmail] = useState("");
return (
<div className="App">
<button
onClick={() => setEmail("dev@gmail.com") }
>
Show
</button>
<ChildComponent email={email} />
</div>
);
}
const ChildComponent = ({ email }) => {
return <>{email && <p>This {email} was passed from my dad!</p>}</>;
};
The email
state variable is passed from the parent component, App.jsx
, to ChildComponent.jsx
which destructures the props object to access email
. When a user clicks the show button, you get:
React State Variables vs Regular Variables
You may wonder why we don't just use regular variables. We can, after all, declare and initialize a variable and also update/replace the value stored in it by reassignment.
// declare and initialize variable
var name = 'tito';
const [name, setName] = useState('tito');
// update name value
name = 'esther';
setName('esther')
These are primary reasons why this won't work:
Changes in regular variables do not trigger React to initiate a render.
Regular variables do not persist between renders. Therefore, if that component re-renders, 'esther' goes back to being 'tito'. React simply won't consider any changes to regular variables.
This is the problem the useState hook solves. It lets us persist data between renders and also initiate re-renders with the newly updated state data.
Recap
In this article we have discussed what the useState hook is, its use cases and why it is important and preferable to regular variables. We've also covered examples to properly illustrate how to use it.
Please leave a like if this was helpful. Comment also if you have questions.
Happy Coding🙌