useState Hook
What is the useState Hook?
This hook lets you declare a state variable.
It takes the initial state.
It returns a pair of values:
The current state
A state setter function that lets you update it.
You can use pretty much every variable inside of a state hook, even arrays and objects.
The useState hook gets called mainly always whenever it is needed to change a variable.
But! A state variable's value never changes within a render
It does not even change if the event handler's code is asynchronous.
React keeps state values fixed within one render's event handlers.
Adding the state variable
How to add the useState hook
Import useState from React at the top of the code.
Write your state hook:
With a normal variable:
let index = 0;
With a useState hook:
const [index, setIndex] = useState(0)
index
is the state variablesetIndex
is the setter function
Using the useState hook
Now, if you want to update something on click, you can add it to a handleClick
function like so:
const [index, setIndex] = useState(0);
function handleClick() {
setIndex(index + 1)
}
Anatomy of the useState hook
When you call useState
you tell React that you want this component to remember something.
Every time your component renders, useState
gives you an array with two values:
State variable: the value you stored.
State setter function: this can update the state variable and trigger React to render the component again.
const [index, setIndex] = useState(0)
What happens in action?
Component renders for the first time, standard value is 0 so it will return
[0, setIndex]
.The state gets updated so it calls the function and executes
setIndex(index + 1)
.Component renders a second time and does the same thing again.
A component can have as many state variables as you would like in one component.
It is especially a good idea if there are multiple state variables if their state is unrelated to each other.
What does it mean to render a state hook?
Rending is what we call it when React is calling your component, and your component is a function. The JSX that you return from this function can be seen as a snapshot of the UI in time. It has props, event handlers and local variables. All these things were calculated with the state that was available at the time of the render.
This snapshot you return is interactive and includes logic. React updates the screen to match this snapshot and connects the event handlers. This means that when you press a button it triggers the click handler from your JSX.
What happens when you re-render a component?
React calls your function again.
Your function returns a new JSX snapshot.
React updates the screen so it matches the snapshot you just returned.
React batches state updates
React waits until all code in the event handlers has run before it will start processing state updates.
This is convenient because it makes it possible to update multiple variables without triggering more re-renders.
What is batching?
This is the process in which your UI will only be updated after your event handler has completed.
Batching makes React apps run faster.
However, React will not batch across multiple intentional events. So let's say you have clicks, every click will be handled separately.
React will only do batching when it's generally safe for it to do so.
Updating state variables multiple times before rendering
This is something that does not happen very often.
If you do want to do this, you can pass a function that calculates the next step based on the previous one in the queue instead of just passing the next state value.
That would mean that this:
setNumber(number + 1)
Turns in to this:
setNumber(n => n + 1)
The above mentioned function can also be seen as an updater function.
It has behavior that happens when you pass a state setter:
React first queues this function so that it gets processed only after all the other code in the event handler has run.
React then goes through thee queue during the next render and gives you a final update.
Example
// queued update
n => n + 1
// n = 0
// returns 0 + 1 = 1
// queued update
n => n + 1
// n = 1
// returns 1 + 1 = 2
// queued update
n => n + 1
// n = 2
// returns 2 + 1 = 3
Working with Objects in State
You should not change objects in your state directly, you should instead create a new one or make a copy of the existing one and then use that copy to set the state.
Immutable values like numbers, strings and booleans can be replaced.
Technically, it is possible to change the contents of an object by mutating it. Let's say you have state with an object.
const [position, setPosition] = useState({ x: 0, y: 0});
//mutationposition.x = 5;
But, you should be treating state like it is read-only.
But, when you want to trigger a re-render, you can create a new object and pass it to the state setting function.
onPointerMove={e => {setPosition({x: e.clientX,y: e.clientY});}}
Copy object with spread syntax
But what if you want the object to remain as is and only change one thing? You can use the object spread syntax (...)
.
// not the way to go
setPerson({
firstName: e.target.value, // new first name from the input
lastName: person.lastName,
email: person.email
});
// way to go
setPerson({...person, // copy the old fields
firstName: e.target.value // but override this one
});
Spread syntax is shallow and therefore only copies things one level deep.
Updating a nested object
When you have a nested object, you can not just use mutation. If you want to update nested objects, you would have to first update the nested object and its content and then the parent object and its content, which is tricky and quite some work.
You can also write it as a single function call
setPerson({...person, // copy other fields
artwork: { // but replace others
...person.artwork, // with the same one
city: 'New Delhi' // but in a new city
}
});
Package information: Immer
Immer is a package that can help be convenient when nested spreads are confusing to you. It let's you write the 'mutating syntax' and then takes care of producing copies for you.
updatePerson(draft => {draft.artwork.city = 'Lagos';});
Working with Arrays in State
Just like objects, but unlike strings, numbers and booleans, arrays are also mutable. But, just like with objects, once they are stored in a state, they should be treated as if they are immutable.
It is the same as with objects, when you want to change an array, you have to create a new one or copy the old one.
Common Array Operators
Adding
Avoid
push
andunshift
.Prefer
concat
and[...arr]
.
Removing
Avoid
pop
,shift
, andsplice
.Prefer
filter
, andslice
.
Replacing
Avoid
splice
, andarr[i] = ...
assignment.Prefer
map
.
Sorting
Avoid
reverse
andsort
.The preferred method is to first copy the array.
Adding to an array
Avoid
push
andunshift
Pushing will mutate the array, which is bad practice.
Prefer
concat
and[...arr]
It is better to create a new array that already contains the existing items and then add the new item at the end.
The easiest way is to use the array spread syntax:
setArtists( // replace the state
[ // with a new array.
..artists, // that contains all the old items
{ id: nextId++, name: name } // and one new item at the end
]
);
You can also add the new item above the spread syntax.
Removing from the array in the right way
Avoid
pop
,shift
,splice
.Prefer
filter
andslice
.
The easiest way is to use the filter method:
setArtists(artists.filter(a => a.id !== artist.id));
The above example means that you can create an array that contains the artists that have different ID's from artist.id
.
Transforming an array
It is always possible that you want to change some items in the array, you can use map()
to create a new array.
You pass a function to map
, and that function can then decide what will happen with each item based on the data or index it has.
Replacing items in an array
Avoid
splice
andarr[i] = ...
assignment.Prefer
map
.
Instead of mutating the original array, we can also use map for when we want to replace items in an array.
When you want to replace an item, you can go ahead and create a new array with map
. Inside of the map
, you will receive the item index as the second argument. This is where you can decide wether you want to return the original item or if you want to return something else.
Inserting into an array
When you want one item to go in to the array somewhere in a particular position, you can use the array spread operator together with the slice()
method.
The slice
method can cut a slice off of the array.
You can create an array that spreads the slice before the point where you want to add that new item, you can then add that and add back the rest of the original array.
A few notes:
Lifting up a state
What is you want the state of two components to always change together? You can fix this by removing the state from both of them, and moving it to their closest common parent. Then, instead of those two both having separate states, they can be passed down to the parent through props.
State is isolated and private
State is local to a component instance on the screen. If you render the same component twice, each copy will have completely isolated state. This makes sure they do not all change when you change one of them.
It is different than regular variables declared at top level in your module of component. State is not tied to a particular function call or a place in te code. It is local to the specific place on the screen.