Beginner50 min read

Conditional Rendering and Lists in React

Learn how to conditionally render components and display lists of data efficiently in React applications.

Topics Covered:

Conditional RenderingListsKeysArray MethodsFilteringMapping

Prerequisites:

  • Understanding Props
  • Managing State with useState

Video Tutorial

Overview

Conditional rendering and lists are fundamental patterns in React. You'll frequently need to show or hide components based on conditions, and display collections of data. This tutorial covers all the ways to conditionally render content and how to properly render lists with keys for optimal performance.

Understanding Conditional Rendering

Conditional rendering allows you to show or hide components based on conditions. React supports several patterns for conditional rendering. Common Use Cases: • Show/hide components based on state • Display different content for logged in/out users • Show loading states • Display error messages conditionally • Render different components based on props Why It's Important: • Creates dynamic, interactive UIs • Improves user experience • Reduces unnecessary DOM elements • Makes components more flexible

Code Example:
// Basic conditional rendering with if/else
function Greeting({ isLoggedIn }) {
  if (isLoggedIn) {
    return <h1>Welcome back!</h1>;
  } else {
    return <h1>Please sign in.</h1>;
  }
}

// Conditional rendering with ternary operator
function UserStatus({ isOnline }) {
  return (
    <div>
      <p>Status: {isOnline ? 'Online' : 'Offline'}</p>
      {isOnline ? (
        <span className="text-green-500">●</span>
      ) : (
        <span className="text-gray-500">○</span>
      )}
    </div>
  );
}

// Conditional rendering with && operator
function Notification({ count }) {
  return (
    <div>
      <h2>Notifications</h2>
      {count > 0 && (
        <p>You have {count} new notifications</p>
      )}
      {count === 0 && (
        <p>No new notifications</p>
      )}
    </div>
  );
}

// Preventing component from rendering
function Warning({ show }) {
  if (!show) {
    return null; // Don't render anything
  }
  
  return <div className="warning">Warning message</div>;
}

Use if/else for simple conditions, ternary for inline conditions, && for conditional rendering, and return null to prevent rendering. Choose the pattern that makes your code most readable.

Rendering Lists of Data

React makes it easy to render lists of items. You use JavaScript's array methods, especially map(), to transform arrays of data into arrays of React elements. Key Concepts: • Use map() to transform arrays to JSX • Each list item needs a unique key • Keys help React identify which items changed • Keys should be stable, predictable, and unique Common Patterns: • Rendering arrays of objects • Filtering before rendering • Sorting lists • Nested lists

Code Example:
// Basic list rendering
function NumberList({ numbers }) {
  return (
    <ul>
      {numbers.map((number) => (
        <li key={number}>{number}</li>
      ))}
    </ul>
  );
}

// Rendering list of objects
const users = [
  { id: 1, name: 'Alice', age: 25 },
  { id: 2, name: 'Bob', age: 30 },
  { id: 3, name: 'Charlie', age: 35 }
];

function UserList() {
  return (
    <ul>
      {users.map((user) => (
        <li key={user.id}>
          {user.name} - {user.age} years old
        </li>
      ))}
    </ul>
  );
}

// List with component
function TodoList({ todos }) {
  return (
    <div>
      {todos.map((todo) => (
        <TodoItem key={todo.id} todo={todo} />
      ))}
    </div>
  );
}

function TodoItem({ todo }) {
  return (
    <div>
      <h3>{todo.title}</h3>
      <p>{todo.description}</p>
    </div>
  );
}

Use map() to transform arrays into JSX elements. Always provide a key prop for list items. Use unique IDs when available, or index as last resort.

Understanding Keys in Lists

