React Component Communication

Overview

In React applications, data passing and communication between components is key to building complex interfaces. This chapter will learn various component communication methods, including parent-child communication, sibling component communication, cross-level communication, and best practices.

📤 Parent-Child Component Communication

Parent to Child Data Passing

// Child component: User card
function UserCard({ user, onEdit, onDelete, showActions = true }) {
  return (
    <div style={{
      border: '1px solid #ddd',
      borderRadius: '8px',
      padding: '16px',
      margin: '8px',
      backgroundColor: '#f9f9f9'
    }}>
      <div style={{ display: 'flex', alignItems: 'center', marginBottom: '12px' }}>
        <img 
          src={user.avatar || '/default-avatar.png'} 
          alt={user.name}
          style={{ width: '50px', height: '50px', borderRadius: '50%', marginRight: '12px' }}
        />
        <div>
          <h3 style={{ margin: 0 }}>{user.name}</h3>
          <p style={{ margin: 0, color: '#666' }}>{user.email}</p>
        </div>
      </div>
      
      <div style={{ marginBottom: '12px' }}>
        <p><strong>Position:</strong> {user.position}</p>
        <p><strong>Department:</strong> {user.department}</p>
        <p><strong>Status:</strong> 
          <span style={{ 
            color: user.isActive ? 'green' : 'red',
            fontWeight: 'bold'
          }}>
            {user.isActive ? 'Online' : 'Offline'}
          </span>
        </p>
      </div>
      
      {showActions && (
        <div style={{ display: 'flex', gap: '8px' }}>
          <button 
            onClick={() => onEdit(user.id)}
            style={{ backgroundColor: '#007bff', color: 'white', border: 'none', padding: '8px 16px', borderRadius: '4px' }}
          >
            Edit
          </button>
          <button 
            onClick={() => onDelete(user.id)}
            style={{ backgroundColor: '#dc3545', color: 'white', border: 'none', padding: '8px 16px', borderRadius: '4px' }}
          >
            Delete
          </button>
        </div>
      )}
    </div>
  );
}

// Parent component: User list
function UserList() {
  const [users, setUsers] = React.useState([
    {
      id: 1,
      name: 'Zhang San',
      email: 'zhangsan@example.com',
      position: 'Frontend Developer',
      department: 'Engineering',
      isActive: true,
      avatar: '/avatars/zhangsan.jpg'
    },
    {
      id: 2,
      name: 'Li Si',
      email: 'lisi@example.com',
      position: 'Backend Developer',
      department: 'Engineering',
      isActive: false,
      avatar: '/avatars/lisi.jpg'
    }
  ]);
  
  const [filter, setFilter] = React.useState('all');
  
  const handleEdit = (userId) => {
    console.log('Edit user:', userId);
    // Implement edit logic
  };
  
  const handleDelete = (userId) => {
    if (window.confirm('Are you sure you want to delete this user?')) {
      setUsers(users.filter(user => user.id !== userId));
    }
  };
  
  const filteredUsers = users.filter(user => {
    if (filter === 'active') return user.isActive;
    if (filter === 'inactive') return !user.isActive;
    return true;
  });
  
  return (
    <div>
      <h2>User Management</h2>
      
      {/* Filter */}
      <div style={{ marginBottom: '20px' }}>
        <label>Filter: </label>
        <select value={filter} onChange={(e) => setFilter(e.target.value)}>
          <option value="all">All</option>
          <option value="active">Online</option>
          <option value="inactive">Offline</option>
        </select>
      </div>
      
      {/* User list */}
      <div>
        {filteredUsers.map(user => (
          <UserCard
            key={user.id}
            user={user}
            onEdit={handleEdit}
            onDelete={handleDelete}
            showActions={true}
          />
        ))}
      </div>
      
      {filteredUsers.length === 0 && (
        <div style={{ textAlign: 'center', padding: '40px', color: '#666' }}>
          No users found matching criteria
        </div>
      )}
    </div>
  );
}

