React Functionalities and Their Origins in OOP Design Patterns

67 reads . 8 min read . February 17, 2025

When using React (the modern frontend library), at first everything seems to be straightforward, especially for apps that don’t have too much logic to be done or complex states (Pieces of data that change in response to user interaction) to manage, this makes the code base easily wired in a spaghetti way in case it becomes complex in the future or it is complex from the initial design.

This article, will not only map each functionality with its under-the-hood OOP origin that I believe these patterns are inspired from, but will be mapped to the official React’s code base to prove the theory!

Note:

Provided React source code snippets are based on commit 0a7dc1b in the main branch.

React’s Internal Processes & Key Terms

This list describes the most important terms that are used in the React code base, these terms are uncommon while using React itself, as they are in packages that React depends on!

  • React fiber is a data structure that represents a unit of work for React’s rendering process.
  • React Fiber Loop: watches all kinds of changes, both which are causing re-render and which aren’t, you can find it in ReactFiberWorkLoop.js
  • Reconciliation Phase, reconcile virtual DOM with actual DOM, you can find it in ReactFiber.js
  • Commit phase (Applying Changes, applies updates to actual DOM), you can find it in ReactFiberCommitWork.js

Article Core: React Functionalities

useRef Hook

Definition

The Solved Problem: reference a DOM element without triggering re-renders.

Cons: relies on the actual DOM and doesn’t keep up with virtual DOM updates.

Optimum use cases: Getting updates from highly changeable elements that don’t depend on UI state changes, such as cursor, or scroll position.

function MouseTracker() {
  const mousePosition = useRef({ x: 0, y: 0 });
  useEffect(() => {
		 // actions based on mouse position
  }, []);
  return (
   <div ref={mousePosition}>
     {/* app */}
   </div>
 );
}

Object-Oriented Origin: Singleton + Memento (behavioral and creational patterns)

  • Singleton Pattern: Restricts the instantiation of a class and ensures that only one instance of the class exists in the application.
  • Memento Pattern: Saving the object’s state without getting into the internal structure, the default behavior of updating internal components in react means updating the virtual DOM which will lead to re-rendering, that is what we are trying to avoid.
/* Singleton pattern */
// there is no code implementation here, as the sengelton object is the actual DOM itself

/* Memento pattern */
var origin = new origin { State = "State1" };
var virtualDOM = new virtualDOM(); // the expensive object that should be computarized as few number of times as possible

virtualDOM.Backup(origin);

origin.State = "State2";  
Console.WriteLine($"Current State: {origin.State}"); // State2
Console.WriteLine($"Current State: {virtualDOM.State}"); // State1

origin.State = "State3";  
Console.WriteLine($"Current State: {origin.State}"); // State3
Console.WriteLine($"Current State: {virtualDOM.State}"); // State1

// update only once when necessary
virtualDOM.State = origin.State;
Console.WriteLine($"Current State: {origin.State}"); // State3
Console.WriteLine($"Current State: {virtualDOM.State}"); // State3

Evidence from the React Code Base

Refs are ignored in the reconciliation phase because this phase is responsible for updating the virtual DOM, whereas refs do not require reconciliation. Instead, refs are managed directly in the commit phase to update the actual DOM. In contrast, useState triggers a re-render because React tracks state updates in the fiber tree. When useState is updated, React calls markRootUpdated, adding the update lane to pendingLanes. The scheduler then processes these lanes, leading to reconciliation and updates in the virtual DOM before the commit phase applies changes to the real DOM.

export function markRootUpdated(root: FiberRoot, updateLane: Lane) {
  root.pendingLanes |= updateLane; // bitwise or to combine that lane with other lanes that the scheduler should manage
  // ...
}

useMemo Hook

Definition

The Solved Problem: unnecessary recalculations by memoizing (caching) the result

Cons: higher cost of computations in case of highly changeable dependencies or even large ones.

Optimum use cases: non-expensive dependencies and expensive computations.

function ExpensiveList({ items }) {
  const sortedItems = useMemo(() => {
    return sortNumbers(items);
  }, [items]);

  return <div>Sorted: {sortedItems.join(", ")}</div>;
}

Object-Oriented Origin: Flyweight (creational pattern)

reduces memory usage by reusing existing objects instead of creating new ones.

class Flyweight
{
    private readonly string _intrinsicState;

    public Flyweight(string intrinsicState)
    {
        _intrinsicState = intrinsicState;
    }

    public void Operation(string extrinsicState)
    {
        Console.WriteLine($"Flyweight: {_intrinsicState}, Used with: {extrinsicState}");
    }
}

class FlyweightFactory
{
    private readonly Dictionary<string, Flyweight> _flyweights = new();

    public Flyweight GetFlyweight(string key)
    {
        if (!_flyweights.ContainsKey(key))
        {
            _flyweights[key] = new Flyweight(key);
            Console.WriteLine($"Created new Flyweight: {key}");
        }
        return _flyweights[key];
    }
}

