Class 16: Components, Props, and State
In React, the entire user interface is built using components. Components are independent, reusable pieces of UI. This class explores how to create components, pass data to them using props, and manage dynamic data within them using state.
React Components
At the heart of every React application are components. A component is a self-contained, reusable piece of UI. Think of them like Lego bricks – you can combine small, simple components to build larger, more complex ones.
Components allow us to:
- Break down the UI: Decompose a complex user interface into smaller, manageable, and independent pieces.
- Promote reusability: Write code once and use it multiple times throughout your application, reducing duplication and making maintenance easier.
- Encapsulate logic: Each component can have its own logic, state, and rendering instructions, making it easier to understand and debug.
Types of Components
Historically, React had two main ways to define components:
- Function Components: Simple JavaScript functions that return JSX. They were initially "stateless" but gained state and lifecycle capabilities with the introduction of Hooks.
-
Class Components: ES6 classes that extend
from
React.Component
and have arender()
method. They were traditionally used for components that needed state or lifecycle methods.
Modern React development primarily uses Function Components with Hooks due to their simplicity, readability, and better performance characteristics. We will focus on function components in this course.
Building Reusable Functional Components
In React, components are the building blocks of your UI. Functional components are JavaScript functions that return JSX. They are the most common way to write React components today, especially with the advent of Hooks.
-
Defining a simple function that returns JSX:
A functional component is essentially a JavaScript function
(often an arrow function) that takes
props
(properties) as its argument and returns JSX.// src/components/Greeting.jsx function Greeting() { return ( <div> <h2>Hello from Greeting Component!</h2> <p>This is a reusable piece of UI.</p> </div> ); } export default Greeting;
- Exporting and importing components: Components are typically defined in their own files and then exported, so they can be imported and used in other components.
-
Rendering components in
App.jsx
: Once imported, you can use your custom components in JSX like any other HTML element.// src/App.jsx import Greeting from './components/Greeting'; function App() { return ( <div> <h1>My Awesome React App</h1> <Greeting /> {/* Use the Greeting component as a custom HTML tag */} <Greeting /> {/* You can use it multiple times */} </div> ); } export default App;
Nesting Components
A core principle of React is breaking down the UI into smaller, manageable, and reusable components. This often involves nesting components within each other to create complex UI hierarchies, forming a parent-child relationship.
// src/components/Header.jsx
function Header() {
return (
<header>
<nav>
<a href="#">Home</a> | <a href="#">About</a> | <a href="#">Contact</a>
</nav>
</header>
);
}
export default Header;
// src/components/Footer.jsx
function Footer() {
return (
<footer>
<p>© {new Date().getFullYear()} My React App</p>
</footer>
);
}
export default Footer;
// src/App.jsx
import Header from './components/Header';
import Greeting from './components/Greeting';
import Footer from './components/Footer';
function App() {
return (
<div>
<Header />
<main>
<h1>Welcome to My Application!</h1>
<Greeting />
</main>
<Footer />
</div>
);
}
export default App;
Passing Data with Props
Props (short for "properties") are a way of passing data from a parent component to a child component. They are read-only, meaning a child component cannot directly modify the props it receives from its parent. Think of props as arguments to a function component.
Key characteristics of Props:
- Unidirectional Data Flow: Data flows down the component tree (from parent to child).
- Immutable: Props are read-only within the receiving component. If a component needs to change a value, it should manage it in its own state or call a function passed via props to trigger a change in the parent.
- Customizable Components: They allow you to make components dynamic and reusable by passing different data to them.
// src/components/UserCard.jsx
// UserCard component receives 'name' and 'age' as props
function UserCard(props) {
return (
<div className="user-card">
<h3>Name: {props.name || 'Guest'}</h3> {/* No name prop, will use 'Guest' */}
<p>Age: {props.age}</p>
</div>
);
}
export default UserCard;
// src/App.jsx (updated)
import UserCard from './components/UserCard';
function App() {
return (
<div>
<h1>User Profiles</h1>
{/* Pass data as attributes to the UserCard component */}
<UserCard name="Alice" age={30} />
<UserCard name="Bob" age={24} />
<UserCard name="Charlie" age={45} />
<UserCard />
</div>
);
}
export default App;
Accessing and Destructuring Props
You can access props using the props
object, but
it's common practice to use object destructuring in the function
signature for cleaner code.
// src/components/UserCard.jsx (using destructuring)
// Destructure props directly in the function parameters
function UserCard({ name, age }) {
return (
<div className="user-card">
<h3>Name: {name}</h3>
<p>Age: {age}</p>
</div>
);
}
export default UserCard;
Managing Component State
While props allow data flow from parent to child, state is the data that is managed within a component and can change over time, triggering re-renders of the component.
The useState
Hook
In function components, state is managed using the
useState
Hook, which is a function that takes one argument (the initial
state value), and returns an array with two elements:
- The current state value.
- A function that lets you update the state value.
// src/components/Counter.jsx
import { useState } from 'react'; // Import useState
function Counter() {
// Declare a state variable 'count' and a function 'setCount' to update it
// Initial value of count is 0
const [count, setCount] = useState(0);
const increment = () => {
setCount(count + 1); // Update the state
};
const decrement = () => {
setCount(count - 1); // Update the state
};
return (
<div className="counter">
<h2>Count: {count}</h2>
<button onClick={increment}>Increment</button>
<button onClick={decrement}>Decrement</button>
</div>
);
}
export default Counter;
// src/App.jsx (updated)
import Counter from './components/Counter';
function App() {
return (
<div>
<h1>Interactive Counter</h1>
<Counter />
</div>
);
}
export default App;
In this example, count
starts at 0. When you click
"Increment" or "Decrement", setCount
is called,
which updates the count
state. React then sees that
the state has changed and re-renders the
Counter
component to display the new
count
value.
Understanding State Updates
When you call the state setter function (e.g.,
setCount
from useState
), React
schedules an update to the component's state. It's important to
understand a few key aspects of how these updates work:
-
Asynchronous Nature of State Updates:
React state updates are often asynchronous. This means that
when you call
setCount(count + 1)
, thecount
variable might not be immediately updated to its new value in the very next line of your code. React batches state updates for performance, so if you need to perform an action immediately after a state update, it's often better to use auseEffect
Hook or a callback function if available. -
Updating State Based on Previous State (Functional
Updates):
Because of the asynchronous nature, if your new state
depends on the previous state, it's recommended to pass a
function to the state setter. This function receives the
previous state as an argument and returns the new state.
This guarantees you're working with the most up-to-date
state value.
// Correct way to increment count if depending on previous state const increment = () => { setCount(prevCount => prevCount + 1); };
-
Immutability in State Updates (Arrays,
Objects):
When updating state that contains objects or arrays, you
must update them immutably. This means instead of directly
modifying the existing object or array, you should create a
new object or array with the desired changes. React relies
on reference equality to detect changes, so mutating the
original object/array will not trigger a re-render.
// Updating an object immutably setUserInfo(prevInfo => ({ ...prevInfo, age: prevInfo.age + 1 })); // Updating an array immutably (adding an item) setItems(prevItems => [...prevItems, newItem]);
List Rendering
Displaying collections of data, such as lists of products,
users, or comments, is a very common task in web development. In
React, you typically render lists using the JavaScript
map()
array method, which transforms an array of data into an array of
React elements.
Key concepts for list rendering:
-
map()
Method: Iterate over an array and return a new array of JSX elements. -
key
Prop: Each item in a list must have a uniquekey
prop. React uses keys to identify which items have changed, been added, or been removed. This helps React efficiently update the UI and maintain component state.
The key
Prop Explained
-
Unique: The
key
should be a string or a number that is unique among siblings. -
Stable: The
key
should be stable across re-renders. Do not use array indexes as keys if the list items can change order, be added, or be removed, as this can lead to performance issues and bugs. Use a unique ID from your data if available (e.g.,item.id
).
Rendering a List of Items
Let's display a list of fruits:
// src/components/FruitList.jsx
function FruitList() {
const fruits = [
{ id: 1, name: 'Apple' },
{ id: 2, name: 'Banana' },
{ id: 3, name: 'Cherry' },
];
return (
<div>
<h2>My Fruit List</h2>
<ul>
{fruits.map((fruit) => (
<li key={fruit.id}>{fruit.name}</li>
))}
</ul>
</div>
);
}
export default FruitList;
In this example, we map
over the
fruits
array. For each fruit
object,
we return an <li>
element. The
key={fruit.id}
is crucial here, as
fruit.id
provides a stable and unique identifier
for each list item.
Basic Component Lifecycle (Conceptual)
While React handles most of the complexities of rendering, understanding the basic "lifecycle" of a component can be helpful. A component goes through three main phases:
- Mounting: When an instance of a component is being created and inserted into the DOM for the first time.
- Updating: When a component's props or state change, leading to a re-render of the component.
- Unmounting: When a component is being removed from the DOM.
The useEffect
Hook, with its dependency array,
allows you to "hook into" these lifecycle phases in functional
components. For example, an empty dependency array
([]
) makes useEffect
behave similarly
to componentDidMount
(runs once after mounting).
Crucially, when a component's state (or props)
changes, React automatically triggers an
update, meaning the component's render function
(and useEffect
if dependencies change) will run
again to reflect the new data. This is how React keeps your UI
in sync with your application's data.