Skip to content
intermediate Phase 7 · React Fundamentals

React Components & JSX

Build component-based UIs with JSX, props, composition patterns, and component lifecycle.

1h 30m
0 problems
Topic Progress 0%

JSX and Component Patterns

JSX and Component Patterns

JSX Fundamentals

function App() {
  const user = { name: 'Alice', role: 'admin' };
  const items = ['React', 'TypeScript', 'Node.js'];

  return (
    <div className="app">
      <h1>Welcome, {user.name}</h1>
      <p>Role: {user.role === 'admin' ? 'Administrator' : 'User'}</p>
      <ul>
        {items.map(item => (
          <li key={item}>{item}</li>
        ))}
      </ul>
    </div>
  );
}

Component Composition

// Props with defaults
function Card({ title, children, variant = 'default', className = '' }) {
  return (
    <div className={`card card-${variant} ${className}`}>
      {title && <div className="card-header"><h3>{title}</h3></div>}
      <div className="card-body">{children}</div>
    </div>
  );
}

// Layout component
function Layout({ children, sidebar }) {
  return (
    <div className="layout">
      <header className="layout-header">Logo</header>
      <div className="layout-content">
        {sidebar && <aside className="layout-sidebar">{sidebar}</aside>}
        <main className="layout-main">{children}</main>
      </div>
      <footer className="layout-footer">Footer</footer>
    </div>
  );
}

// Usage
<Layout sidebar={<Navigation />}>
  <Card title="Dashboard">
    <p>Welcome back!</p>
  </Card>
</Layout>

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} className="tracker">
      {render(position)}
    </div>
  );
}

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

Compound Components

const TabsContext = createContext();

function Tabs({ children, defaultTab }) {
  const [activeTab, setActiveTab] = useState(defaultTab);
  return (
    <TabsContext.Provider value={{ activeTab, setActiveTab }}>
      <div className="tabs">{children}</div>
    </TabsContext.Provider>
  );
}

function TabList({ children }) {
  return <div className="tab-list" role="tablist">{children}</div>;
}

function Tab({ value, children }) {
  const { activeTab, setActiveTab } = useContext(TabsContext);
  return (
    <button
      role="tab"
      aria-selected={activeTab === value}
      className={activeTab === value ? 'active' : ''}
      onClick={() => setActiveTab(value)}
    >
      {children}
    </button>
  );
}

function TabPanel({ value, children }) {
  const { activeTab } = useContext(TabsContext);
  return activeTab === value ? <div role="tabpanel">{children}</div> : null;
}

// Usage — clean API
<Tabs defaultTab="overview">
  <TabList>
    <Tab value="overview">Overview</Tab>
    <Tab value="details">Details</Tab>
  </TabList>
  <TabPanel value="overview"><OverviewContent /></TabPanel>
  <TabPanel value="details"><DetailsContent /></TabPanel>
</Tabs>

Forms and User Input

Forms and User Input

Controlled Components

function LoginForm() {
  const [formData, setFormData] = useState({ email: '', password: '' });
  const [errors, setErrors] = useState({});

  const handleChange = (e) => {
    const { name, value } = e.target;
    setFormData(prev => ({ ...prev, [name]: value }));
    // Clear error when user types
    if (errors[name]) setErrors(prev => ({ ...prev, [name]: '' }));
  };

  const validate = () => {
    const newErrors = {};
    if (!formData.email) newErrors.email = 'Email required';
    else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) newErrors.email = 'Invalid email';
    if (!formData.password) newErrors.password = 'Password required';
    else if (formData.password.length < 8) newErrors.password = 'Min 8 characters';
    return newErrors;
  };

  const handleSubmit = async (e) => {
    e.preventDefault();
    const validationErrors = validate();
    if (Object.keys(validationErrors).length > 0) {
      setErrors(validationErrors);
      return;
    }
    try {
      const res = await fetch('/api/login', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(formData),
      });
      const data = await res.json();
      if (data.token) navigate('/dashboard');
    } catch (err) {
      setErrors({ submit: 'Login failed' });
    }
  };

  return (
    <form onSubmit={handleSubmit} noValidate>
      <div className="field">
        <label htmlFor="email">Email</label>
        <input
          type="email" id="email" name="email"
          value={formData.email} onChange={handleChange}
          aria-invalid={!!errors.email} aria-describedby={errors.email ? 'email-error' : undefined}
        />
        {errors.email && <span id="email-error" className="error">{errors.email}</span>}
      </div>
      <div className="field">
        <label htmlFor="password">Password</label>
        <input
          type="password" id="password" name="password"
          value={formData.password} onChange={handleChange}
        />
        {errors.password && <span className="error">{errors.password}</span>}
      </div>
      {errors.submit && <p className="error">{errors.submit}</p>}
      <button type="submit">Sign In</button>
    </form>
  );
}

Custom Form Hook

function useForm(initialValues, validateFn) {
  const [values, setValues] = useState(initialValues);
  const [errors, setErrors] = useState({});

  const handleChange = (e) => {
    const { name, value, type, checked } = e.target;
    setValues(prev => ({
      ...prev,
      [name]: type === 'checkbox' ? checked : value,
    }));
  };

  const handleSubmit = async (onSubmit) => {
    const validationErrors = validateFn(values);
    setErrors(validationErrors);
    if (Object.keys(validationErrors).length === 0) {
      await onSubmit(values);
    }
  };

  return { values, errors, handleChange, handleSubmit };
}

