React.js Fundamentals

Props & Data Flow

20 min Lesson 5 of 40

Understanding Props in Depth

Props (properties) are React's mechanism for passing data and functionality from parent components to child components. They are the primary way components communicate with each other in React's unidirectional data flow architecture.

Props Core Concepts:
  • Props are read-only - components cannot modify their own props
  • Props flow downward - from parent to child, never upward
  • Props can be any data type - strings, numbers, objects, arrays, functions, even other components
  • Props make components reusable - the same component can display different data

Passing Different Data Types as Props

1. String Props

// Strings can be passed with or without curly braces <Button text="Submit" /> <Button text={'Submit'} /> // Also valid, but quotes are preferred // Multiline strings <Description text="This is a very long description that spans multiple lines and contains lots of detail." />

2. Number Props

// Numbers must use curly braces <Counter count={42} /> <Price value={19.99} /> <RatingStars rating={4.5} /> function Counter({ count }) { return <div>Count: {count}</div>; }

3. Boolean Props

// Explicit boolean values <Button disabled={true} /> <Modal isOpen={false} /> // Shorthand for true (just the attribute name) <Button disabled /> <Input required /> <Checkbox checked /> // These are equivalent: <Button disabled={true} /> <Button disabled />

4. Array Props

function TagList({ tags }) { return ( <div className="tags"> {tags.map((tag, index) => ( <span key={index} className="tag">{tag}</span> ))} </div> ); } // Usage <TagList tags={['React', 'JavaScript', 'Frontend']} />

5. Object Props

function UserProfile({ user }) { return ( <div> <h2>{user.name}</h2> <p>{user.email}</p> <p>Age: {user.age}</p> </div> ); } // Usage <UserProfile user={{ name: 'Sarah Johnson', email: 'sarah@example.com', age: 28 }} />
Performance Note: When passing object literals directly in JSX (like user={{name: 'Sarah'}}), a new object is created on every render. For better performance, define objects outside the component or in state.

6. Function Props (Callbacks)

function Button({ onClick, text }) { return <button onClick={onClick}>{text}</button>; } function App() { const handleClick = () => { console.log('Button clicked!'); }; return <Button onClick={handleClick} text="Click Me" />; }

The Children Prop

The children prop is a special prop that represents the content between a component's opening and closing tags. It's one of React's most powerful features for creating flexible, composable components.

// Basic children usage function Card({ children }) { return ( <div className="card"> {children} </div> ); } // Usage <Card> <h2>Card Title</h2> <p>Card content goes here.</p> </Card>

Children can be anything:

// String children <Button>Click Me</Button> // JSX children <Container> <Header /> <Content /> <Footer /> </Container> // Mixed content children <Alert> <strong>Warning:</strong> This action cannot be undone! </Alert> // Function children (render props pattern) <DataProvider> {data => <div>{data.message}</div>} </DataProvider>

Practical Example: Layout Components

// Flexible layout component function PageLayout({ children }) { return ( <div className="page-layout"> <header className="header"> <h1>My Application</h1> </header> <main className="main-content"> {children} </main> <footer className="footer"> <p>© 2024 Company Name</p> </footer> </div> ); } // Usage - different content for different pages function HomePage() { return ( <PageLayout> <h2>Welcome Home!</h2> <p>This is the home page content.</p> </PageLayout> ); } function AboutPage() { return ( <PageLayout> <h2>About Us</h2> <p>This is the about page content.</p> </PageLayout> ); }

Prop Destructuring Patterns

1. Basic Destructuring

// Without destructuring function UserCard(props) { return ( <div> <h2>{props.name}</h2> <p>{props.email}</p> </div> ); } // With destructuring (preferred) function UserCard({ name, email }) { return ( <div> <h2>{name}</h2> <p>{email}</p> </div> ); }

2. Destructuring with Default Values

function Button({ text = 'Click me', type = 'button', disabled = false }) { return ( <button type={type} disabled={disabled}> {text} </button> ); } // All these are valid: <Button /> // Uses all defaults <Button text="Submit" /> // Overrides text only <Button text="Delete" type="button" disabled /> // Overrides all

3. Rest Parameters (Spreading Remaining Props)