Child to Parent Data Passing

// Child component: Form
function UserForm({ initialUser = null, onSubmit, onCancel }) {
  const [formData, setFormData] = React.useState({
    name: initialUser?.name || '',
    email: initialUser?.email || '',
    position: initialUser?.position || '',
    department: initialUser?.department || ''
  });
  
  const [errors, setErrors] = React.useState({});
  
  const validateForm = () => {
    const newErrors = {};
    
    if (!formData.name.trim()) {
      newErrors.name = 'Name cannot be empty';
    }
    
    if (!formData.email.trim()) {
      newErrors.email = 'Email cannot be empty';
    } else if (!/\S+@\S+\.\S+/.test(formData.email)) {
      newErrors.email = 'Invalid email format';
    }
    
    if (!formData.position.trim()) {
      newErrors.position = 'Position cannot be empty';
    }
    
    setErrors(newErrors);
    return Object.keys(newErrors).length === 0;
  };
  
  const handleChange = (e) => {
    const { name, value } = e.target;
    setFormData(prev => ({ ...prev, [name]: value }));
    
    // Clear corresponding field error
    if (errors[name]) {
      setErrors(prev => ({ ...prev, [name]: '' }));
    }
  };
  
  const handleSubmit = (e) => {
    e.preventDefault();
    
    if (validateForm()) {
      // Pass data to parent component
      onSubmit({
        ...formData,
        id: initialUser?.id || Date.now()
      });
    }
  };
  
  return (
    <form onSubmit={handleSubmit} style={{ maxWidth: '400px', margin: '20px 0' }}>
      <h3>{initialUser ? 'Edit User' : 'Add User'}</h3>
      
      <div style={{ marginBottom: '15px' }}>
        <label>Name:</label>
        <input
          type="text"
          name="name"
          value={formData.name}
          onChange={handleChange}
          style={{ 
            width: '100%', 
            padding: '8px', 
            borderColor: errors.name ? 'red' : '#ccc'
          }}
        />
        {errors.name && <div style={{ color: 'red', fontSize: '12px' }}>{errors.name}</div>}
      </div>
      
      <div style={{ marginBottom: '15px' }}>
        <label>Email:</label>
        <input
          type="email"
          name="email"
          value={formData.email}
          onChange={handleChange}
          style={{ 
            width: '100%', 
            padding: '8px',
            borderColor: errors.email ? 'red' : '#ccc'
          }}
        />
        {errors.email && <div style={{ color: 'red', fontSize: '12px' }}>{errors.email}</div>}
      </div>
      
      <div style={{ marginBottom: '15px' }}>
        <label>Position:</label>
        <input
          type="text"
          name="position"
          value={formData.position}
          onChange={handleChange}
          style={{ 
            width: '100%', 
            padding: '8px',
            borderColor: errors.position ? 'red' : '#ccc'
          }}
        />
        {errors.position && <div style={{ color: 'red', fontSize: '12px' }}>{errors.position}</div>}
      </div>
      
      <div style={{ marginBottom: '15px' }}>
        <label>Department:</label>
        <select
          name="department"
          value={formData.department}
          onChange={handleChange}
          style={{ width: '100%', padding: '8px' }}
        >
          <option value="">Select Department</option>
          <option value="Engineering">Engineering</option>
          <option value="Product">Product</option>
          <option value="Design">Design</option>
          <option value="Marketing">Marketing</option>
        </select>
      </div>
      
      <div style={{ display: 'flex', gap: '10px' }}>
        <button type="submit" style={{ flex: 1, padding: '10px', backgroundColor: '#007bff', color: 'white', border: 'none' }}>
          {initialUser ? 'Update' : 'Add'}
        </button>
        <button type="button" onClick={onCancel} style={{ flex: 1, padding: '10px', backgroundColor: '#6c757d', color: 'white', border: 'none' }}>
          Cancel
        </button>
      </div>
    </form>
  );
}

// Parent component: User management
function UserManager() {
  const [users, setUsers] = React.useState([]);
  const [showForm, setShowForm] = React.useState(false);
  const [editingUser, setEditingUser] = React.useState(null);
  
  const handleAddUser = (userData) => {
    setUsers(prev => [...prev, { ...userData, isActive: true }]);
    setShowForm(false);
  };
  
  const handleEditUser = (userData) => {
    setUsers(prev => prev.map(user => 
      user.id === userData.id ? { ...user, ...userData } : user
    ));
    setEditingUser(null);
    setShowForm(false);
  };
  
  const handleDeleteUser = (userId) => {
    setUsers(prev => prev.filter(user => user.id !== userId));
  };
  
  const startEdit = (userId) => {
    const user = users.find(u => u.id === userId);
    setEditingUser(user);
    setShowForm(true);
  };
  
  const cancelForm = () => {
    setShowForm(false);
    setEditingUser(null);
  };
  
  return (
    <div>
      <h2>User Management System</h2>
      
      <button 
        onClick={() => setShowForm(true)}
        disabled={showForm}
        style={{ 
          padding: '10px 20px', 
          backgroundColor: '#28a745', 
          color: 'white', 
          border: 'none', 
          borderRadius: '4px',
          marginBottom: '20px'
        }}
      >
        Add User
      </button>
      
      {showForm && (
        <UserForm
          initialUser={editingUser}
          onSubmit={editingUser ? handleEditUser : handleAddUser}
          onCancel={cancelForm}
        />
      )}
      
      <div>
        {users.map(user => (
          <UserCard
            key={user.id}
            user={user}
            onEdit={startEdit}
            onDelete={handleDeleteUser}
          />
        ))}
      </div>
    </div>
  );
}

🔄 Sibling Component Communication

Communicating Through Common Parent

// Product component
function ProductList({ products, onSelectProduct }) {
  return (
    <div style={{ width: '300px', border: '1px solid #ddd', padding: '20px' }}>
      <h3>Product List</h3>
      {products.map(product => (
        <div 
          key={product.id}
          onClick={() => onSelectProduct(product)}
          style={{
            padding: '10px',
            margin: '5px 0',
            border: '1px solid #ccc',
            borderRadius: '4px',
            cursor: 'pointer',
            backgroundColor: '#f8f9fa'
          }}
        >
          <div style={{ fontWeight: 'bold' }}>{product.name}</div>
          <div style={{ color: '#666' }}>${product.price}</div>
        </div>
      ))}
    </div>
  );
}

// Product detail component
function ProductDetail({ product }) {
  if (!product) {
    return (
      <div style={{ width: '400px', border: '1px solid #ddd', padding: '20px' }}>
        <h3>Product Detail</h3>
        <p>Please select a product to view details</p>
      </div>
    );
  }
  
  return (
    <div style={{ width: '400px', border: '1px solid #ddd', padding: '20px' }}>
      <h3>Product Detail</h3>
      <img 
        src={product.image || '/placeholder.jpg'} 
        alt={product.name}
        style={{ width: '100%', height: '200px', objectFit: 'cover', marginBottom: '15px' }}
      />
      <h4>{product.name}</h4>
      <p style={{ fontSize: '24px', color: '#007bff', fontWeight: 'bold' }}>
        ${product.price}
      </p>
      <p><strong>Category:</strong> {product.category}</p>
      <p><strong>Description:</strong> {product.description}</p>
      <p><strong>Stock:</strong> {product.stock} units</p>
      
      <button 
        style={{ 
          padding: '10px 20px', 
          backgroundColor: '#007bff', 
          color: 'white', 
          border: 'none', 
          borderRadius: '4px',
          width: '100%'
        }}
        disabled={product.stock === 0}
      >
        {product.stock > 0 ? 'Add to Cart' : 'Out of Stock'}
      </button>
    </div>
  );
}

// Parent component: Manage sibling component communication
function ProductCatalog() {
  const [products] = React.useState([
    {
      id: 1,
      name: 'iPhone 15',
      price: 5999,
      category: 'Smartphones',
      description: 'Latest iPhone with A17 chip',
      stock: 10,
      image: '/products/iphone15.jpg'
    },
    {
      id: 2,
      name: 'MacBook Pro',
      price: 12999,
      category: 'Laptops',
      description: 'Powerful professional laptop',
      stock: 5,
      image: '/products/macbook.jpg'
    },
    {
      id: 3,
      name: 'AirPods Pro',
      price: 1899,
      category: 'Headphones',
      description: 'Active noise cancelling wireless earbuds',
      stock: 0,
      image: '/products/airpods.jpg'
    }
  ]);
  
  const [selectedProduct, setSelectedProduct] = React.useState(null);
  
  return (
    <div>
      <h2>Product Catalog</h2>
      <div style={{ display: 'flex', gap: '20px' }}>
        <ProductList 
          products={products} 
          onSelectProduct={setSelectedProduct}
        />
        <ProductDetail product={selectedProduct} />
      </div>
    </div>
  );
}

🌐 Context Cross-Level Communication

Creating and Using Context

// Create theme context
const ThemeContext = React.createContext();

// Create user context
const UserContext = React.createContext();

// Theme provider component
function ThemeProvider({ children }) {
  const [theme, setTheme] = React.useState('light');
  
  const toggleTheme = () => {
    setTheme(prevTheme => prevTheme === 'light' ? 'dark' : 'light');
  };
  
  const themeStyles = {
    light: {
      backgroundColor: '#ffffff',
      color: '#000000',
      borderColor: '#dddddd'
    },
    dark: {
      backgroundColor: '#2d3748',
      color: '#ffffff',
      borderColor: '#4a5568'
    }
  };
  
  return (
    <ThemeContext.Provider value={{
      theme,
      toggleTheme,
      styles: themeStyles[theme]
    }}>
      {children}
    </ThemeContext.Provider>
  );
}

// User provider component
function UserProvider({ children }) {
  const [user, setUser] = React.useState(null);
  const [isLoading, setIsLoading] = React.useState(false);
  
  const login = async (credentials) => {
    setIsLoading(true);
    try {
      // Simulate login API
      await new Promise(resolve => setTimeout(resolve, 1000));
      setUser({
        id: 1,
        name: credentials.username,
        email: `${credentials.username}@example.com`,
        role: 'user'
      });
    } catch (error) {
      console.error('Login failed:', error);
    } finally {
      setIsLoading(false);
    }
  };
  
  const logout = () => {
    setUser(null);
  };
  
  return (
    <UserContext.Provider value={{
      user,
      isLoading,
      login,
      logout,
      isAuthenticated: !!user
    }}>
      {children}
    </UserContext.Provider>
  );
}

// Custom hooks
function useTheme() {
  const context = React.useContext(ThemeContext);
  if (!context) {
    throw new Error('useTheme must be used within a ThemeProvider');
  }
  return context;
}

function useUser() {
  const context = React.useContext(UserContext);
  if (!context) {
    throw new Error('useUser must be used within a UserProvider');
  }
  return context;
}

// Header component
function Header() {
  const { theme, toggleTheme, styles } = useTheme();
  const { user, logout, isAuthenticated } = useUser();
  
  return (
    <header style={{
      ...styles,
      padding: '20px',
      borderBottom: `1px solid ${styles.borderColor}`,
      display: 'flex',
      justifyContent: 'space-between',
      alignItems: 'center'
    }}>
      <h1>My App</h1>
      
      <div style={{ display: 'flex', alignItems: 'center', gap: '15px' }}>
        <button onClick={toggleTheme} style={{
          backgroundColor: styles.backgroundColor,
          color: styles.color,
          border: `1px solid ${styles.borderColor}`,
          padding: '8px 16px',
          borderRadius: '4px'
        }}>
          {theme === 'light' ? '🌙' : '☀️'} {theme === 'light' ? 'Dark' : 'Light'}
        </button>
        
        {isAuthenticated ? (
          <div style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
            <span>Welcome, {user.name}</span>
            <button onClick={logout} style={{
              backgroundColor: '#dc3545',
              color: 'white',
              border: 'none',
              padding: '8px 16px',
              borderRadius: '4px'
            }}>
              Logout
            </button>
          </div>
        ) : (
          <LoginForm />
        )}
      </div>
    </header>
  );
}

// Login form component
function LoginForm() {
  const [credentials, setCredentials] = React.useState({
    username: '',
    password: ''
  });
  const { login, isLoading } = useUser();
  const { styles } = useTheme();
  
  const handleSubmit = (e) => {
    e.preventDefault();
    login(credentials);
  };
  
  return (
    <form onSubmit={handleSubmit} style={{ display: 'flex', gap: '10px' }}>
      <input
        type="text"
        placeholder="Username"
        value={credentials.username}
        onChange={(e) => setCredentials({ ...credentials, username: e.target.value })}
        style={{
          padding: '8px',
          backgroundColor: styles.backgroundColor,
          color: styles.color,
          border: `1px solid ${styles.borderColor}`,
          borderRadius: '4px'
        }}
      />
      <input
        type="password"
        placeholder="Password"
        value={credentials.password}
        onChange={(e) => setCredentials({ ...credentials, password: e.target.value })}
        style={{
          padding: '8px',
          backgroundColor: styles.backgroundColor,
          color: styles.color,
          border: `1px solid ${styles.borderColor}`,
          borderRadius: '4px'
        }}
      />
      <button 
        type="submit" 
        disabled={isLoading}
        style={{
          backgroundColor: '#007bff',
          color: 'white',
          border: 'none',
          padding: '8px 16px',
          borderRadius: '4px'
        }}
      >
        {isLoading ? 'Logging in...' : 'Login'}
      </button>
    </form>
  );
}

// Main content component
function MainContent() {
  const { styles } = useTheme();
  const { isAuthenticated, user } = useUser();
  
  return (
    <main style={{
      ...styles,
      padding: '20px',
      minHeight: '400px'
    }}>
      {isAuthenticated ? (
        <div>
          <h2>Welcome back, {user.name}!</h2>
          <p>This is protected content area.</p>
          <UserProfile />
        </div>
      ) : (
        <div>
          <h2>Please login first</h2>
          <p>Login to view more content.</p>
        </div>
      )}
    </main>
  );
}

// User profile component
function UserProfile() {
  const { user } = useUser();
  const { styles } = useTheme();
  
  return (
    <div style={{
      ...styles,
      border: `1px solid ${styles.borderColor}`,
      borderRadius: '8px',
      padding: '20px',
      marginTop: '20px'
    }}>
      <h3>User Profile</h3>
      <p><strong>Name:</strong> {user.name}</p>
      <p><strong>Email:</strong> {user.email}</p>
      <p><strong>Role:</strong> {user.role}</p>
    </div>
  );
}

// App root component
function ContextApp() {
  return (
    <ThemeProvider>
      <UserProvider>
        <div>
          <Header />
          <MainContent />
        </div>
      </UserProvider>
    </ThemeProvider>
  );
}

📝 Chapter Summary

Through this chapter, you should have mastered:

Communication Methods

  • ✅ Parent-child component communication: Props and callbacks
  • ✅ Sibling component communication: Through common parent
  • ✅ Cross-level communication: Context API
  • ✅ Complex state management: State lifting pattern

Best Practices

  1. Unidirectional Data Flow: Keep data flow clear
  2. State Lifting: Lift shared state to the nearest common parent
  3. Context Usage: Suitable for cross-level global state
  4. Component Decoupling: Communicate through interfaces rather than implementation
  5. Performance Optimization: Use memo and useCallback to optimize rendering

Continue Learning: Next Chapter - React Conditional Rendering