ReactJS Hooks: useState and useEffect
ReactJS introduced two new features, called hooks, in its newer versions: useState
and useEffect
. These hooks allow functional components to have their own state and lifecycle methods, which used to be possible only with class components.
Before Hooks: Class Components
In older versions of React, if a component needed to change based on user actions or data updates, we had to use class components. For simple display purposes, we used functional components.
Example: Greeting Component with a Class
Imagine a UI that asks for a username and displays a greeting:
class Greeting extends Component {
constructor(props) {
super(props);
this.state = {
name: '' // Initial state for name
};
}
handleChange = (event) => {
this.setState({ name: event.target.value });
}
render() {
return (
<div>
<input
type="text"
value={this.state.name}
onChange={this.handleChange}
placeholder="Enter your name"
/>
<p>Hello, {this.state.name || "stranger"}!</p>
</div>
);
}
}
In this example:
The state (
name
) is defined in the constructor.handleChange
updates the state when the input changes.render
method re-renders the UI whenever the state changes.
With Hooks: Functional Components
With hooks, we can achieve the same functionality in a simpler way using functional components.
Example: Greeting Component with Hooks
const Greeting = () => {
const [username, setUsername] = useState('');
const handleChange = (e) => {
setUsername(e.target.value);
};
return (
<div>
<input
type="text"
placeholder="Enter your name"
value={username}
onChange={handleChange}
/>
{username && <h2>Hello, {username}!</h2>}
</div>
);
};
In this example:
useState
hook creates a state variableusername
and a functionsetUsername
to update it.The
handleChange
function updates the state.The component re-renders whenever the state changes.
How Hooks Work
You might wonder how the state is retained in functional components since they are just functions that get called repeatedly. The answer lies in the virtual DOM, a concept in React that helps efficiently update the user interface.
When useState
is called, React stores the state in a special place called a Fiber node within the virtual DOM. This allows React to remember the state across re-renders and component navigations.
React creates a virtual DOM tree as it parses JSX, starting from the root element, and adds a node called a Fiber whenever it encounters a JSX element tag. The Fiber node contains details such as the type of element, parent, sibling, children, and more.
Each component has a virtual DOM beneath it, consisting of all the components generated by its JSX. Relevant details stored in a Fiber node include:
type: The component type (function, class, etc.).
key: A unique key for identifying nodes.
stateNode: The local state of the component.
The stateNode
is present for both class-based and function-based components.
For class-based components, the stateNode
is a JavaScript object that is updated whenever the setState
method of the component is called. For function components, the stateNode
is a list of tuples. Each tuple contains a variable and its setter method.
Class Components
For a class component, whenever it is to be rendered, React invokes the render
method, creates a new virtual DOM, compares it with the earlier DOM, and replaces it where required. It also makes a copy of the state of the component and stores it in stateNode
.
Functional Components
For functional components, the flow is slightly different. React executes the function every time a re-render is required. When the function is called, the lines with useState
calls are executed.
useState
refers to a global variable to get the current Fiber node. It checks if stateNode
is empty. If it is empty, it initializes an index counter (e.g., variableIndex
) to 0 and creates a tuple of [variable, setterFunction]
and appends it to stateNode
. If stateNode
is not empty, it picks the tuple at the current index (0) and returns it, then increments variableIndex
.
Next time useState
is called, it will now pick up the variable at index 1, and so on. This is why it is mandatory to put all the useState
statements at the top of the function and not in any conditional statements; this ensures the correct ordering since it is all referred to by position.
In summary, the state is stored in the Fiber node.
How useState
Works
Initialization:
When a component first renders,
useState
initializes state and stores it in a special structure within a Fiber node in the virtual DOM.For example,
const [count, setCount] = useState(0);
will storecount
andsetCount
as a tuple in the Fiber node.
Re-rendering:
When the component re-renders (due to state or props changes), React needs to retrieve the state values in the same order they were declared.
React keeps an internal counter (let's call it
stateIndex
) that tracks the current position of the state in the list of tuples stored in the Fiber node.
Order Matters:
The order of
useState
calls must remain consistent between renders.During each render, React will retrieve state based on the
stateIndex
.On the first
useState
call,stateIndex
is 0, so it picks the first state tuple. For the seconduseState
call,stateIndex
is 1, so it picks the second state tuple, and so on.
Why Order Matters
If you place useState
calls inside conditional statements, the order of useState
calls could change between renders. This would break the consistency required by React to correctly retrieve state values.
Example
Consider this component:
function Counter() {
const [count, setCount] = useState(0);
if (someCondition) {
const [extra, setExtra] = useState(0);
}
return (
<div>
<p>Count: {count}</p>
{/* other component code */}
</div>
);
}
Initial Render (someCondition is true):
Initialization:
const [count, setCount] = useState(0);
is called. React assigns this tostateIndex
0.stateIndex
is incremented to 1.if (someCondition) { const [extra, setExtra] = useState(0); }
is true, soextra
is initialized atstateIndex
1.stateIndex
is incremented to 2.
State Tracking:
- The Fiber node now has two state entries:
stateIndex
0 forcount
andstateIndex
1 forextra
.
- The Fiber node now has two state entries:
Second Render (someCondition becomes false):
Initialization:
const [count, setCount] = useState(0);
is called again. React assigns this tostateIndex
0 as expected.stateIndex
is incremented to 1.if (someCondition) { const [extra, setExtra] = useState(0); }
is now false, soextra
is not initialized.
State Tracking:
- React expects a state entry at
stateIndex
1, but because theuseState
call forextra
is skipped, there is a mismatch.
- React expects a state entry at
Consequences:
State Desynchronization:
The
stateIndex
for subsequentuseState
calls will be off by one.React might return incorrect state values for any further hooks, leading to unexpected behavior or bugs.
Correct Usage
To ensure the correct order, always place useState
(and other hooks) at the top level of your component function:
function Counter() {
const [count, setCount] = useState(0);
const [extra, setExtra] = useState(0);
if (someCondition) {
// Use the extra state only if needed
}
return (
<div>
<p>Count: {count}</p>
{/* other component code */}
</div>
);
}
In this corrected version, useState
calls are at the top level, maintaining their order between renders regardless of conditional logic inside the component. This ensures React can reliably track and update state.
Comparing Class Components and Functional Components
React strongly recommends the use of hooks and says that new APIs will be designed with hooks in mind. However, there are trade-offs between class-based and function-based components.
Class Components
Easier Visualization: For people used to the object-oriented paradigm, classes are easier to visualize. Traditionally, functions are verbs, and classes are nouns. Classes were called things like
Counter
orEmployee
, and their methods were calledincrement
,initiateExit
, etc.System Visualization: It was easier to visualize the system in terms of class and sequence diagrams, with a set of classes sending messages to each other in response to user actions.
Simpler Understanding: With classes, it is easier to understand what is happening without ever knowing about the virtual DOM tree or the Fiber architecture.
Explicit Contracts: Every class interface is a contract between a client and a service. This makes it clear what functionality belongs in a class and simplifies the signature of each function.
Testability: Since all the state is inside the class, it is easier to test without the React framework.
Functional Components
State Reuse: The functional approach with hooks allows for the reuse of stateful logic.
React's Future: React's future API development is focused on hooks, making them a more forward-looking choice.
Understanding Virtual DOM: An important part of the functionality of functional components rests within the virtual DOM, requiring an understanding of the virtual DOM architecture to code properly.
Side Effects: Pure functions take some input, perform calculations, and return a value without side effects. In classes, methods transform the state internally. In functional components, an invocation updates the entire state tree, making side effects seem global.
Conclusion
Hooks like useState
and useEffect
make it possible to use state and lifecycle methods in functional components, simplifying the code while maintaining the same functionality as class components. Although it requires some understanding of React's internal workings, the shift to hooks provides a more modern and streamlined way to build React applications.