Discover how React’s functional components, powered by the Hook API, revolutionized Java development by simplifying state management and lifecycle handling—making class components a thing of the past.
Two types of components exist in React: class components and function components. As the names suggest, some are based on classes, others on functions. For a long time, the community discussed smart and dumb components (i.e., intelligent and stupid components) because, until a few releases ago, function components could only be used to represent structures. They could not have their own states or lifecycles.
However, that limitation changed with React 16.8, which introduced the Hook Application Programming Interface (API). As of that version, you can also manage a local state and the lifecycle of the component in functional components. This release also marked the demise of the heavier-weight class components to the point where hardly any class components are used in modern React applications today. For this reason, we’ll also focus exclusively on functional components in this post.
A big advantage of React is that components are quite lightweight. A component consists of a function that returns a JSX structure. In the first step, we start with a static list component that will display the contact list we’ll later fetch from the REST API. The listing below shows the source code of the static list component.
const contacts = [
{
id: 1,
firstName: 'John',
lastName: 'Doe',
email: 'johndoe@example.com',
},
{
id: 2,
firstName: 'Erica',
lastName: 'Doe',
email: 'ericadoe@example.com',
},
];
function List() {
return (
<table>
<thead>
<tr>
<th>ID</th>
<th>First name</th>
<th>Last name</th>
<th>Email</th>
</tr>
</thead>
<tbody>
{contacts.map((contact) => (
<tr key={contact.id}>
<td>{contact.id}</td>
<td>{contact.firstName}</td>
<td>{contact.lastName}</td>
<td>{contact.email}</td>
</tr>
))}
</tbody>
</table>
);
}
export default List;
For the list representation, we’ll use the contacts array, which consists of two entries. The array is defined outside the component because it is independent of it. Also, under certain conditions, the component function is called more frequently by React, which would cause the contacts array to be regenerated as well.
The core of the component is the List function. As you know, components are distinguished from elements by an uppercase first letter, so the component function also starts with an uppercase letter. The component returns an HTML table. In a way, JSX is the template language of React with the difference that JSX has ability to use logical operations or loops. In this case, you must use native JavaScript. In the case of the List component, a table row should be created for each record.
You can usually achieve this kind of iteration over an array structure in React using the map() method of the array. Unlike the forEach() method, map() produces output, in this case, an array of JSX elements included in the surrounding structure and eventually rendered. This method is how the final table is created. In our example, notice the key attribute in the tr element, which is used internally by React to map elements. Using this key attribute, React can decide whether a list element can be reused when the representation is changed or whether an element must be recreated. For this reason, this process can be regarded as a performance optimization.
To see the result in the browser, you must first export the list component, which is done with the export default statement in the last line. Then, you must integrate the component into the application. A suitable place is the app component in the App.js file. The customized version of this file is shown below.
import List from './List';
function App() {
return <List />;
}
export default App;
Of the generated app component, only the frame of the app function and the default export remain. You can delete the rest and import and include the List component instead. Now, switch to your browser, and you’ll see the contact list shown in this figure.
State: The Local State of a Component
The problem with the previous implementation of the List component is that it is completely static. The component obtains the data for display from a constant JavaScript array. Even if you would adjust the array now, for example, by inserting a new element into the array after 10 seconds, the display remains as in the initial display.
This limitation is due to the way React tracks changes to components and displays them: A component is redrawn when its local state (the state) changes or when it receives changed input parameters, also referred to as props. First, we’ll look at the state of the component and later at the props of a component.
The useState function allows you to define a local state for a component. This function accepts an initial value for the component’s state and returns an array. The first element of this array is an object that you can use for read access to the component’s state. The function you receive as a second array element enables you to change the state. A component is only redrawn if you manipulate the state using this function. For our contact list, this means little change at first, as shown in the listing below.
import { useState } from 'react';
const initialContacts = […];
function List() {
const [contacts, setContacts] = useState(initialContacts);
return (
<table>…</table>
);
}
export default List;
You should rename the existing array to initialContacts to make it clear that this is the initial value of the local state. When using the useState function, the destructuring feature of JavaScript is usually used, where you can specify that the first array element in our case is stored in the contacts constant and the second element in the setContacts constant.
If you now use the setContacts function to manipulate the state, React redraws the component. Now is exactly when problems arise: If you call a setContacts within the component function, a change of state occurs; the component is redrawn, so React has to execute the component function again, which results in another setContacts, so your application is stuck in an infinite loop. The solution at this point is to use the component’s lifecycle to perform an action once.
The Lifecycle of a Component
The lifecycle of a React component consists of three stages:
- Mount: The component is initially added to the component tree.
- Update: The state of the component or the props of the component change.
- Unmount: The component is removed. At this point, you should close open resources such as data streams.
Similar to a state, the implementation of a component’s lifecycle uses React’s Hook API in the form of the useEffect function. This function accepts two parameters: a function (the effect function) and an array of dependencies. The function is executed by default on each update. If that’s not what you want, you can influence its behavior via the second parameter. If you specify an empty array, the effect function is executed only once when the component is mounted. So, this case covers mounting the component. If you specify references to variables in the dependency array, the effect function is executed only when these variables change.
To cover the third phase of the component’s lifecycle, you want to define a function
that returns the effect function. This function is executed when the component is unhooked (i.e., not displayed any further).
In general, you should avoid executing any side effects such as timeouts, intervals, or even server communication directly in a component and instead always use in an effect function. The reason for this approach is that the execution of the component function of React can be canceled and either continued later or restarted.
For our List component, we want to define an effect function with an empty dependency array and communicate with the REST API to load the data. Once the server has responded, you can write the information to the state. This listing shows the integration of the Effect hook.
import { useState, useEffect } from 'react';
function List() {
const [contacts, setContacts] = useState([]);
useEffect(() => {
fetch('http://localhost:8001/api/contacts')
.then((response) => response.json())
.then((data) => setContacts(data));
}, []);
return (
<table>…</table>
);
}
export default List;
Due to the empty array passed to the useState function when it is called, React presents an empty list when the component is initially rendered. Then, the effect function gets executed. The empty dependency array ensures that the data is loaded from the server only once when the component is mounted. Within the effect function, you can use the Fetch API to send a GET request to the REST API to read the data.
In the first step, you handle the asynchronous feedback from the server using a promise and extract the JavaScript Object Notation (JSON) data from it. This asynchronous operation finally returns the actual data that you write to the local state of the component using the set- Contacts function. This function call ensures that the component is redrawn with the server’s data.
Learn more about the lifecycle of React components in this post.
Editor’s note: This post has been adapted from a section of the book Full Stack Web Development: The Comprehensive Guide by Philip Ackermann.
Comments