Keys are special string attributes you need to include when creating lists of elements. They help React identify which items have changed, been added, or removed. Why Keys Matter: • Help React efficiently update the DOM • Preserve component state across re-renders • Prevent bugs when list order changes • Improve performance Key Rules: • Keys must be unique among siblings • Keys should be stable (don't use index if items can reorder) • Keys should be predictable (not random) • Keys only need to be unique within the same list What NOT to Use: • Don't use array index if items can be reordered • Don't use random values • Don't use keys that change on every render

Code Example:
// ✅ GOOD: Using unique IDs
function UserList({ users }) {
  return (
    <ul>
      {users.map((user) => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

// ✅ OK: Using index when list is static
function StaticList({ items }) {
  return (
    <ul>
      {items.map((item, index) => (
        <li key={index}>{item}</li>
      ))}
    </ul>
  );
}

// ❌ BAD: Using index when items can be reordered
function TodoList({ todos }) {
  const [sortedTodos, setSortedTodos] = useState(todos);
  
  return (
    <ul>
      {sortedTodos.map((todo, index) => (
        <li key={index}>{todo.text}</li>
        // ❌ If todos are reordered, React will get confused
      ))}
    </ul>
  );
}

// ✅ GOOD: Using stable unique IDs
function TodoList({ todos }) {
  const [sortedTodos, setSortedTodos] = useState(todos);
  
  return (
    <ul>
      {sortedTodos.map((todo) => (
        <li key={todo.id}>{todo.text}</li>
        // ✅ React can track items even when reordered
      ))}
    </ul>
  );
}

// Creating keys from multiple properties
function ProductList({ products }) {
  return (
    <div>
      {products.map((product) => (
        <Product
          key={`${product.category}-${product.id}`}
          product={product}
        />
      ))}
    </div>
  );
}

Keys help React efficiently update lists. Use unique, stable IDs when possible. Only use index for static lists that never reorder. Never use random values or values that change on every render.

Filtering and Transforming Lists

Often you need to filter, sort, or transform data before rendering it. JavaScript array methods work perfectly with React. Common Operations: • filter() - Show only items matching criteria • sort() - Order items • slice() - Show limited items • find() - Find specific item • reduce() - Aggregate data Best Practices: • Filter before mapping when possible • Use useMemo for expensive operations • Keep transformations readable • Consider extracting complex logic

Code Example:
// Filtering before rendering
function ActiveUsers({ users }) {
  const activeUsers = users.filter(user => user.isActive);
  
  return (
    <ul>
      {activeUsers.map(user => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

// Filtering and mapping together
function TodoList({ todos, filter }) {
  const filteredTodos = todos.filter(todo => {
    if (filter === 'active') return !todo.completed;
    if (filter === 'completed') return todo.completed;
    return true; // 'all'
  });
  
  return (
    <ul>
      {filteredTodos.map(todo => (
        <TodoItem key={todo.id} todo={todo} />
      ))}
    </ul>
  );
}

// Sorting before rendering
function SortedProductList({ products }) {
  const sortedProducts = [...products].sort((a, b) => 
    a.price - b.price
  );
  
  return (
    <div>
      {sortedProducts.map(product => (
        <Product key={product.id} product={product} />
      ))}
    </div>
  );
}

// Complex filtering and transformation
function UserDashboard({ users }) {
  const stats = useMemo(() => {
    const active = users.filter(u => u.isActive).length;
    const admins = users.filter(u => u.role === 'admin').length;
    const total = users.length;
    
    return { active, admins, total };
  }, [users]);
  
  return (
    <div>
      <p>Total: {stats.total}</p>
      <p>Active: {stats.active}</p>
      <p>Admins: {stats.admins}</p>
    </div>
  );
}

// Conditional list rendering
function SearchResults({ query, items }) {
  const filtered = items.filter(item =>
    item.name.toLowerCase().includes(query.toLowerCase())
  );
  
  if (filtered.length === 0) {
    return <p>No results found</p>;
  }
  
  return (
    <ul>
      {filtered.map(item => (
        <li key={item.id}>{item.name}</li>
      ))}
    </ul>
  );
}

Use JavaScript array methods to filter, sort, and transform data before rendering. Use useMemo for expensive operations. Always filter before mapping when possible for better performance.

Nested Lists and Complex Structures

Sometimes you need to render nested lists or complex data structures. The same principles apply, but you need to handle keys carefully. Nested List Patterns: • Lists within lists • Tables with rows and cells • Nested components with lists • Hierarchical data structures Key Considerations: • Each level needs its own keys • Keys must be unique at their level • Consider component structure • Keep nesting shallow when possible

Code Example:
// Nested lists
function CategoryList({ categories }) {
  return (
    <ul>
      {categories.map(category => (
        <li key={category.id}>
          <h3>{category.name}</h3>
          <ul>
            {category.items.map(item => (
              <li key={item.id}>{item.name}</li>
            ))}
          </ul>
        </li>
      ))}
    </ul>
  );
}

// Table with rows
function DataTable({ data }) {
  return (
    <table>
      <thead>
        <tr>
          <th>Name</th>
          <th>Email</th>
          <th>Role</th>
        </tr>
      </thead>
      <tbody>
        {data.map(row => (
          <tr key={row.id}>
            <td>{row.name}</td>
            <td>{row.email}</td>
            <td>{row.role}</td>
          </tr>
        ))}
      </tbody>
    </table>
  );
}

// Complex nested structure
function CommentThread({ comments }) {
  return (
    <div>
      {comments.map(comment => (
        <div key={comment.id}>
          <Comment comment={comment} />
          {comment.replies && comment.replies.length > 0 && (
            <div className="ml-8">
              {comment.replies.map(reply => (
                <Comment key={reply.id} comment={reply} />
              ))}
            </div>
          )}
        </div>
      ))}
    </div>
  );
}

// Grid layout
function ProductGrid({ products }) {
  return (
    <div className="grid grid-cols-3 gap-4">
      {products.map(product => (
        <ProductCard key={product.id} product={product} />
      ))}
    </div>
  );
}

Nested lists work the same way - each level needs keys. Keep structure clear and consider extracting complex nested components for better readability.

Common Patterns and Best Practices

Following best practices makes your list rendering code more maintainable and performant. Best Practices: • Always use keys for list items • Use unique, stable IDs when possible • Filter before mapping • Extract list items into components • Use useMemo for expensive transformations • Handle empty states • Consider pagination for large lists • Use virtual scrolling for very long lists Common Patterns: • Empty state handling • Loading states • Error states • Pagination • Infinite scroll • Search/filter UI

Code Example:
// ✅ GOOD: Complete list component with all states
function UserList({ users, loading, error }) {
  if (loading) {
    return <div>Loading users...</div>;
  }
  
  if (error) {
    return <div>Error: {error.message}</div>;
  }
  
  if (users.length === 0) {
    return <div>No users found</div>;
  }
  
  return (
    <ul>
      {users.map(user => (
        <UserItem key={user.id} user={user} />
      ))}
    </ul>
  );
}

// ✅ GOOD: Extracted list item component
function TodoList({ todos }) {
  return (
    <ul>
      {todos.map(todo => (
        <TodoItem key={todo.id} todo={todo} />
      ))}
    </ul>
  );
}

function TodoItem({ todo }) {
  return (
    <li className={todo.completed ? 'completed' : ''}>
      <input type="checkbox" checked={todo.completed} />
      <span>{todo.text}</span>
    </li>
  );
}

// ✅ GOOD: Using useMemo for expensive operations
function ExpensiveList({ items, filter }) {
  const filteredItems = useMemo(() => {
    return items
      .filter(item => item.category === filter)
      .sort((a, b) => a.price - b.price);
  }, [items, filter]);
  
  return (
    <div>
      {filteredItems.map(item => (
        <ItemCard key={item.id} item={item} />
      ))}
    </div>
  );
}

// ✅ GOOD: Pagination
function PaginatedList({ items, pageSize = 10 }) {
  const [page, setPage] = useState(1);
  const startIndex = (page - 1) * pageSize;
  const paginatedItems = items.slice(startIndex, startIndex + pageSize);
  
  return (
    <div>
      <ul>
        {paginatedItems.map(item => (
          <li key={item.id}>{item.name}</li>
        ))}
      </ul>
      <div>
        <button onClick={() => setPage(p => p - 1)} disabled={page === 1}>
          Previous
        </button>
        <span>Page {page}</span>
        <button 
          onClick={() => setPage(p => p + 1)} 
          disabled={startIndex + pageSize >= items.length}
        >
          Next
        </button>
      </div>
    </div>
  );
}

Follow best practices: handle all states (loading, error, empty), extract list items into components, use useMemo for expensive operations, and consider pagination for large lists.

Conclusion

Conditional rendering and lists are fundamental to React development. Use conditional rendering to create dynamic UIs, and always provide keys when rendering lists. Filter and transform data before rendering, handle empty states, and extract list items into components for better maintainability. Remember: keys help React efficiently update lists, so use unique, stable IDs whenever possible.