React Deep Dive

Chapter 2: Introduction to React Fiber

In the previous chapter, we saw how React's recursive rendering process could block the main thread and make the UI unresponsive. React 16 introduced a new reconciliation engine called Fiber to solve these problems. But what exactly is Fiber, and how does it work?

What is Fiber?

Fiber is React's new reconciliation engine that enables:

  • Interruptible rendering
  • Priority-based updates
  • Concurrent rendering
  • Time-slicing

At its core, Fiber is a reimplementation of the stack, but with a crucial difference: it can be interrupted and resumed. Let's understand why this is important.

Why Some Lifecycle Methods Are Marked as UNSAFE

Remember those lifecycle methods with the UNSAFE_ prefix I mentioned in Chapter 1? Now we can understand why they were marked as unsafe. The methods in question are:

  • componentWillMount
  • componentWillReceiveProps
  • componentWillUpdate

These methods were marked as unsafe because Fiber's interruptible nature means they might be called multiple times during a single update. Here's why:

1// Example of a problematic lifecycle method
2class Component extends React.Component {
3  componentWillMount() {
4      // This might be called multiple times
5      // due to Fiber's interruptible rendering
6      this.setState({ count: this.state.count + 1 });
7  }
8}

In the old stack-based renderer, these methods were guaranteed to be called exactly once per update. But with Fiber:

  1. Rendering can be interrupted and resumed
  2. The same update might be processed multiple times
  3. These lifecycle methods could be called multiple times
  4. This could lead to unexpected side effects and bugs

This is why React team:

  1. Added the UNSAFE_ prefix to warn developers
  2. Introduced new lifecycle methods that are safe with interruptible rendering
  3. Recommended using the new methods instead

The Stack vs Fiber

In the previous chapter, we saw this recursive render function:

1function render(element, container) {
2  const dom =
3    element.type == "TEXT_ELEMENT"
4      ? document.createTextNode("")
5      : document.createElement(element.type)
6
7  const isProperty = key => key !== "children"
8  Object.keys(element.props)
9    .filter(isProperty)
10    .forEach(name => {
11      dom[name] = element.props[name]
12    })
13
14  element.props.children.forEach(child =>
15    render(child, dom)
16  )
17
18  container.appendChild(dom)
19}

This implementation uses the JavaScript call stack, which means:

  1. Each recursive call adds a new frame to the stack
  2. The stack must be completely unwound before we can do anything else
  3. We can't pause in the middle of rendering

Fiber replaces this with a linked list of fiber nodes. Here's a simplified version of what a fiber node looks like:

1type Fiber = {
2  type: any,                    // The type of element (div, span, etc.)
3  props: any,                   // The props
4  dom: HTMLElement | null,      // The actual DOM node
5  parent: Fiber | null,         // Parent fiber
6  child: Fiber | null,          // First child fiber
7  sibling: Fiber | null,        // Next sibling fiber
8  alternate: Fiber | null,      // The fiber from the previous render
9  effectTag: string,            // What to do with this fiber (PLACEMENT, UPDATE, DELETION)
10  lanes: number,                // Priority of this update
11}

How Fiber Works

Instead of recursively calling the render function, Fiber breaks the work into small units and processes them one at a time. Here's a simplified version of how it works:

1function performUnitOfWork(fiber) {
2  // 1. Create DOM node
3  if (!fiber.dom) {
4      fiber.dom = createDom(fiber)
5  }
6
7  // 2. Create new fibers for children
8  const elements = fiber.props.children
9  let index = 0
10  let prevSibling = null
11
12  while (index < elements.length) {
13      const element = elements[index]
14      const newFiber = {
15          type: element.type,
16          props: element.props,
17          parent: fiber,
18          dom: null,
19      }
20
21      // Link fibers together
22      if (index === 0) {
23          fiber.child = newFiber
24      } else {
25          prevSibling.sibling = newFiber
26      }
27
28      prevSibling = newFiber
29      index++
30  }
31
32  // 3. Return next unit of work
33  if (fiber.child) {
34      return fiber.child
35  }
36  let nextFiber = fiber
37  while (nextFiber) {
38      if (nextFiber.sibling) {
39          return nextFiber.sibling
40      }
41      nextFiber = nextFiber.parent
42  }
43}

The key differences from the recursive approach are:

  1. Work is broken into small units
  2. Each unit can be processed independently
  3. We can pause and resume work at any point
  4. The browser can handle high-priority tasks between units

The Work Loop

Fiber uses a work loop to process these units. For this example, we'll use requestIdleCallback to demonstrate the concept, but React actually implements its own scheduler for better control and browser compatibility:

1function workLoop(deadline) {
2  let shouldYield = false
3  while (nextUnitOfWork && !shouldYield) {
4      nextUnitOfWork = performUnitOfWork(nextUnitOfWork)
5      shouldYield = deadline.timeRemaining() < 1
6  }
7  requestIdleCallback(workLoop)
8}
9
10requestIdleCallback(workLoop)

This loop:

  1. Processes one unit of work at a time
  2. Checks if there's time remaining in the current frame
  3. Yields to the browser if needed
  4. Continues with the next unit in the next frame

In React's actual implementation, the scheduler is much more sophisticated:

  • It handles priority levels for different types of updates
  • It can interrupt and resume work based on priority
  • It manages time slicing across multiple frames
  • It provides better browser compatibility than requestIdleCallback

We'll explore React's scheduler in more detail in Chapter 4 when we discuss priority management.

Benefits of Fiber

This new architecture enables several important features:

  1. Concurrent Rendering: React can work on multiple versions of the UI simultaneously
  2. Time Slicing: Work is broken into small chunks that can be spread across multiple frames
  3. Priority-based Updates: Different updates can have different priorities
  4. Suspense: Components can "suspend" while waiting for data
  5. Error Boundaries: Better error handling with the ability to recover from errors

In the next chapter, we'll dive deeper into how Fiber handles updates and reconciliation.