javascript-today

Props

Props - Passing Data to Components

Props (short for “properties”) are how you pass data from parent components to child components in React. They’re React’s way of making components reusable and dynamic.

What Are Props?

Props are arguments passed to components, just like function parameters:

// Parent passes data via props
function App() {
  return <Greeting name="Alice" age={25} />;
}

// Child receives props as an object
function Greeting(props) {
  return <h1>Hello, {props.name}! You are {props.age}.</h1>;
}

Destructuring Props

The modern way to handle props:

// Instead of this
function Greeting(props) {
  return <h1>Hello, {props.name}!</h1>;
}

// Do this (destructure in parameter)
function Greeting({ name }) {
  return <h1>Hello, {name}!</h1>;
}

// Multiple props
function UserCard({ name, email, age, avatar }) {
  return (
    <div>
      <img src={avatar} alt={name} />
      <h3>{name}</h3>
      <p>{email}</p>
      <p>Age: {age}</p>
    </div>
  );
}

Passing Props

String props:

<Greeting name="Alice" />

Number props (use curly braces):

<Counter initialCount={0} />

Boolean props:

<Button disabled={true} />
<Button disabled />  {/* Shorthand for true */}

Array props:

<TodoList items={['Task 1', 'Task 2', 'Task 3']} />

Object props:

<UserCard user={{ name: 'Alice', age: 25 }} />

Function props:

<Button onClick={handleClick} />

Props are Read-Only

Never modify props - they’re immutable:

// ❌ Wrong - mutating props
function Button(props) {
  props.text = "Modified";  // Error!
  return <button>{props.text}</button>;
}

// ✅ Right - use props as-is
function Button({ text }) {
  return <button>{text}</button>;
}

// ✅ Right - derive new values
function Button({ text }) {
  const uppercase = text.toUpperCase();
  return <button>{uppercase}</button>;
}

Default Props

Set default values for props:

// Method 1: Default parameters (modern)
function Greeting({ name = "Guest", greeting = "Hello" }) {
  return <h1>{greeting}, {name}!</h1>;
}

// Method 2: defaultProps (legacy but still valid)
function Greeting({ name, greeting }) {
  return <h1>{greeting}, {name}!</h1>;
}

Greeting.defaultProps = {
  name: "Guest",
  greeting: "Hello"
};

// Usage
<Greeting />  // Hello, Guest!
<Greeting name="Alice" />  // Hello, Alice!

Passing Multiple Props

Individual props:

<UserCard 
  name="Alice"
  email="alice@example.com"
  age={25}
  avatar="/avatar.jpg"
/>

Spread operator (pass entire object):

const user = {
  name: "Alice",
  email: "alice@example.com",
  age: 25,
  avatar: "/avatar.jpg"
};

<UserCard {...user} />  // Spreads all properties as props

Combining spread with overrides:

<UserCard {...user} age={26} />  // Override specific prop

Children Prop

Special prop for content between component tags:

function Card({ children }) {
  return (
    <div className="card">
      <div className="card-content">
        {children}
      </div>
    </div>
  );
}

// Usage
<Card>
  <h3>Title</h3>
  <p>This content becomes the children prop</p>
</Card>

Children can be anything:

// Text
<Button>Click me</Button>

// Elements
<Card>
  <h3>Header</h3>
  <p>Body</p>
</Card>

// Components
<Layout>
  <Header />
  <Content />
  <Footer />
</Layout>

// Expressions
<Alert>
  {error ? error.message : "No errors"}
</Alert>

Props Flow (One-Way Data Flow)

Data flows down from parent to child:

function App() {
  const user = { name: "Alice", role: "Admin" };
  
  return (
    <div>
      <Header user={user} />      {/* Parent → Child */}
      <Sidebar user={user} />     {/* Parent → Child */}
      <Content user={user} />     {/* Parent → Child */}
    </div>
  );
}

function Header({ user }) {
  return <h1>Welcome, {user.name}</h1>;
}

Child cannot directly modify parent’s data. Use callback props instead (covered later).

Callback Props

Pass functions as props to let children communicate with parents:

function Parent() {
  const handleChildClick = (message) => {
    console.log("Child says:", message);
  };
  
  return <Child onAction={handleChildClick} />;
}

function Child({ onAction }) {
  return (
    <button onClick={() => onAction("Hello from child!")}>
      Click me
    </button>
  );
}

Prop Types Validation

Using TypeScript (recommended):

interface UserCardProps {
  name: string;
  age: number;
  email?: string;  // Optional
}

function UserCard({ name, age, email }: UserCardProps) {
  return (
    <div>
      <h3>{name}</h3>
      <p>Age: {age}</p>
      {email && <p>Email: {email}</p>}
    </div>
  );
}

Using PropTypes (JavaScript):

import PropTypes from 'prop-types';

function UserCard({ name, age, email }) {
  return (
    <div>
      <h3>{name}</h3>
      <p>Age: {age}</p>
      {email && <p>Email: {email}</p>}
    </div>
  );
}

UserCard.propTypes = {
  name: PropTypes.string.isRequired,
  age: PropTypes.number.isRequired,
  email: PropTypes.string
};

Practical Examples

Example 1: Product Card

function ProductCard({ name, price, image, inStock, onAddToCart }) {
  return (
    <div className="product-card">
      <img src={image} alt={name} />
      <h3>{name}</h3>
      <p className="price">${price.toFixed(2)}</p>
      
      {inStock ? (
        <button onClick={() => onAddToCart(name)}>
          Add to Cart
        </button>
      ) : (
        <p className="out-of-stock">Out of Stock</p>
      )}
    </div>
  );
}

