React Elements and Rendering

React Elements and Rendering

Understanding how React creates and renders elements is fundamental to building React applications. Let's explore how React elements work and how they get rendered to the DOM.

React Elements

React elements are the smallest building blocks of React applications. They describe what you want to see on the screen.

Creating Elements

// Using JSX
const element = <h1>Hello, world!</h1>;

// Without JSX (what JSX compiles to)
const element = React.createElement(
  'h1',
  null,
  'Hello, world!'
);

Element Structure

React elements are plain objects:

// The JSX <h1>Hello</h1> creates this object:
{
  type: 'h1',
  props: {
    children: 'Hello'
  }
}

Rendering Elements

To render a React element into the DOM, you need a "root" DOM node:

<!-- In your HTML file -->
<div id="root"></div>
// In your JavaScript file
import React from 'react';
import ReactDOM from 'react-dom/client';

const element = <h1>Hello, world!</h1>;

// Create a root
const root = ReactDOM.createRoot(
  document.getElementById('root')
);

// Render the element
root.render(element);

Updating Rendered Elements

React elements are immutable. Once created, you can't change their children or attributes. To update the UI, create a new element and render it:

const root = ReactDOM.createRoot(
  document.getElementById('root')
);

function tick() {
  const element = (
    <div>
      <h1>Hello, world!</h1>
      <h2>It is {new Date().toLocaleTimeString()}.</h2>
    </div>
  );
  root.render(element);
}

setInterval(tick, 1000);

React Only Updates What's Necessary

React DOM compares the element and its children to the previous one, and only applies the DOM updates necessary to bring the DOM to the desired state.

// Even though we create a new element every second,
// React DOM only updates the time text node
function Clock() {
  const [time, setTime] = React.useState(new Date());
  
  React.useEffect(() => {
    const timer = setInterval(() => {
      setTime(new Date());
    }, 1000);
    
    return () => clearInterval(timer);
  }, []);
  
  return (
    <div>
      <h1>Static Title</h1>
      <p>Current time: {time.toLocaleTimeString()}</p>
    </div>
  );
}

Component Elements

Elements can also represent user-defined components:

// Component definition
function Welcome(props) {
  return <h1>Hello, {props.name}</h1>;
}

// Component element
const element = <Welcome name="Sara" />;

// This creates:
{
  type: Welcome,
  props: {
    name: 'Sara'
  }
}

The Rendering Process

1. Initial Render

function App() {
  return (
    <div>
      <Header />
      <Content />
      <Footer />
    </div>
  );
}

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);

2. Virtual DOM

React creates a virtual representation of the DOM:

// Virtual DOM representation
{
  type: 'div',
  props: {
    children: [
      { type: Header, props: {} },
      { type: Content, props: {} },
      { type: Footer, props: {} }
    ]
  }
}

3. Reconciliation

When state changes, React:

  1. Creates a new virtual DOM tree
  2. Compares it with the previous virtual DOM tree
  3. Calculates the minimum set of changes
  4. Updates only the changed parts in the real DOM

Conditional Rendering

Render different elements based on conditions:

function UserGreeting({ isLoggedIn }) {
  if (isLoggedIn) {
    return <h1>Welcome back!</h1>;
  }
  return <h1>Please sign up.</h1>;
}

// Using ternary operator
function Greeting({ isLoggedIn }) {
  return (
    <div>
      {isLoggedIn ? (
        <UserDashboard />
      ) : (
        <LoginForm />
      )}
    </div>
  );
}

Rendering Lists

Render multiple elements from arrays:

function NumberList({ numbers }) {
  return (
    <ul>
      {numbers.map((number) => (
        <li key={number.toString()}>
          {number}
        </li>
      ))}
    </ul>
  );
}

const numbers = [1, 2, 3, 4, 5];
root.render(<NumberList numbers={numbers} />);

Keys in Lists

Keys help React identify which items have changed:

function TodoList({ todos }) {
  return (
    <ul>
      {todos.map((todo) => (
        <li key={todo.id}>
          {todo.text}
        </li>
      ))}
    </ul>
  );
}

// Keys must be unique among siblings
const todos = [
  { id: 1, text: 'Learn React' },
  { id: 2, text: 'Build an app' },
  { id: 3, text: 'Deploy to production' }
];

Rendering Nothing

Components can return null to render nothing:

function WarningBanner({ warn }) {
  if (!warn) {
    return null;
  }
  
  return (
    <div className="warning">
      Warning!
    </div>
  );
}

Portals

Render children into a DOM node outside the parent component:

function Modal({ children }) {
  return ReactDOM.createPortal(
    children,
    document.getElementById('modal-root')
  );
}

function App() {
  return (
    <div>
      <h1>My App</h1>
      <Modal>
        <p>This is rendered outside the app root!</p>
      </Modal>
    </div>
  );
}

Performance Considerations

1. Batching Updates

React batches multiple state updates for performance:

function Counter() {
  const [count, setCount] = useState(0);
  
  const handleClick = () => {
    // These updates are batched
    setCount(c => c + 1);
    setCount(c => c + 1);
    setCount(c => c + 1);
    // Results in one re-render, not three
  };
  
  return <button onClick={handleClick}>Count: {count}</button>;
}

2. React.memo

Prevent unnecessary re-renders:

const ExpensiveComponent = React.memo(function ExpensiveComponent({ data }) {
  return <div>{/* Complex rendering logic */}</div>;
});

3. Lazy Loading

Load components only when needed:

const HeavyComponent = React.lazy(() => import('./HeavyComponent'));

function App() {
  return (
    <React.Suspense fallback={<div>Loading...</div>}>
      <HeavyComponent />
    </React.Suspense>
  );
}

Debugging Rendering

React DevTools

Use React DevTools to:

  • Inspect component tree
  • Track renders
  • Profile performance

Console Logging

function MyComponent({ prop }) {
  console.log('MyComponent rendered');
  
  return <div>{prop}</div>;
}

Best Practices

  1. Keep components pure - Same props should always produce same output
  2. Avoid direct DOM manipulation - Let React handle the DOM
  3. Use keys properly - Stable, unique keys for list items
  4. Minimize re-renders - Use React.memo, useMemo, useCallback
  5. Batch state updates - Group related state changes
  6. Lazy load heavy components
  7. Profile performance - Use React DevTools Profiler

Common Pitfalls

1. Missing Keys

// ❌ Bad - using index as key
items.map((item, index) => <li key={index}>{item}</li>)

// ✅ Good - using stable, unique ID
items.map(item => <li key={item.id}>{item.name}</li>)

2. Modifying State Directly

// ❌ Bad
state.items.push(newItem);

// ✅ Good
setState([...state.items, newItem]);

3. Expensive Operations in Render

// ❌ Bad
function Component() {
  const expensiveValue = calculateExpensiveValue(); // Runs every render
  return <div>{expensiveValue}</div>;
}

// ✅ Good
function Component() {
  const expensiveValue = useMemo(() => calculateExpensiveValue(), []);
  return <div>{expensiveValue}</div>;
}

Next Steps

Now that you understand rendering in React, you're ready to learn about:

  • Components and props
  • State management
  • Event handling
  • Lifecycle methods and hooks