React Hooks Tutorial: Products Page

Understanding the Problem

We need to complete a product list page with the following features:

All changes will be made in src/components/ProductView/ProductView.jsx.

Devising a Plan

  1. Implement the side panel toggle functionality using useState
  2. Add state to track the selected product
  3. Highlight the selected product in the list
  4. Connect product selection to the details panel
  5. Add effects to enhance user experience:
    • Open the panel when a product is selected
    • Clear selection when panel is closed
  6. Debug and refine our solution
  7. Bonus: Persist the side panel state in local storage

Phase 1: Toggle Side Panel

First, we need to convert the sideOpen constant to a state variable so we can toggle it.

Step 1: Import useState

Add useState to your React imports:

import React, { useState } from 'react';

Step 2: Replace Constant with State Variable

Replace the constant with a state variable:

// Before
const sideOpen = true;

// After
const [sideOpen, setSideOpen] = useState(true);

Step 3: Implement Toggle

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.

Completed Code for Phase 1

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.

Phase 2: Track Selected Item

Now we need to add state to track the selected product and connect it to the details panel.

Step 1: Add State for Selected Product

const [selectedProduct, setSelectedProduct] = useState();

Step 2: Update ProductListItem Click Handler

Modify the click handler to set the selected product:

// Before
onClick={() => console.log('SELECT PRODUCT', item)}

// After
onClick={() => setSelectedProduct(item)}

Step 3: Pass Selected Product to ProductDetails

Update the ProductDetails component to receive the selected product:

// Before
<ProductDetails visible={sideOpen} />

// After
<ProductDetails product={selectedProduct} visible={sideOpen} />

Step 4: Highlight Selected Product

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)}
/>

Completed Code for Phase 2

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.

Phase 3: Enhance the User Experience

Let's add some effects to improve the user experience using the useEffect hook.

Step 1: Import useEffect

import React, { useState, useEffect } from 'react';

Step 2: Add Effect to Open Side Panel when Product is Selected

// Auto-open side panel when product is selected
useEffect(() => {
  if (selectedProduct) {
    setSideOpen(true);
  }
}, [selectedProduct]);

Step 3: Add Effect to Clear Selection when Side Panel is Closed

// Clear selection when side panel is closed
useEffect(() => {
  if (!sideOpen) {
    setSelectedProduct();
  }
}, [sideOpen]);

Completed Code for Phase 3

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.

Phase 4: Debug Re-Rendering

Let's add some debugging to understand when components re-render.

Step 1: Add Debugging to useEffect Hooks

// 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]);

Step 2: Add Debugging before Return Statement

// Add before return statement
console.log('Rendering ProductView');

Step 3: Test and Observe

Open the browser console and observe the output as you interact with the application. You should see:

Completed Code with Debugging

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;

Bonus: Remember State Using Local Storage

Let's enhance our app to remember the side panel state between page reloads using local storage.

Step 1: Initialize State from Local Storage

// Initialize sideOpen from localStorage, defaulting to true if not set
const [sideOpen, setSideOpen] = useState(
  localStorage.getItem('sidePanelOpen') !== "false"
);

Step 2: Update Local Storage when Side Panel State Changes

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]);

Completed Bonus Code

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!

Looking Back: Understanding React Hooks

useState Hook

The useState hook allows us to add state to functional components:

Real-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.

useEffect Hook

The useEffect hook lets us perform side effects in functional components:

Real-world analogy: Think of useEffect like setting up an automatic response, like "whenever the temperature (dependency) changes, adjust the thermostat (effect)".

Common React Hook Patterns

Additional Practice Ideas

To reinforce your understanding of React hooks, try these enhancements:

  1. Add a "Clear Selection" button that resets the selected product
  2. Add filter buttons to show only certain types of products
  3. Implement a "Last Viewed" feature that remembers the last 3 products viewed
  4. Add a dark/light theme toggle using useState and localStorage
  5. Create a shopping cart using useState and useEffect