// Usage
<ProductCard
  name="Laptop"
  price={999.99}
  image="/laptop.jpg"
  inStock={true}
  onAddToCart={(productName) => console.log("Added:", productName)}
/>

Example 2: Reusable Button

function Button({ 
  children, 
  variant = "primary", 
  size = "medium",
  disabled = false,
  onClick 
}) {
  const className = `btn btn-${variant} btn-${size}`;
  
  return (
    <button 
      className={className}
      disabled={disabled}
      onClick={onClick}
    >
      {children}
    </button>
  );
}

// Usage
<Button variant="primary" size="large" onClick={handleSave}>
  Save Changes
</Button>

<Button variant="secondary" onClick={handleCancel}>
  Cancel
</Button>

<Button variant="danger" disabled>
  Delete (disabled)
</Button>

Example 3: User Profile

function UserProfile({ user, showEmail = true, showBio = true }) {
  return (
    <div className="user-profile">
      <Avatar src={user.avatar} alt={user.name} />
      <h2>{user.name}</h2>
      
      {showEmail && user.email && (
        <p className="email">{user.email}</p>
      )}
      
      {showBio && user.bio && (
        <p className="bio">{user.bio}</p>
      )}
      
      <Stats 
        followers={user.followers} 
        following={user.following} 
      />
    </div>
  );
}

function Avatar({ src, alt }) {
  return <img src={src} alt={alt} className="avatar" />;
}

function Stats({ followers, following }) {
  return (
    <div className="stats">
      <span>{followers} Followers</span>
      <span>{following} Following</span>
    </div>
  );
}

Example 4: List with Custom Rendering

function List({ items, renderItem }) {
  return (
    <ul className="list">
      {items.map((item, index) => (
        <li key={item.id || index}>
          {renderItem(item)}
        </li>
      ))}
    </ul>
  );
}

// Usage with different render functions
const users = [
  { id: 1, name: "Alice", role: "Admin" },
  { id: 2, name: "Bob", role: "User" }
];

// Simple rendering
<List 
  items={users}
  renderItem={(user) => <span>{user.name}</span>}
/>

// Complex rendering
<List 
  items={users}
  renderItem={(user) => (
    <div>
      <strong>{user.name}</strong>
      <span className="role">{user.role}</span>
    </div>
  )}
/>

Common Patterns

1. Configuration object:

function DataTable({ config }) {
  const { columns, data, pageSize, sortable } = config;
  
  return (
    <table>
      {/* Use config to render table */}
    </table>
  );
}

2. Render props:

function MouseTracker({ render }) {
  const [position, setPosition] = useState({ x: 0, y: 0 });
  
  const handleMouseMove = (e) => {
    setPosition({ x: e.clientX, y: e.clientY });
  };
  
  return (
    <div onMouseMove={handleMouseMove}>
      {render(position)}
    </div>
  );
}

// Usage
<MouseTracker 
  render={({ x, y }) => (
    <p>Mouse at ({x}, {y})</p>
  )}
/>

3. Props forwarding:

function FancyButton({ children, ...otherProps }) {
  return (
    <button className="fancy-button" {...otherProps}>
      {children}
    </button>
  );
}

// All props except children are forwarded
<FancyButton onClick={handleClick} disabled>
  Click me
</FancyButton>

Best Practices

1. Keep props simple:

// Good - simple props
<UserCard name="Alice" age={25} />

// Bad - overly nested
<Component data={{ user: { profile: { details: { name: "Alice" }}}}} />

2. Use destructuring:

// Good
function Component({ name, age, email }) { }

// Less good
function Component(props) {
  return <div>{props.name} {props.age} {props.email}</div>;
}

3. Don’t pass too many props:

// Bad - too many props
<Component prop1={a} prop2={b} prop3={c} prop4={d} prop5={e} prop6={f} />

// Better - group related props
<Component user={userData} config={configData} handlers={handlers} />

// Or split into smaller components
<UserComponent user={userData} />
<ConfigComponent config={configData} />

4. Use meaningful names:

// Good
<Button onClick={handleSubmit} disabled={isLoading}>Submit</Button>

// Bad
<Button fn={fn1} flag={f}>Submit</Button>

5. Extract repeated prop sets:

// Before
<Button onClick={handleClick} className="primary" size="large">Save</Button>
<Button onClick={handleClick2} className="primary" size="large">Submit</Button>

// After
const buttonProps = {
  className: "primary",
  size: "large"
};

<Button {...buttonProps} onClick={handleClick}>Save</Button>
<Button {...buttonProps} onClick={handleClick2}>Submit</Button>

Common Mistakes

1. Modifying props:

// Wrong
function Component({ items }) {
  items.push('new item');  // Mutating props!
  return <ul>...</ul>;
}

// Right
function Component({ items }) {
  const newItems = [...items, 'new item'];
  return <ul>...</ul>;
}

2. Forgetting curly braces:

// Wrong - passes string "25" not number 25
<Component age="25" />

// Right
<Component age={25} />

3. Prop drilling (passing props through many levels):

// Problem
<GrandParent data={data} />
  <Parent data={data} />
    <Child data={data} />
      <GrandChild data={data} />  // Finally used here

// Solution: Use Context or state management (covered later)

Props vs State

Props:

  • Passed from parent
  • Read-only
  • Controlled by parent
  • Use for configuration

State (covered next):

  • Internal to component
  • Mutable
  • Controlled by component itself
  • Use for interactivity
// Props example - data from parent
function Greeting({ name }) {  // name is a prop
  return <h1>Hello, {name}</h1>;
}

// State example - component's own data (next lesson)
function Counter() {
  const [count, setCount] = useState(0);  // count is state
  return <button onClick={() => setCount(count + 1)}>{count}</button>;
}