function Button({ text, variant, ...otherProps }) { // Extract specific props, spread the rest return ( <button className={`btn btn-${variant}`} {...otherProps}> {text} </button> ); } // Usage - onClick, disabled, etc. are spread onto the button <Button text="Submit" variant="primary" onClick={handleClick} disabled={isLoading} data-testid="submit-btn" />

4. Nested Object Destructuring

function UserProfile({ user: { name, email, address } }) { return ( <div> <h2>{name}</h2> <p>{email}</p> <p>{address}</p> </div> ); } // Usage <UserProfile user={{ name: 'Alice', email: 'alice@example.com', address: '123 Main St' }} />

Prop Drilling and Its Challenges

Prop drilling occurs when you pass props through multiple layers of components, even when intermediate components don't need those props.

// Prop drilling example function App() { const user = { name: 'Alice', role: 'Admin' }; return <Dashboard user={user} />; } function Dashboard({ user }) { // Dashboard doesn't use user, just passes it down return ( <div> <Sidebar user={user} /> </div> ); } function Sidebar({ user }) { // Sidebar doesn't use user either, just passes it down return ( <div> <UserMenu user={user} /> </div> ); } function UserMenu({ user }) { // Finally! UserMenu actually uses the user prop return ( <div> <p>Welcome, {user.name}!</p> <p>Role: {user.role}</p> </div> ); }
Problems with Prop Drilling:
  • Makes components less reusable (they require props they don't use)
  • Harder to refactor (changing props affects many components)
  • More verbose code with repeated prop passing
  • Difficult to track data flow in large applications

Solutions include Context API (covered in later lessons), state management libraries, or component composition patterns.

Callback Props: Parent-Child Communication

While props flow down, callbacks allow children to communicate back to parents:

function Parent() { const handleChildClick = (childName) => { console.log(`${childName} was clicked!`); }; return ( <div> <Child name="Child 1" onChildClick={handleChildClick} /> <Child name="Child 2" onChildClick={handleChildClick} /> </div> ); } function Child({ name, onChildClick }) { return ( <button onClick={() => onChildClick(name)}> Click {name} </button> ); }

Practical Example: Form with Callbacks

function LoginForm() { const handleSubmit = (email, password) => { console.log('Logging in with:', email, password); // Make API call here }; return ( <div> <h2>Login</h2> <Form onSubmit={handleSubmit} /> </div> ); } function Form({ onSubmit }) { const [email, setEmail] = React.useState(''); const [password, setPassword] = React.useState(''); const handleFormSubmit = (e) => { e.preventDefault(); onSubmit(email, password); }; return ( <form onSubmit={handleFormSubmit}> <input type="email" value={email} onChange={(e) => setEmail(e.target.value)} placeholder="Email" /> <input type="password" value={password} onChange={(e) => setPassword(e.target.value)} placeholder="Password" /> <button type="submit">Login</button> </form> ); }

Prop Types and Validation

While React doesn't enforce prop types by default, you can add validation using PropTypes or TypeScript:

// Using PropTypes (requires prop-types package) import PropTypes from 'prop-types'; function UserCard({ name, age, email, isActive }) { return ( <div> <h2>{name}</h2> <p>Age: {age}</p> <p>Email: {email}</p> {isActive && <span>Active</span>} </div> ); } // Define prop types UserCard.propTypes = { name: PropTypes.string.isRequired, age: PropTypes.number.isRequired, email: PropTypes.string.isRequired, isActive: PropTypes.bool }; // Define default props UserCard.defaultProps = { isActive: false };
PropTypes vs TypeScript: PropTypes provide runtime validation (errors appear in console). TypeScript provides compile-time type checking (errors during development). Modern React apps increasingly use TypeScript for better type safety.

Advanced Prop Patterns

1. Render Props Pattern

// Component that shares logic through a render prop function Mouse({ render }) { const [position, setPosition] = React.useState({ x: 0, y: 0 }); const handleMouseMove = (event) => { setPosition({ x: event.clientX, y: event.clientY }); }; return ( <div onMouseMove={handleMouseMove}> {render(position)} </div> ); } // Usage <Mouse render={({ x, y }) => ( <div> <h1>Mouse Position</h1> <p>X: {x}, Y: {y}</p> </div> )} />

2. Component as Props

function Layout({ Header, Content, Footer }) { return ( <div className="layout"> <Header /> <Content /> <Footer /> </div> ); } // Usage <Layout Header={() => <header>My App</header>} Content={() => <main>Content here</main>} Footer={() => <footer>© 2024</footer>} />

3. Spread Props Pattern

// Wrapper component that passes through all props function StyledButton(props) { return <button {...props} className="styled-btn" />; } // All props (onClick, disabled, etc.) are passed to the button <StyledButton onClick={handleClick} disabled={isLoading}> Submit </StyledButton>

Best Practices for Props

1. Keep Prop Names Clear and Consistent ✅ Good: <Button onClick={handleClick} isLoading={loading} /> ❌ Bad: <Button click={handleClick} load={loading} /> 2. Use Boolean Props with "is/has" Prefix ✅ Good: isActive, isLoading, hasError, canEdit ❌ Bad: active, loading, error, edit 3. Keep Components Focused ✅ Good: <Button text="Submit" onClick={handleSubmit} /> ❌ Bad: <Button text="Submit" onClick={handleSubmit} color="blue" fontSize={16} padding={10} border="1px solid" ... /> // Instead, use className or style props 4. Avoid Passing Too Many Props If a component needs 10+ props, consider: - Breaking it into smaller components - Grouping related props into objects - Using composition instead 5. Document Complex Props // Add comments for complex prop structures /** * @param {Object} user - User object * @param {string} user.name - User's full name * @param {string} user.email - User's email address * @param {number} user.age - User's age */ function UserProfile({ user }) { ... }
Exercise 1: Product List with Callbacks

Create a product listing system where child components communicate with the parent:

Requirements:

  1. Create a ProductList parent component
  2. Create a ProductCard child component
  3. ProductCard should have an "Add to Cart" button
  4. When clicked, it should call a callback prop with the product info
  5. Parent should log which product was added
  6. Pass an array of products as props

Test data:

const products = [ { id: 1, name: 'Laptop', price: 999 }, { id: 2, name: 'Mouse', price: 29 }, { id: 3, name: 'Keyboard', price: 79 } ];
Exercise 2: Flexible Card Component with Children

Build a flexible card component system using the children prop:

Requirements:

  1. Create a Card component that accepts children
  2. Add optional props: variant ('default', 'primary', 'danger'), padding, shadow
  3. Style the card differently based on variant
  4. Create 3 different cards with different content using children
  5. One card should contain text, one should contain an image and text, one should contain a form
Exercise 3: Data Flow Challenge

Build a todo list where data flows between components:

Component structure:

<TodoApp> <TodoInput onAddTodo={callback} /> <TodoList todos={array}> <TodoItem todo={object} onToggle={callback} onDelete={callback} /> </TodoList> </TodoApp>

Requirements:

  • TodoApp manages the todo list array
  • TodoInput receives a callback to add new todos
  • TodoList receives the todos array and renders TodoItems
  • Each TodoItem receives a todo object and callbacks for toggle/delete
  • Demonstrate proper prop passing through all levels

Common Prop Mistakes to Avoid

1. Mutating Props
// ❌ NEVER modify props directly function BadComponent({ items }) { items.push('new item'); // WRONG! return <div>{items.length}</div>; } // ✅ Create new arrays instead function GoodComponent({ items, onAddItem }) { const newItems = [...items, 'new item']; onAddItem(newItems); return <div>{newItems.length}</div>; }
2. Creating Functions in Render
// ❌ Creates new function on every render <Button onClick={() => handleClick(id)} /> // ✅ Create function once or use useCallback (later lesson) const handleButtonClick = () => handleClick(id); <Button onClick={handleButtonClick} />
3. Passing Entire State as Props
// ❌ Child gets entire state, even data it doesn't need <UserProfile state={this.state} /> // ✅ Pass only what's needed <UserProfile name={state.name} email={state.email} />

Summary

In this lesson, you mastered props and data flow in React:

  • Props are read-only data passed from parent to child components
  • Props can be any data type: strings, numbers, booleans, objects, arrays, functions
  • The children prop provides flexible component composition
  • Prop destructuring makes component code cleaner and more readable
  • Callback props enable child-to-parent communication
  • Prop drilling can become a problem in deeply nested components
  • PropTypes and TypeScript provide prop validation and type safety
  • Various advanced patterns (render props, component props, spread props)
  • Best practices include clear naming, focused components, and proper documentation

In the next lesson, we'll explore React State - how components manage and update their own data, making your applications truly interactive and dynamic!