// Usage
const { values, errors, handleChange, handleSubmit } = useForm(
  { email: '', password: '' },
  (values) => {
    const errors = {};
    if (!values.email) errors.email = 'Required';
    return errors;
  }
);

Component Lifecycle and Side Effects

Component Lifecycle and Side Effects

useEffect Patterns

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);

  // Data fetching
  useEffect(() => {
    let cancelled = false;
    setLoading(true);

    fetch(`/api/users/${userId}`)
      .then(res => res.json())
      .then(data => {
        if (!cancelled) setUser(data);
      })
      .finally(() => {
        if (!cancelled) setLoading(false);
      });

    return () => { cancelled = true; };  // cleanup
  }, [userId]);  // re-run when userId changes

  // Document title
  useEffect(() => {
    document.title = user ? `${user.name} - Profile` : 'Loading...';
  }, [user]);

  // Event listeners
  useEffect(() => {
    const handleResize = () => setWidth(window.innerWidth);
    window.addEventListener('resize', handleResize);
    return () => window.removeEventListener('resize', handleResize);
  }, []);

  if (loading) return <Spinner />;
  return <div>{user.name}</div>;
}

Custom Hooks

function useLocalStorage(key, initialValue) {
  const [value, setValue] = useState(() => {
    try {
      const stored = localStorage.getItem(key);
      return stored ? JSON.parse(stored) : initialValue;
    } catch {
      return initialValue;
    }
  });

  useEffect(() => {
    localStorage.setItem(key, JSON.stringify(value));
  }, [key, value]);

  return [value, setValue];
}

function useDebounce(value, delay) {
  const [debouncedValue, setDebouncedValue] = useState(value);

  useEffect(() => {
    const timer = setTimeout(() => setDebouncedValue(value), delay);
    return () => clearTimeout(timer);
  }, [value, delay]);

  return debouncedValue;
}

// Usage
function SearchPage() {
  const [query, setQuery] = useLocalStorage('search', '');
  const debouncedQuery = useDebounce(query, 300);
  // debouncedQuery updates 300ms after user stops typing
}

useReducer for Complex State

const todoReducer = (state, action) => {
  switch (action.type) {
    case 'ADD':
      return [...state, { id: crypto.randomUUID(), text: action.text, done: false }];
    case 'TOGGLE':
      return state.map(t => t.id === action.id ? { ...t, done: !t.done } : t);
    case 'DELETE':
      return state.filter(t => t.id !== action.id);
    case 'REORDER':
      const items = [...state];
      const [moved] = items.splice(action.from, 1);
      items.splice(action.to, 0, moved);
      return items;
    default:
      return state;
  }
};

function TodoList() {
  const [todos, dispatch] = useReducer(todoReducer, []);
  
  return (
    <div>
      <AddTodo onAdd={(text) => dispatch({ type: 'ADD', text })} />
      <ul>
        {todos.map((todo, i) => (
          <TodoItem
            key={todo.id}
            todo={todo}
            onToggle={() => dispatch({ type: 'TOGGLE', id: todo.id })}
            onDelete={() => dispatch({ type: 'DELETE', id: todo.id })}
          />
        ))}
      </ul>
    </div>
  );
}

Performance Optimization

Performance Optimization

React.memo and useMemo

// Prevent re-renders when props haven't changed
const ExpensiveList = React.memo(function ExpensiveList({ items }) {
  console.log('ExpensiveList rendered');
  return (
    <ul>
      {items.map(item => (
        <li key={item.id}>{item.name} - {item.price.toFixed(2)}</li>
      ))}
    </ul>
  );
});

function ProductPage({ products }) {
  // Memoize expensive computations
  const sortedProducts = useMemo(() => {
    return [...products].sort((a, b) => a.price - b.price);
  }, [products]);

  const totalValue = useMemo(() => {
    return products.reduce((sum, p) => sum + p.price * p.quantity, 0);
  }, [products]);

  return (
    <div>
      <p>Total: ${totalValue.toFixed(2)}</p>
      <ExpensiveList items={sortedProducts} />
    </div>
  );
}

useCallback for Stable References

function TodoApp() {
  const [todos, setTodos] = useState([]);

  // Without useCallback, new function reference on every render
  // causes child components to re-render
  const addTodo = useCallback((text) => {
    setTodos(prev => [...prev, { id: Date.now(), text, done: false }]);
  }, []);

  const toggleTodo = useCallback((id) => {
    setTodos(prev => prev.map(t => t.id === id ? { ...t, done: !t.done } : t));
  }, []);

  return (
    <div>
      <AddTodoForm onAdd={addTodo} />
      <TodoList todos={todos} onToggle={toggleTodo} />
    </div>
  );
}

Virtualization for Long Lists

import { FixedSizeList } from 'react-window';

function VirtualizedList({ items }) {
  const Row = ({ index, style }) => (
    <div style={style} className="list-row">
      {items[index].name}
    </div>
  );

  return (
    <FixedSizeList
      height={600}
      width="100%"
      itemCount={items.length}
      itemSize={50}
    >
      {Row}
    </FixedSizeList>
  );
}

Key Takeaways

  • Use React.memo for pure presentational components that receive the same props
  • Use useMemo for expensive calculations
  • Use useCallback for functions passed as props to memoized children
  • Use virtualization for lists with hundreds or thousands of items
  • Profile with React DevTools before optimizing — measure first