We need to complete a product list page with the following features:
All changes will be made in src/components/ProductView/ProductView.jsx.
useStateFirst, we need to convert the sideOpen constant to a state variable so we can toggle it.
Add useState to your React imports:
import React, { useState } from 'react';
Replace the constant with a state variable:
// Before
const sideOpen = true;
// After
const [sideOpen, setSideOpen] = useState(true);
Update the click handler to toggle the side panel:
// Before
onClick={() => console.log('TOGGLE SIDE PANEL')}
// After
onClick={() => setSideOpen(!sideOpen)}
The {sideOpen ? '>' : '<'} expression in the JSX already uses the sideOpen state to show the appropriate toggle icon based on the panel state.
import React, { useState } from 'react';
import ProductListItem from '../ProductListItem';
import ProductDetails from '../ProductDetails';
import './ProductView.css';
function ProductView({ products }) {
const [sideOpen, setSideOpen] = useState(true);
return (
<div className="product-view">
<div className="product-main-area">
<h1>Products</h1>
<div className="product-list">
{products.map(item =>
<ProductListItem
key={item.id}
product={item}
onClick={() => console.log('SELECT PRODUCT', item)}
/>
)}
</div>
</div>
<div className="product-side-panel">
<div className="product-side-panel-toggle-wrapper">
<div className="product-side-panel-toggle"
onClick={() => setSideOpen(!sideOpen)}>
{sideOpen ? '>' : '<'}
</div>
</div>
<ProductDetails visible={sideOpen} />
</div>
</div>
);
}
export default ProductView;
Test the application by clicking the toggle. The side panel should now open and close.
Now we need to add state to track the selected product and connect it to the details panel.
const [selectedProduct, setSelectedProduct] = useState();
Modify the click handler to set the selected product:
// Before
onClick={() => console.log('SELECT PRODUCT', item)}
// After
onClick={() => setSelectedProduct(item)}
Update the ProductDetails component to receive the selected product:
// Before
<ProductDetails visible={sideOpen} />
// After
<ProductDetails product={selectedProduct} visible={sideOpen} />
Add the isSelected prop to ProductListItem:
// Before
<ProductListItem
key={item.id}
product={item}
onClick={() => setSelectedProduct(item)}
/>
// After
<ProductListItem
key={item.id}
product={item}
isSelected={selectedProduct && selectedProduct.id === item.id}
onClick={() => setSelectedProduct(item)}
/>
import React, { useState } from 'react';
import ProductListItem from '../ProductListItem';
import ProductDetails from '../ProductDetails';
import './ProductView.css';
function ProductView({ products }) {
const [sideOpen, setSideOpen] = useState(true);
const [selectedProduct, setSelectedProduct] = useState();
return (
<div className="product-view">
<div className="product-main-area">
<h1>Products</h1>
<div className="product-list">
{products.map(item =>
<ProductListItem
key={item.id}
product={item}
isSelected={selectedProduct && selectedProduct.id === item.id}
onClick={() => setSelectedProduct(item)}
/>
)}
</div>
</div>
<div className="product-side-panel">
<div className="product-side-panel-toggle-wrapper">
<div className="product-side-panel-toggle"
onClick={() => setSideOpen(!sideOpen)}>
{sideOpen ? '>' : '<'}
</div>
</div>
<ProductDetails product={selectedProduct} visible={sideOpen} />
</div>
</div>
);
}
export default ProductView;
Now when you click a product, its details should appear in the side panel, and the product item should be highlighted in the list.
Let's add some effects to improve the user experience using the useEffect hook.
import React, { useState, useEffect } from 'react';
// Auto-open side panel when product is selected
useEffect(() => {
if (selectedProduct) {
setSideOpen(true);
}
}, [selectedProduct]);
// Clear selection when side panel is closed
useEffect(() => {
if (!sideOpen) {
setSelectedProduct();
}
}, [sideOpen]);
import React, { useState, useEffect } from 'react';
import ProductListItem from '../ProductListItem';
import ProductDetails from '../ProductDetails';
import './ProductView.css';
function ProductView({ products }) {
const [sideOpen, setSideOpen] = useState(true);
const [selectedProduct, setSelectedProduct] = useState();
// Auto-open side panel when product is selected
useEffect(() => {
if (selectedProduct) {
setSideOpen(true);
}
}, [selectedProduct]);
// Clear selection when side panel is closed
useEffect(() => {
if (!sideOpen) {
setSelectedProduct();
}
}, [sideOpen]);
return (
<div className="product-view">
<div className="product-main-area">
<h1>Products</h1>
<div className="product-list">
{products.map(item =>
<ProductListItem
key={item.id}
product={item}
isSelected={selectedProduct && selectedProduct.id === item.id}
onClick={() => setSelectedProduct(item)}
/>
)}
</div>
</div>
<div className="product-side-panel">
<div className="product-side-panel-toggle-wrapper">
<div className="product-side-panel-toggle"
onClick={() => setSideOpen(!sideOpen)}>
{sideOpen ? '>' : '<'}
</div>
</div>
<ProductDetails product={selectedProduct} visible={sideOpen} />
</div>
</div>
);
}
export default ProductView;
Now the side panel should automatically open when a product is selected, and the selection should clear when the panel is closed.
Let's add some debugging to understand when components re-render.
// Add debugging to auto-open effect
useEffect(() => {
console.log('selectedProduct CHANGED TO', selectedProduct);
if (selectedProduct) {
setSideOpen(true);
}
}, [selectedProduct]);
// Add debugging to clear selection effect
useEffect(() => {
console.log('sideOpen CHANGED TO', sideOpen);
if (!sideOpen) {
setSelectedProduct();
}
}, [sideOpen]);
// Add before return statement
console.log('Rendering ProductView');
Open the browser console and observe the output as you interact with the application. You should see:
import React, { useState, useEffect } from 'react';
import ProductListItem from '../ProductListItem';
import ProductDetails from '../ProductDetails';
import './ProductView.css';
function ProductView({ products }) {
const [sideOpen, setSideOpen] = useState(true);
const [selectedProduct, setSelectedProduct] = useState();
// Auto-open side panel when product is selected
useEffect(() => {
console.log('selectedProduct CHANGED TO', selectedProduct);
if (selectedProduct) {
setSideOpen(true);
}
}, [selectedProduct]);
// Clear selection when side panel is closed
useEffect(() => {
console.log('sideOpen CHANGED TO', sideOpen);
if (!sideOpen) {
setSelectedProduct();
}
}, [sideOpen]);
console.log('Rendering ProductView');
return (
<div className="product-view">
<div className="product-main-area">
<h1>Products</h1>
<div className="product-list">
{products.map(item =>
<ProductListItem
key={item.id}
product={item}
isSelected={selectedProduct && selectedProduct.id === item.id}
onClick={() => setSelectedProduct(item)}
/>
)}
</div>
</div>
<div className="product-side-panel">
<div className="product-side-panel-toggle-wrapper">
<div className="product-side-panel-toggle"
onClick={() => setSideOpen(!sideOpen)}>
{sideOpen ? '>' : '<'}
</div>
</div>
<ProductDetails product={selectedProduct} visible={sideOpen} />
</div>
</div>
);
}
export default ProductView;
Let's enhance our app to remember the side panel state between page reloads using local storage.
// Initialize sideOpen from localStorage, defaulting to true if not set
const [sideOpen, setSideOpen] = useState(
localStorage.getItem('sidePanelOpen') !== "false"
);
Modify the sideOpen effect to save the state to localStorage:
// Update localStorage when sideOpen changes
useEffect(() => {
console.log('sideOpen CHANGED TO', sideOpen);
localStorage.setItem('sidePanelOpen', sideOpen);
if (!sideOpen) {
setSelectedProduct();
}
}, [sideOpen]);
import React, { useState, useEffect } from 'react';
import ProductListItem from '../ProductListItem';
import ProductDetails from '../ProductDetails';
import './ProductView.css';
function ProductView({ products }) {
const [selectedProduct, setSelectedProduct] = useState();
// Initialize from localStorage, defaulting to true if not set
const [sideOpen, setSideOpen] = useState(
localStorage.getItem('sidePanelOpen') !== "false"
);
// Auto-open side panel when product is selected
useEffect(() => {
console.log('selectedProduct CHANGED TO', selectedProduct);
if (selectedProduct) {
setSideOpen(true);
}
}, [selectedProduct]);
// Update localStorage and clear selection when side panel is closed
useEffect(() => {
console.log('sideOpen CHANGED TO', sideOpen);
localStorage.setItem('sidePanelOpen', sideOpen);
if (!sideOpen) {
setSelectedProduct();
}
}, [sideOpen]);
console.log('Rendering ProductView');
return (
<div className="product-view">
<div className="product-main-area">
<h1>Products</h1>
<div className="product-list">
{products.map(item =>
<ProductListItem
key={item.id}
product={item}
isSelected={selectedProduct && selectedProduct.id === item.id}
onClick={() => setSelectedProduct(item)}
/>
)}
</div>
</div>
<div className="product-side-panel">
<div className="product-side-panel-toggle-wrapper">
<div className="product-side-panel-toggle"
onClick={() => setSideOpen(!sideOpen)}>
{sideOpen ? '>' : '<'}
</div>
</div>
<ProductDetails product={selectedProduct} visible={sideOpen} />
</div>
</div>
);
}
export default ProductView;
Now your application will remember the side panel state between page reloads!
The useState hook allows us to add state to functional components:
const [state, setState] = useState(initialValue);setState(newValue) to update the stateReal-world analogy: Think of useState like a light switch. The state is the current position (on/off), and the setter function is the action of flipping the switch.
The useEffect hook lets us perform side effects in functional components:
useEffect(() => { /* effect code */ }, [dependencies]);Real-world analogy: Think of useEffect like setting up an automatic response, like "whenever the temperature (dependency) changes, adjust the thermostat (effect)".
To reinforce your understanding of React hooks, try these enhancements: