Props and Data Flow

Understanding how data moves through React applications

Props: The Communication Channel

Props (short for "properties") are React's mechanism for passing data from parent to child components. They are the primary way components communicate with each other and form the backbone of React's unidirectional data flow pattern.

Parent Component Child Component A Child Component B Child Component C props props props

Analogy: Mail Delivery System

Think of props like a mail delivery system:

  • Parent Component: The post office that sends packages
  • Props: The packages themselves, containing information
  • Child Components: Homes that receive packages
  • Read-Only Nature: Like sealed packages, props shouldn't be modified by recipients
  • Unidirectional Flow: Mail always flows from the post office to homes, not vice versa

Just as the postal service only delivers packages downward (from central distribution to individual homes), React's data flows down the component tree through props.

Passing Props to Components

Props can be passed to components in several ways, similar to HTML attributes:

Basic Props Passing

// Parent component passing props
function App() {
  return (
    <div>
      <UserProfile 
        name="John Doe" 
        role="Developer" 
        isActive={true} 
        loginCount={42} 
      />
    </div>
  );
}

// Child component receiving props
function UserProfile(props) {
  return (
    <div className="user-profile">
      <h2>{props.name}</h2>
      <p>Role: {props.role}</p>
      <p>Status: {props.isActive ? 'Active' : 'Inactive'}</p>
      <p>Login count: {props.loginCount}</p>
    </div>
  );
}

Destructuring Props

// Destructuring in function parameter
function UserProfile({ name, role, isActive, loginCount }) {
  return (
    <div className="user-profile">
      <h2>{name}</h2>
      <p>Role: {role}</p>
      <p>Status: {isActive ? 'Active' : 'Inactive'}</p>
      <p>Login count: {loginCount}</p>
    </div>
  );
}

// Alternatively, destructure in function body
function UserProfile(props) {
  const { name, role, isActive, loginCount } = props;
  
  return (
    <div className="user-profile">
      <h2>{name}</h2>
      <p>Role: {role}</p>
      <p>Status: {isActive ? 'Active' : 'Inactive'}</p>
      <p>Login count: {loginCount}</p>
    </div>
  );
}

Props with Default Values

// Using default parameters
function UserProfile({ 
  name = 'Guest User', 
  role = 'Visitor', 
  isActive = false, 
  loginCount = 0 
}) {
  return (
    <div className="user-profile">
      <h2>{name}</h2>
      <p>Role: {role}</p>
      <p>Status: {isActive ? 'Active' : 'Inactive'}</p>
      <p>Login count: {loginCount}</p>
    </div>
  );
}

// For class components or older method
UserProfile.defaultProps = {
  name: 'Guest User',
  role: 'Visitor',
  isActive: false,
  loginCount: 0
};

Best Practices for Props

  • Keep component props focused - Pass only what's needed
  • Use descriptive prop names - Self-documenting code is easier to maintain
  • Provide default values - Makes components more robust
  • Destructure for readability - Especially with many props
  • Consider using prop objects - For related data (e.g., user={userData})

Props Can Be Various Data Types

React props can be any valid JavaScript values:

Primitive Values

<Component 
  stringProp="Hello World" 
  numberProp={42} 
  booleanProp={true} 
  nullProp={null} 
  undefinedProp={undefined} 
/>

Objects

const user = {
  id: 1,
  name: 'John Doe',
  email: 'john@example.com'
};

<UserCard user={user} />

Arrays

const items = ['Apple', 'Banana', 'Cherry'];

<ShoppingList items={items} />

Functions

function handleClick() {
  console.log('Button clicked!');
}

<Button onClick={handleClick} />

React Elements

const title = <h1>User Dashboard</h1>;

<Panel header={title}>
  Content goes here
</Panel>

Using the Spread Operator with Props

const buttonProps = {
  type: 'submit',
  className: 'primary-button',
  disabled: false,
  onClick: handleSubmit
};

// Spread all properties as individual props
<Button {...buttonProps} />

// Equivalent to:
<Button 
  type="submit" 
  className="primary-button" 
  disabled={false} 
  onClick={handleSubmit} 
/>

The Special 'children' Prop

The children prop is a special prop that contains any elements included between the opening and closing tags of a component:

function Card({ title, children }) {
  return (
    <div className="card">
      <div className="card-header">
        <h2>{title}</h2>
      </div>
      <div className="card-body">
        {children}
      </div>
    </div>
  );
}

// Usage
<Card title="Welcome">
  <p>This is the card content.</p>
  <button>Learn More</button>
</Card>

Prop Types and Type Checking

React provides a way to validate props using PropTypes, helping catch bugs and document the intended data types.

Using PropTypes

import PropTypes from 'prop-types';

function UserProfile({ name, age, isAdmin, friends }) {
  return (
    <div>
      <h2>{name}</h2>
      <p>Age: {age}</p>
      {isAdmin && <p>Admin</p>}
      <div>
        <h3>Friends</h3>
        <ul>
          {friends.map(friend => (
            <li key={friend.id}>{friend.name}</li>
          ))}
        </ul>
      </div>
    </div>
  );
}

UserProfile.propTypes = {
  name: PropTypes.string.isRequired,
  age: PropTypes.number,
  isAdmin: PropTypes.bool,
  friends: PropTypes.arrayOf(
    PropTypes.shape({
      id: PropTypes.number.isRequired,
      name: PropTypes.string.isRequired
    })
  )
};

UserProfile.defaultProps = {
  age: 21,
  isAdmin: false,
  friends: []
};

Common PropTypes Validators

MyComponent.propTypes = {
  // Basic types
  optionalString: PropTypes.string,
  optionalNumber: PropTypes.number,
  optionalBool: PropTypes.bool,
  optionalFunc: PropTypes.func,
  optionalObject: PropTypes.object,
  optionalArray: PropTypes.array,
  optionalSymbol: PropTypes.symbol,
  
  // Anything that can be rendered
  optionalNode: PropTypes.node,
  
  // A React element
  optionalElement: PropTypes.element,
  
  // An element type (e.g., MyComponent)
  optionalElementType: PropTypes.elementType,
  
  // Instance of a class
  optionalInstance: PropTypes.instanceOf(MyClass),
  
  // One of specific values
  optionalEnum: PropTypes.oneOf(['News', 'Photos']),
  
  // One of several types
  optionalUnion: PropTypes.oneOfType([
    PropTypes.string,
    PropTypes.number
  ]),
  
  // Array of specific type
  optionalArrayOf: PropTypes.arrayOf(PropTypes.number),
  
  // Object with values of specific type
  optionalObjectOf: PropTypes.objectOf(PropTypes.number),
  
  // Object with specific shape
  optionalObjectWithShape: PropTypes.shape({
    name: PropTypes.string,
    age: PropTypes.number
  }),
  
  // Required versions
  requiredString: PropTypes.string.isRequired,
  
  // Custom validator
  customProp: function(props, propName, componentName) {
    if (!/matchme/.test(props[propName])) {
      return new Error(
        'Invalid prop `' + propName + '` supplied to' +
        ' `' + componentName + '`. Validation failed.'
      );
    }
  }
};

TypeScript: A More Robust Alternative

For larger applications, TypeScript provides a more comprehensive type-checking system:

interface UserProfileProps {
  name: string;
  age?: number;
  isAdmin?: boolean;
  friends?: Array<{
    id: number;
    name: string;
  }>;
}

function UserProfile({ name, age = 21, isAdmin = false, friends = [] }: UserProfileProps) {
  return (
    <div>
      <h2>{name}</h2>
      <p>Age: {age}</p>
      {isAdmin && <p>Admin</p>}
      <div>
        <h3>Friends</h3>
        <ul>
          {friends.map(friend => (
            <li key={friend.id}>{friend.name}</li>
          ))}
        </ul>
      </div>
    </div>
  );
}

Unidirectional Data Flow

React enforces a unidirectional data flow: data passes down from parent to child component through props. This makes applications more predictable and easier to debug.

flowchart TD subgraph "App Component" A[State] end subgraph "Parent Component" B[Props from App] C[Local State] end subgraph "Child Component A" D[Props from Parent] E[Local State] end subgraph "Child Component B" F[Props from Parent] G[Local State] end A --"Props"--> B B --"Data Flow"--> D B --"Data Flow"--> F C -.-> C E -.-> E G -.-> G style A fill:#ffcccc style C fill:#ffcccc style E fill:#ffcccc style G fill:#ffcccc style B fill:#ccffcc style D fill:#ccffcc style F fill:#ccffcc

Core Principles of Unidirectional Data Flow

Example of Unidirectional Data Flow

function App() {
  // State in the parent component
  const [user, setUser] = useState({
    name: 'John Doe',
    isAdmin: false
  });
  
  // Function to update state
  const toggleAdminStatus = () => {
    setUser(prevUser => ({
      ...prevUser,
      isAdmin: !prevUser.isAdmin
    }));
  };
  
  return (
    <div className="app">
      {/* Pass data and callback function as props */}
      <UserProfile 
        user={user} 
        onToggleAdmin={toggleAdminStatus} 
      />
    </div>
  );
}

function UserProfile({ user, onToggleAdmin }) {
  return (
    <div className="user-profile">
      <h2>{user.name}</h2>
      <p>Status: {user.isAdmin ? 'Admin' : 'Regular User'}</p>
      
      {/* Call the function received from props */}
      <button onClick={onToggleAdmin}>
        {user.isAdmin ? 'Revoke Admin' : 'Make Admin'}
      </button>
      
      {/* Pass only necessary data to child component */}
      <UserPermissions isAdmin={user.isAdmin} />
    </div>
  );
}

function UserPermissions({ isAdmin }) {
  return (
    <div className="permissions">
      <h3>Permissions</h3>
      {isAdmin ? (
        <ul>
          <li>Can view content</li>
          <li>Can edit content</li>
          <li>Can manage users</li>
          <li>Can access settings</li>
        </ul>
      ) : (
        <ul>
          <li>Can view content</li>
        </ul>
      )}
    </div>
  );
}

Analogy: Water Flow in a Waterfall

Think of React's data flow like a waterfall:

  • Water (data) always flows from higher to lower levels (parent to child)
  • Water cannot naturally flow upward (children can't directly modify parent data)
  • Each pool (component) can only affect pools below it, not above it
  • To change the flow at the source, you need a pump system (callback functions passed as props)

This consistent downward flow makes the system predictable and easier to debug, just like it's easier to trace water contamination in a waterfall (it must be from above) than in a complex network where water flows in multiple directions.

Lifting State Up

"Lifting state up" is a pattern in React where state is moved to a common ancestor of components that need it. This maintains the unidirectional data flow while allowing multiple components to share and update the same data.

flowchart TD subgraph "Before: Separate State" A1[Parent Component] B1[Child A\nwith State] C1[Child B\nwith State] A1 --> B1 A1 --> C1 end subgraph "After: Lifted State" A2[Parent Component\nwith Shared State] B2[Child A\nwith Props] C2[Child B\nwith Props] A2 --"Props"--> B2 A2 --"Props"--> C2 B2 --"Callbacks"--> A2 C2 --"Callbacks"--> A2 end

Example: Temperature Converter

Let's look at an example where two components need to share state:

import { useState } from 'react';

function TemperatureConverter() {
  // State is lifted to the parent component
  const [temperature, setTemperature] = useState('');
  const [scale, setScale] = useState('c'); // 'c' for Celsius, 'f' for Fahrenheit
  
  // Helper functions for converting temperatures
  const toCelsius = (fahrenheit) => {
    return (fahrenheit - 32) * 5 / 9;
  };
  
  const toFahrenheit = (celsius) => {
    return (celsius * 9 / 5) + 32;
  };
  
  // Event handlers
  const handleCelsiusChange = (value) => {
    setTemperature(value);
    setScale('c');
  };
  
  const handleFahrenheitChange = (value) => {
    setTemperature(value);
    setScale('f');
  };
  
  // Calculate both temperatures based on input and scale
  const celsius = scale === 'f' ? toCelsius(parseFloat(temperature)) : parseFloat(temperature);
  const fahrenheit = scale === 'c' ? toFahrenheit(parseFloat(temperature)) : parseFloat(temperature);
  
  return (
    <div>
      <h2>Temperature Converter</h2>
      
      {/* Child component for Celsius input */}
      <TemperatureInput 
        scale="c"
        temperature={celsius}
        onTemperatureChange={handleCelsiusChange}
      />
      
      {/* Child component for Fahrenheit input */}
      <TemperatureInput 
        scale="f"
        temperature={fahrenheit}
        onTemperatureChange={handleFahrenheitChange}
      />
      
      {/* Display water state */}
      {temperature !== '' && !isNaN(celsius) && (
        <div className="result">
          {celsius >= 100 ? 'The water would boil.' : 'The water would not boil.'}
        </div>
      )}
    </div>
  );
}

function TemperatureInput({ scale, temperature, onTemperatureChange }) {
  const scaleNames = {
    c: 'Celsius',
    f: 'Fahrenheit'
  };
  
  const handleChange = (e) => {
    // Pass the new value back to the parent
    onTemperatureChange(e.target.value);
  };
  
  return (
    <fieldset>
      <legend>Enter temperature in {scaleNames[scale]}:</legend>
      <input 
        value={isNaN(temperature) ? '' : temperature}
        onChange={handleChange}
        type="number"
      />
    </fieldset>
  );
}

When to Lift State Up

  • When multiple components need to reflect the same changing data
  • When you need to synchronize state between components
  • When child components need to update data that affects sibling components
  • When you want a single source of truth for a particular piece of data

Remember: Lift state only as high as necessary, but no higher!

Passing Data from Child to Parent

Although React's data flow is unidirectional (parent to child), you can create a two-way communication pattern by passing callback functions as props from parent to child.

Parent Component Child Component props (with callback) invoke callback

Simple Example: Form Input

function ParentComponent() {
  const [name, setName] = useState('');
  
  // This callback will be passed to the child
  const handleNameChange = (newName) => {
    setName(newName);
  };
  
  return (
    <div className="parent">
      <h1>Hello, {name || 'Stranger'}!</h1>
      
      {/* Pass the callback to the child */}
      <ChildComponent onNameChange={handleNameChange} />
    </div>
  );
}

function ChildComponent({ onNameChange }) {
  const handleInputChange = (e) => {
    // Call the parent's callback with the new value
    onNameChange(e.target.value);
  };
  
  return (
    <div className="child">
      <label>
        Enter your name:
        <input type="text" onChange={handleInputChange} />
      </label>
    </div>
  );
}

More Complex Example: Todo List

function TodoList() {
  const [todos, setTodos] = useState([
    { id: 1, text: 'Learn React', completed: false },
    { id: 2, text: 'Build a project', completed: false }
  ]);
  
  // Callback to add a new todo
  const addTodo = (text) => {
    const newTodo = {
      id: Date.now(),
      text,
      completed: false
    };
    setTodos([...todos, newTodo]);
  };
  
  // Callback to toggle completion status
  const toggleTodo = (id) => {
    setTodos(
      todos.map(todo => 
        todo.id === id ? { ...todo, completed: !todo.completed } : todo
      )
    );
  };
  
  // Callback to delete a todo
  const deleteTodo = (id) => {
    setTodos(todos.filter(todo => todo.id !== id));
  };
  
  return (
    <div className="todo-app">
      <h1>Todo List</h1>
      
      {/* Child component to add todos */}
      <AddTodoForm onAddTodo={addTodo} />
      
      {/* Child component to display and manage todos */}
      <TodoItems 
        todos={todos} 
        onToggle={toggleTodo} 
        onDelete={deleteTodo} 
      />
    </div>
  );
}

function AddTodoForm({ onAddTodo }) {
  const [text, setText] = useState('');
  
  const handleSubmit = (e) => {
    e.preventDefault();
    if (!text.trim()) return;
    onAddTodo(text);
    setText('');
  };
  
  return (
    <form onSubmit={handleSubmit}>
      <input
        type="text"
        value={text}
        onChange={(e) => setText(e.target.value)}
        placeholder="Add a new todo"
      />
      <button type="submit">Add</button>
    </form>
  );
}

function TodoItems({ todos, onToggle, onDelete }) {
  return (
    <ul className="todo-list">
      {todos.map(todo => (
        <li key={todo.id}>
          <input
            type="checkbox"
            checked={todo.completed}
            onChange={() => onToggle(todo.id)}
          />
          <span style={{ 
            textDecoration: todo.completed ? 'line-through' : 'none' 
          }}>
            {todo.text}
          </span>
          <button onClick={() => onDelete(todo.id)}>Delete</button>
        </li>
      ))}
    </ul>
  );
}

Real-World Pattern: Container and Presentational Components

A common pattern in React applications is to separate components into:

  • Container Components: Manage state, data fetching, and business logic
  • Presentational Components: Focus on UI rendering, receive data via props, and communicate events up via callbacks
// Container component
function UserContainer() {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  
  useEffect(() => {
    async function fetchUser() {
      try {
        setLoading(true);
        const response = await fetch('/api/user');
        const data = await response.json();
        setUser(data);
        setError(null);
      } catch (err) {
        setError('Failed to fetch user data');
        setUser(null);
      } finally {
        setLoading(false);
      }
    }
    
    fetchUser();
  }, []);
  
  const handleUpdateProfile = async (updatedData) => {
    try {
      setLoading(true);
      const response = await fetch('/api/user', {
        method: 'PUT',
        headers: {
          'Content-Type': 'application/json'
        },
        body: JSON.stringify(updatedData)
      });
      
      const data = await response.json();
      setUser(data);
    } catch (err) {
      setError('Failed to update profile');
    } finally {
      setLoading(false);
    }
  };
  
  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error}</div>;
  if (!user) return <div>No user data found</div>;
  
  // Pass data and callbacks to presentational component
  return <UserProfile user={user} onUpdate={handleUpdateProfile} />;
}

// Presentational component
function UserProfile({ user, onUpdate }) {
  const [formData, setFormData] = useState({
    name: user.name,
    email: user.email
  });
  
  const handleChange = (e) => {
    const { name, value } = e.target;
    setFormData(prev => ({ ...prev, [name]: value }));
  };
  
  const handleSubmit = (e) => {
    e.preventDefault();
    onUpdate(formData); // Call the container's callback
  };
  
  return (
    <div className="user-profile">
      <h2>{user.name}'s Profile</h2>
      
      <form onSubmit={handleSubmit}>
        <div>
          <label>
            Name:
            <input
              type="text"
              name="name"
              value={formData.name}
              onChange={handleChange}
            />
          </label>
        </div>
        
        <div>
          <label>
            Email:
            <input
              type="email"
              name="email"
              value={formData.email}
              onChange={handleChange}
            />
          </label>
        </div>
        
        <button type="submit">Update Profile</button>
      </form>
    </div>
  );
}

Props vs. State

Understanding the difference between props and state is fundamental to mastering React's data flow.

Props State
Received from parent component Defined and managed within the component
Read-only, shouldn't be modified Can be updated with setState/useState
Re-rendered when parent re-renders Re-rendered when state changes
Access via props in functional components
or this.props in class components
Access via state/setState hooks in functional
components or this.state/this.setState in class components
Used to pass data down the component tree Used for component's internal data management

Using Props and State Together

function Counter({ initialCount = 0, step = 1 }) {
  // initialCount and step are props
  // count is state initialized from a prop
  const [count, setCount] = useState(initialCount);
  
  const increment = () => {
    // Using the step prop with internal state
    setCount(count + step);
  };
  
  const decrement = () => {
    setCount(count - step);
  };
  
  const reset = () => {
    // Resetting state back to the initialCount prop
    setCount(initialCount);
  };
  
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={decrement}>- {step}</button>
      <button onClick={increment}>+ {step}</button>
      <button onClick={reset}>Reset</button>
    </div>
  );
}

Analogy: Props vs. State

  • Props are like function arguments - They're passed in from the outside and shouldn't be changed by the function itself.
  • State is like local variables - Created, used, and modified within the function's scope.

Another way to think about it:

  • Props: Like the specifications a client gives you for a project - they come from outside and you work with them as given
  • State: Like your internal notes and drafts while working on the project - you create and modify them as needed during the work

Common Props Patterns

Render Props Pattern

The render props pattern involves passing a function as a prop that returns a React element. This allows the parent component to control what gets rendered while the child handles the logic.

function MouseTracker() {
  const [position, setPosition] = useState({ x: 0, y: 0 });
  
  const handleMouseMove = (e) => {
    setPosition({
      x: e.clientX,
      y: e.clientY
    });
  };
  
  return (
    <div onMouseMove={handleMouseMove} style={{ height: '300px', border: '1px solid #ccc' }}>
      <h2>Move your mouse around!</h2>
      
      {/* Render prop pattern */}
      <MousePosition 
        render={(pos) => (
          <p>Current position: ({pos.x}, {pos.y})</p>
        )}
        position={position}
      />
    </div>
  );
}

function MousePosition({ render, position }) {
  return render(position);
}

Compound Components Pattern

Compound components are components that work together to form a complete UI. The parent component typically manages shared state and provides context for its children.

function Tabs({ children, defaultTab }) {
  const [activeTab, setActiveTab] = useState(defaultTab);
  
  // Clone children to inject the active prop
  const tabs = React.Children.map(children, child => {
    if (child.type === Tab) {
      return React.cloneElement(child, {
        active: child.props.id === activeTab,
        onActivate: () => setActiveTab(child.props.id)
      });
    }
    return child;
  });
  
  return <div className="tabs">{tabs}</div>;
}

function Tab({ id, title, children, active, onActivate }) {
  return (
    <div className={`tab ${active ? 'active' : ''}`}>
      <div className="tab-header" onClick={onActivate}>
        {title}
      </div>
      {active && <div className="tab-content">{children}</div>}
    </div>
  );
}

// Usage
function App() {
  return (
    <Tabs defaultTab="profile">
      <Tab id="profile" title="Profile">
        <p>This is the profile tab</p>
      </Tab>
      <Tab id="settings" title="Settings">
        <p>This is the settings tab</p>
      </Tab>
      <Tab id="notifications" title="Notifications">
        <p>This is the notifications tab</p>
      </Tab>
    </Tabs>
  );
}

Props Spreading and Destructuring

For components that forward props to underlying DOM elements or other components, props spreading can be useful.

function Button({ className, children, ...rest }) {
  // Combine custom class with default button class
  const buttonClass = `button ${className || ''}`.trim();
  
  // Spread the remaining props to the button element
  return (
    <button className={buttonClass} {...rest}>
      {children}
    </button>
  );
}

// Usage
function App() {
  return (
    <div>
      <Button 
        onClick={() => alert('Clicked!')} 
        disabled={false}
        className="primary"
        data-testid="submit-button"
      >
        Click Me
      </Button>
    </div>
  );
}

Be Careful with Props Spreading

While convenient, props spreading can sometimes pass unnecessary or even harmful props. Consider using more explicit props when:

  • You need better prop type validation
  • You're working on a security-sensitive component
  • You want to avoid accidental prop overrides
  • You want to make the component API more explicit

Context API for Deep Prop Passing

Sometimes, props need to be passed through many levels of components, which can lead to "prop drilling". React's Context API offers a solution to this problem by allowing data to be passed directly to any component in the tree without explicit prop passing.

graph TD A[App with User Data] --> B[Header] A --> C[MainContent] A --> D[Footer] C --> E[Sidebar] C --> F[ArticleList] F --> G[Article] G --> H[CommentSection] subgraph "Without Context (Prop Drilling)" A1[App] --user--> B1[Header] A1 --user--> C1[MainContent] C1 --user--> F1[ArticleList] F1 --user--> G1[Article] G1 --user--> H1[CommentSection] end subgraph "With Context API" A2[App / UserProvider] --> C2[MainContent] C2 --> F2[ArticleList] F2 --> G2[Article] G2 --> H2[CommentSection\nUseContext(UserContext)] end

Using Context API

import React, { createContext, useContext, useState } from 'react';

// Create a context with default value
const UserContext = createContext({
  user: null,
  setUser: () => {}
});

// Provider component to wrap the app
function UserProvider({ children }) {
  const [user, setUser] = useState(null);
  
  // The value prop contains the data to be shared
  return (
    <UserContext.Provider value={{ user, setUser }}>
      {children}
    </UserContext.Provider>
  );
}

// Custom hook for easy context usage
function useUser() {
  const context = useContext(UserContext);
  if (context === undefined) {
    throw new Error('useUser must be used within a UserProvider');
  }
  return context;
}

// App component with the provider
function App() {
  return (
    <UserProvider>
      <div className="app">
        <Header />
        <MainContent />
        <Footer />
      </div>
    </UserProvider>
  );
}

// No need to pass user props through these components
function MainContent() {
  return (
    <main>
      <h2>Main Content</h2>
      <ArticleList />
    </main>
  );
}

function ArticleList() {
  return (
    <div>
      <h3>Articles</h3>
      <Article id="1" title="Context API Explained" />
      <Article id="2" title="Props vs. Context" />
    </div>
  );
}

function Article({ id, title }) {
  return (
    <article>
      <h4>{title}</h4>
      <p>Article content...</p>
      <CommentSection articleId={id} />
    </article>
  );
}

// Deep component accessing context directly
function CommentSection({ articleId }) {
  // Access user data from context
  const { user } = useUser();
  
  return (
    <div className="comments">
      <h5>Comments</h5>
      
      {user ? (
        <div>
          <p>Comment as {user.name}</p>
          <textarea placeholder="Write a comment..." />
          <button>Submit</button>
        </div>
      ) : (
        <p>Please log in to comment</p>
      )}
    </div>
  );
}

// Component that modifies the context
function Header() {
  const { user, setUser } = useUser();
  
  const login = () => {
    // Simulated login
    setUser({ id: 1, name: 'John Doe' });
  };
  
  const logout = () => {
    setUser(null);
  };
  
  return (
    <header>
      <h1>My App</h1>
      <div>
        {user ? (
          <>
            <span>Welcome, {user.name}!</span>
            <button onClick={logout}>Logout</button>
          </>
        ) : (
          <button onClick={login}>Login</button>
        )}
      </div>
    </header>
  );
}

When to Use Context vs. Props

  • Use Props When:
    • Data is only needed by immediate children
    • Component reusability is critical
    • Component relationships are clear and shallow
  • Use Context When:
    • Data is needed by many components at different levels
    • Passing props would create excessive "prop drilling"
    • You need a global state for themes, user auth, etc.

Practice Activities

Activity 1: Props and Data Flow

Objective: Practice passing props and implementing unidirectional data flow.

Instructions:

  1. Create a ShoppingCart component that manages a list of items
  2. Create a CartItem component that displays an item with quantity controls
  3. Create an AddItemForm component to add new items to the cart
  4. Implement callbacks to update quantities and add/remove items
  5. Add a CartSummary component to display the total price

Ensure all data flow follows React's unidirectional pattern.

Activity 2: Lifting State Up

Objective: Practice the "lifting state up" pattern.

Instructions:

  1. Create a FilterableProductTable component with:
    • SearchBar component with text input and "in stock only" checkbox
    • ProductTable component displaying filtered products
  2. Lift the filter state (search text and "in stock only" flag) to the parent component
  3. Implement callbacks to update the filter state
  4. Filter products based on the state before passing them to ProductTable

Activity 3: Context API

Objective: Practice using Context API to avoid prop drilling.

Instructions:

  1. Create a ThemeContext with light and dark theme options
  2. Implement a ThemeProvider component with state for the current theme
  3. Create a deep component tree with at least 3 levels of nesting
  4. Add a ThemeToggle button component that uses the context to change themes
  5. Apply theme styles to various components at different levels of the tree

Compare this implementation with how it would look using props.

Resources for Further Learning

Official Documentation:

Additional Resources:

Summary

In the next lecture, we'll explore component composition patterns and learn how to build more complex UIs by combining simpler components.