Codecademy Logo

Building React Apps with Data and Forms Using AI

Related learning

  • Learn front-end development with AI tools. This course teaches you to build React applications using AI coding agents to speed up workflows.
    • Includes 2 Courses
    • With Certificate
    • Intermediate.
      6 hours

Side Effects

The primary purpose of a React component is to return some JSX to be rendered. Often, it is helpful for a component to execute some code that performs side effects in addition to rendering JSX.

In function components, we manage side effects with the Effect Hook. Some common side effects include: fetching data from a server, subscribing to a data stream, logging values to the console, interval timers, and directly interacting with the DOM.

The Effect Hook

The useEffect hook runs side effects after a render, with the frequency controlled by its dependency array. It takes two arguments in the form useEffect(callback, dependencies). The callback contains the side‑effect logic and runs after the initial render and again whenever one of the dependencies changes (or after every render if you omit the dependency array).

import React, { useState, useEffect } from 'react';
function TitleCount() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `You clicked ${count} times`;
}, [count]);
return <button onClick={() => setCount(prev => prev + 1)}>+</button>;
}

Effect Dependency Array

The dependency array is used to tell the useEffect() method when to call the effect.

  • By default, with no dependency array provided, the effect is called after every render.
  • An empty dependency array signals that the effect never needs to be re-run.
  • A non-empty dependency array signals that the hook runs the effect only when any of the dependency array values changes.
useEffect(() => {
alert('called after every render');
});
useEffect(() => {
alert('called after first render');
}, []);
useEffect(() => {
alert('called when value of `endpoint` or `id` changes');
}, [endpoint, id]);

Controlled vs. Uncontrolled Form Fields

In React, form fields are considered either uncontrolled, meaning they maintain their own state, or controlled, meaning that some parent maintains their state and passes it to them to display. Usually, the form fields will be controlled.

The example code shows an uncontrolled and controlled input.

const uncontrolledInput = <input />;
const controlledInput = (
<input value={stateValue} onChange={handleInputChange} />
);

React Form Validation

Form validation in React can trigger at several points: while typing (onChange), upon exiting a field (onBlur), or before submitting (onSubmit). Each option balances real-time feedback against user experience.

Immediate validation, like onChange, helps prevent errors while typing, but might overwhelm users with feedback. onBlur is suitable for a polished UI, and onSubmit ensures thorough checks at the final stage.

import React, { useState } from 'react';
function LoginForm() {
const [email, setEmail] = useState('');
const [error, setError] = useState('');
const handleSubmit = (e) => {
e.preventDefault();
if (!email.includes('@')) {
setError('Invalid email');
return;
}
console.log('Submitted:', email);
};
return (
<form onSubmit={handleSubmit}>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
onBlur={() => email && !email.includes('@') && setError('Invalid email')}
/>
{error && <span>{error}</span>}
<button type="submit">Submit</button>
</form>
);
}

Effect Cleanup Functions

The cleanup function is optionally returned by the first argument of the Effect Hook.

If the effect does anything that needs to be cleaned up to prevent memory leaks, then the effect returns a cleanup function. The cleanup function is called before re-running the effect (when dependencies change) and when the component unmounts.

useEffect(() => {
document.addEventListener('keydown', handleKeydown);
//Clean up the effect:
return () => document.removeEventListener('keydown', handleKeydown);
});

The useEffectEvent Hook

The useEffectEvent hook provides a way to efficiently manage dependencies by distinguishing between reactive and non-reactive values. It enables access to the latest values during side effects without causing unnecessary re-execution. By separating non-reactive logic, it avoids stale closures and reduces the need to list every changing value in an effect’s dependency array, which helps keep effects focused and easier to reason about.

This hook is handy when working with timers, subscriptions, and other external events that must always access up-to-date state without forcing unnecessary re-runs of the surrounding effect.

In this example, the chat connection logs incoming messages alongside the current draft text. Without useEffectEvent, including messagein the dependency array would cause the connection to reset with every keystroke. The hook allows the effect to access the latest draft value while keeping the dependency array focused onroomId`, the only value that should trigger reconnection.

import { useState, useEffect, useEffectEvent } from 'react';
function ChatRoom({ roomId }) {
const [message, setMessage] = useState('');
// Effect Event: can read current 'message' without being reactive to it
const onMessage = useEffectEvent((msg) => {
console.log(`Received: ${msg}, Current draft: ${message}`);
});
useEffect(() => {
const connection = createConnection(roomId);
connection.on('message', onMessage);
connection.connect();
return () => connection.disconnect();
}, [roomId]); // Only reconnects when room changes, not on every keystroke
return (
<input
value={message}
onChange={(e) => setMessage(e.target.value)}
placeholder="Type a message..."
/>
);
}

Controlled Components

A controlled form element in React is built with a change handler function and a value attribute.

const controlledInput = (
<input value={stateValue} onChange={handleInputChange} />
);

Coordinating State Management

React applications commonly juggle multiple interrelated state variables, demanding meticulous planning to ensure seamless updates are maintained across the app. Developers should consider scenarios like form handling, where a change to one field might necessitate updates elsewhere. This kind of interaction emphasizes the importance of a comprehensive understanding of state dependencies.

import React, { useState, useEffect } from 'react';
function RegistrationForm() {
const [username, setUsername] = useState('');
const [email, setEmail] = useState('');
const [isFormValid, setIsFormValid] = useState(false);
// States interact: username and email changes affect form validity
useEffect(() => {
const isValid = username.length >= 3 && email.includes('@');
setIsFormValid(isValid);
}, [username, email]);
return (
<div>
<input
type="text"
placeholder="Username (min 3 chars)"
value={username}
onChange={(e) => setUsername(e.target.value)}
/>
<input
type="email"
placeholder="Email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<button disabled={!isFormValid}>Submit</button>
{isFormValid && <p>Form is ready to submit!</p>}
</div>
);
}

React Derived State

Values based on the existing state or props should be calculated during render rather than stored separately. This strategy minimizes components’ complexity, enhances performance, and reduces the risk of inconsistencies. An example of this is dynamic UI adjustments, where a display might depend on computed values from current props or state.

function PriceDisplay({ basePrice, discount }) {
const finalPrice = basePrice - discount;
return <p>Final Price: {finalPrice}</p>;
}

Learn more on Codecademy

  • Learn front-end development with AI tools. This course teaches you to build React applications using AI coding agents to speed up workflows.
    • Includes 2 Courses
    • With Certificate
    • Intermediate.
      6 hours