As obvious here, the Flyweight design pattern is about sharing expensive objects not results, these cached objects here are stored in a dictionary of flyweights that are identified by keys, each memorized object in React’s runtime can be identified by a unique key under the hood.

Evidence from the React Code Base

It stores the computed value directly inside the fiber, remember when mentioned before that flyweight patterns may cash flyweights that are identified by keys? here in react computed values are identified by the fiber that is attached to it.

function updateMemo<T>(
  nextCreate: () => T,
  deps: Array<mixed> | void | null,
): T {
	// get the current state duing the render phase
  const hook = updateWorkInProgressHook();
	// get dependencies
  const nextDeps = deps === undefined ? null : deps;
  // get previous state
  const prevState = hook.memoizedState;
  if (nextDeps !== null) { // this means that if there is no dependencies, the calculation will be caluclated on each render
    const prevDeps: Array<mixed> | null = prevState[1];
    // comparision
    if (areHookInputsEqual(nextDeps, prevDeps)) {
      return prevState[0];
    }
  }
  const nextValue = nextCreate();
  
  // ..
  
  hook.memoizedState = [nextValue, nextDeps];
  return nextValue;
}

Context API

Definition

The Solved Problem: Prop Drilling, the classic problem in managing react states.

Cons: it re-renders all nested components inside the context wrapper on each state update which may impact the performance dramatically.

Optimum use cases:

  • Use it only for states that will really require the whole application to re-render.
  • Use many contexts instead of only one wrapping the whole application to reduce the number of re-rendering related components, and this still can impact performance as there exists way too many optimized ways to handle states instead of that.
/* wrapper component */
const ThemeContext = createContext();
function App() {
  const [theme, setTheme] = useState("dark");

  return (
    <ThemeContext.Provider value={theme}>
      <Parent />
    </ThemeContext.Provider>
  );
}

/* nested component */
function GrandChild() {
  const theme = useContext(ThemeContext);
  return <div>Current Theme: {theme}</div>;
}

Object-Oriented Origin: Singleton + Pub/Sub (creational & behavioral patterns)

  • Singleton pattern: restricts the instantiation of a class and ensures that only one instance of the class exists in the application, the same purpose in react of creating one state that can be read by multiple system components instead of creating it multiple times.
  • Pub/Sub pattern: used when multiple components need to react to changes without knowing each other, the same happens in the React components case.
private static ThemeContext? _instance;
private string _theme = "light";


/* Singleton instance */
public static ThemeContext Instance
{
    get
    {
        if (_instance == null)
            _instance = new ThemeContext();
        return _instance;
    }
}

/* Pub/Sub */
public string Theme => _theme;
public void SetTheme(string newTheme)
{
    _theme = newTheme;
    NotifySubscribers();
}
private void NotifySubscribers()
{
    foreach (var subscriber in _subscribers)
    {
        subscriber.OnThemeChanged(_theme);
    }
}

Evidence from the React Code Base

Consumer objects that are called subscribers in the pub/sub pattern are fibers, and then react components that are attached to this fiber consume the shared state, then components are not direct consumers!

export function createContext<T>(defaultValue: T): ReactContext<T> {
  const context: ReactContext<T> = {
    $$typeof: REACT_CONTEXT_TYPE,
    _currentValue: defaultValue,   
    // ...
  };

	// ...
	
  (context: any).Provider = {
    $$typeof: REACT_PROVIDER_TYPE,
    _context: context,
  };
  
  // ...
  
  return context;
}
export function pushProvider<T>(
  providerFiber: Fiber,
  context: ReactContext<T>,
  nextValue: T,
): void {
	// ...
  push(valueCursor, context._currentValue, providerFiber);

  context._currentValue = nextValue;
	// ...
}
function readContextForConsumer<T>(
  consumer: Fiber | null,
  context: ReactContext<T>,
): T {

  const value = context._currentValue;

	// ...

  const contextItem = {
    context: ((context: any): ReactContext<mixed>),
    memoizedValue: value,
    next: null,
  };

  // ...
   
  // This sets a flag on the fiber indicating that it depends on a context value. If the context updates, React will check this flag and re-render the fiber if necessary
  consumer.flags |= NeedsPropagation;
  
  // ...
  
  return value;
}

The first code snippet is the context store, the second is the provider that we create in your daily life from after layers of abstraction <Context.Provider value={newValue}>, and the last code snippet is created when a component uses data from that provider that we create using const theme = useContext(ThemeContext);.

Reflection

  • Do not overuse React’s functionalities, as that will lead to new unsolvable problems instead of solving ones.
  • Get to know how some React snippets work under the hood under layers of abstraction to get more convinced with the optimum use case.
  • Thinking in design patterns is the way to deal with highly organized solutions like the React codebase.
  • OOP is the school of design patterns, same or highly related concepts are implemented with other programming paradigms such as procedural and functional.

Conclusion

I hope the OOP world gets us all inspired and uses the abstraction layers that are provided by modern tools wisely, and look forward to the next level of software engineering, which is understanding how they work under the hood instead of using them blindly.

Back To Top