DEV Community

Cover image for The Zen of Preact's source code
PuruVJ
PuruVJ

Posted on • Originally published at puruvj.dev

The Zen of Preact's source code

Artwork by Fernando Jorge

Read in light, dark or sepia theme

Preact is [web dev]household name at this point. Almost every web developer who's been in this business for longer than 2 years has heard of it and maybe even tried it themselves. And probably reached the same conclusion as me: It's awesome!! 😻.

So today, I'm gonna do a deep dive into Preact's source code, and remark on some interesting things I find there.

What is Preact?

In case you're not familiar, Preact is the 3KB alternative to the 42KB of React, by Jason Miller. It's fully compatible with React's API and supports all packages that rely on React. Its awesome that way.

Observations

Before we look at the code, I'll remark on some things about Preact.

Written in TypeScript, but not quite...

Preact's source code is written in TypeScript, but the main files themselves aren't. The main files with the functionality are written in plain JavaScript, but they use JSDoc to pull in Types from TypeScript Definition files (.d.ts).

An example:

This is the types.d.ts file:

export type RenamedType = number | null;
Enter fullscreen mode Exit fullscreen mode

And here's the JS file

/**
 * @param {import('./types').RenamedType} a
 * @param {import('./types').RenamedType} b
 * @returns
 */
function sum(a, b) {
  return a + b;
}
Enter fullscreen mode Exit fullscreen mode

As you can see, the JavaScript code is just that: JavaScript. You won't see TypeScript style type specified in it. Rather all type information is specified in comments, which are ignored completely. There's a whole article about Using TypeScript without TypeScript, but the TLDR; here would be: Avoid development time tooling. If its just plain JS, you don't need to run a file watcher to transpile files as you change them. Just run what you got. And you already got a TypeScript compiler running all the time without you explicitly running it: Your VSCode.

This is a very interesting approach and I see more and more libraries take it up, especially non-UI libraries(For UI libraries, you already got a web server running, so adding in TypeScript in the tooling won't change much, go ahead and add TypeScript)

Very well written code

I don't need to say this out loud really, but Preact's source code is very very well written and commented, as you'd expect from such a paramount framework.

It reuses itself a lot

Yup. One of the reasons Preact is so small is that it reuses it's own exported function in its other exported functions. A LOTT!! I'll show you some places where this happens

Disclaimer

This is not gonna be a complete breakdown, and won't be sequential. Preact is quite a big library to cover in a blog post, so I'll just cover the interesting parts.

So, let's begin!! We'll look at some interesting things in the core module(i.e., the one when you type import {} from 'preact'), then we'll get to hooks

Core module

index.js

As is the tradition, let's start with the index.js file:

export { render, hydrate } from './render';
export {
  createElement,
  createElement as h,
  Fragment,
  createRef,
  isValidElement,
} from './create-element';
export { Component } from './component';
export { cloneElement } from './clone-element';
export { createContext } from './create-context';
export { toChildArray } from './diff/children';
export { default as options } from './options';
Enter fullscreen mode Exit fullscreen mode

Notable points: h, which is Preact's JSX factory, is actually named createElement. Just like React.createElement. But is exported as h because it allows you to write raw Preact(Without JSX), also because it was initially inspired from HyperScript 👇

h('div', { class: 'haha' }, h('span', { key: 34 }, h('h1', {}, h('span', {}, 'Whoa'))));
Enter fullscreen mode Exit fullscreen mode

Also it is notable that it exports createElement as it is too, to maintain compatibility with React.createElement

create-element.js

import options from './options';

export function createElement(type, props, children) {
  /*...*/
}

export function createVNode(type, props, key, ref, original) {
  /*...*/
}

export function createRef() {
  return { current: null };
}

export function Fragment(props) {
  return props.children;
}

export const isValidElement = (vnode) => vnode != null && vnode.constructor === undefined;
Enter fullscreen mode Exit fullscreen mode

Omitted createElement and createVNode as they're quite big.

createRef

Let me blow your mind. refs in P/React are basically used to encapsulate values that shouldn't trigger re-renders and are not re-created on every re-render. Lets see how Preact defines it:

export function createRef() {
  return { current: null };
}
Enter fullscreen mode Exit fullscreen mode

A ref is just an object with current property set to null. It's always advertised as that, but I never thought that it's actually an object internally too.

A little clip of me when I found this out 👇

Astonished

Fragment

Next up, we have Fragment. Its also another astonishing thing.

export function Fragment(props) {
  return props.children;
}
Enter fullscreen mode Exit fullscreen mode

Fragment, just returns its children. That's all! 🤯🤯

I knew that's what it's supposed to do, but I always pictured some complex code. Didn't realise that it was just this super simple thing.

isValidElement

/**
 * Check if a the argument is a valid Preact VNode.
 * @param {*} vnode
 * @returns {vnode is import('./internal').VNode}
 */
export const isValidElement = (vnode) => vnode != null && vnode.constructor === undefined;
Enter fullscreen mode Exit fullscreen mode

Simply checking if the current Virtual DOM Node passed to it is valid or not. Again, one liner, super small, but here's a pattern I found out by looking at this code only. Notice @returns {vnode is import('./internal').VNode} in JSDoc. The code is basically using type guards. Right in the JSDoc. I haven't seen this pattern before, which is all the more proof that reading code written by those smarter than you can make you a better dev.

render.js

Remember the index.jsx file, where you initialize your Preact app

import { render, h } from 'preact';
import App from './App';

render(<App />, document.querySelector('#app'));
Enter fullscreen mode Exit fullscreen mode

This is the render function 👇

export function render(vnode, parentDom, replaceNode) {
  if (options._root) options._root(vnode, parentDom);

  // We abuse the `replaceNode` parameter in `hydrate()` to signal if we are in
  // hydration mode or not by passing the `hydrate` function instead of a DOM
  // element..
  let isHydrating = typeof replaceNode === 'function';

  // To be able to support calling `render()` multiple times on the same
  // DOM node, we need to obtain a reference to the previous tree. We do
  // this by assigning a new `_children` property to DOM nodes which points
  // to the last rendered tree. By default this property is not present, which
  // means that we are mounting a new tree for the first time.
  let oldVNode = isHydrating ? null : (replaceNode && replaceNode._children) || parentDom._children;

  vnode = ((!isHydrating && replaceNode) || parentDom)._children = createElement(Fragment, null, [
    vnode,
  ]);

  // List of effects that need to be called after diffing.
  let commitQueue = [];
  diff(
    parentDom,
    // Determine the new vnode tree and store it on the DOM element on
    // our custom `_children` property.
    vnode,
    oldVNode || EMPTY_OBJ,
    EMPTY_OBJ,
    parentDom.ownerSVGElement !== undefined,
    !isHydrating && replaceNode
      ? [replaceNode]
      : oldVNode
      ? null
      : parentDom.firstChild
      ? EMPTY_ARR.slice.call(parentDom.childNodes)
      : null,
    commitQueue,
    !isHydrating && replaceNode ? replaceNode : oldVNode ? oldVNode._dom : parentDom.firstChild,
    isHydrating
  );

  // Flush all queued effects
  commitRoot(commitQueue, vnode);
}

export function hydrate(vnode, parentDom) {
  render(vnode, parentDom, hydrate);
}
Enter fullscreen mode Exit fullscreen mode

First off, very well commented.

From how well I can make sense of the situation here, render function is basically making a commitQueue to store all the changes needed to be done. next, the diff function is taking in the old VNode and the new VNode, making sense of situation and figuring out which DOM Nodes need to be updated, and populating commitQueue.

Then its basically committing these changes. Its just like how we do it in Database. We perform some operation in batch, the commit, so they all get applied one by one at the same time.

I would love to cover diff in the blog post too, but its so big it has its own 500 lines long file 😵. All you have to know, its job is to figure out which DOM Nodes need to be updated and which to keep the same.

hydrate

This function is very interesting, as it nothing but calling the render function. But something even more interesting, its passing along itself as the 3rd argument. And if you look again at render function, it actually has an if condition looking if the function passed to it is named hydrate. Heck there's even a comment about abusing the 3rd argument 😂. These people are way too smart!!

I'm probably exhausting my repeat limit, but darn!! Preact's reuse of itself is really, darn good!!!

create-context.js

This one will probably excite you, as Context is a very, very loved API by a majority of P/React developers. This wasn't always the case, but the useContext hooks made it very easy to use context. Way too easy!!

const { lemonsCount, setLemonsCount } = useContext(lemonsContext);
Enter fullscreen mode Exit fullscreen mode
import { enqueueRender } from './component';

export let i = 0;

export function createContext(defaultValue, contextId) {
  contextId = '__cC' + i++;

  const context = {
    _id: contextId,
    _defaultValue: defaultValue,
    /** @type {import('./internal').FunctionComponent} */
    Consumer(props, contextValue) {
      return props.children(contextValue);
    },
    /** @type {import('./internal').FunctionComponent} */
    Provider(props) {
      if (!this.getChildContext) {
        let subs = [];
        let ctx = {};
        ctx[contextId] = this;

        this.getChildContext = () => ctx;

        this.shouldComponentUpdate = function (_props) {
          if (this.props.value !== _props.value) {
            subs.some(enqueueRender);
          }
        };

        this.sub = (c) => {
          subs.push(c);
          let old = c.componentWillUnmount;
          c.componentWillUnmount = () => {
            subs.splice(subs.indexOf(c), 1);
            if (old) old.call(c);
          };
        };
      }

      return props.children;
    },
  };

  // Devtools needs access to the context object when it
  // encounters a Provider. This is necessary to support
  // setting `displayName` on the context object instead
  // of on the component itself. See:
  // https://reactjs.org/docs/context.html#contextdisplayname

  return (context.Provider._contextRef = context.Consumer.contextType = context);
}
Enter fullscreen mode Exit fullscreen mode

This file, this small file, is all there's to the core context API. These 42 lines do so much(Comments excluded).

So, let's inspect Consumer. Go back a long time back and remember we used to use Consumer to access context data.

This is how it looks

<Consumer>{(data) => <div>Hello {data}</div>}</Consumer>
Enter fullscreen mode Exit fullscreen mode

This looks pretty manageable, but it could get worse when your code grew.

So, if we look at the code of Consumer, it's just this:

Consumer(props, contextValue) {
  return props.children(contextValue);
},
Enter fullscreen mode Exit fullscreen mode

That's it!! Its expecting its children to be a function, and it's simply calling it with the context data. Suddenly the Consumer pattern example above makes sense 🤯🤯.

As for Provider, what it's doing mostly is modifying its parent component's lifecycle hooks to watch for context state changes.

Lastly, there's the return statement at the bottom. The last line is big mutation trick that is used often while coding classical languages like C, C++, Java etc, that is, returning a variable and mutating it at the same time. Here, it is mutating it for the sake of Preact devtools, so as to show the displayName in devtools, as React Devtools do.

And now, its time for the section you probably came here for entirely: HOOKS!!

Hooks

So, first off, Hooks are located in a separate directory. Unlike React, everything is opt-in in Preact, which makes the Minimalist in me rejoice. There's intentionality in every thing you do here. I 😍 that.

So, let's start off with the very, very first hook you ever encountered: useState

But Beware, a twist lies here 😈

useState

This, is useState:

export function useState(initialState) {
  currentHook = 1;
  return useReducer(invokeOrReturn, initialState);
}
Enter fullscreen mode Exit fullscreen mode

Wait, what!?!?

Mindblown right? As you can see, useState is basically calling useReducer, which is another standard React hook. So basically, useState is just an alias of useReducer, you could say.

The variables invokeOrReturn and currentHook are defined in the same file, in the module scope and managed by Preact.

And lemme give you another nugget. See the currentHook = 1 expression? Guess what: It's not needed in the core functionality. It exists solely for Preact Devtools. That is, if Devtools weren't a consideration, this code might as well have been:

const useState = (initialState) => useReducer(invokeOrReturn, initialState);
Enter fullscreen mode Exit fullscreen mode

Literally a one liner!! 🤯🤯🤯🤯

Again, intense focus on the whole self-reusing thing I keep repeating.

All the heavy lifting here is done by the useReducer, so let's look at it next.

useReducer

export function useReducer(reducer, initialState, init) {
  /** @type {import('./internal').ReducerHookState} */
  const hookState = getHookState(currentIndex++, 2);
  hookState._reducer = reducer;
  if (!hookState._component) {
    hookState._value = [
      !init ? invokeOrReturn(undefined, initialState) : init(initialState),

      (action) => {
        const nextValue = hookState._reducer(hookState._value[0], action);
        if (hookState._value[0] !== nextValue) {
          hookState._value = [nextValue, hookState._value[1]];
          hookState._component.setState({});
        }
      },
    ];

    hookState._component = currentComponent;
  }

  return hookState._value;
}
Enter fullscreen mode Exit fullscreen mode

I'll admit I don't fully understand what's going on here 😅, but something that caught my eye here: Look at the hookState._value = [ declaration inside the if block. Its an array with 2 elements. 1st element is simply a value. 2nd one is a function.

Wait a sec. 1st element a value, 2nd element a function...

Holy smokes!!! Its the [state, setState] pair returned from useState 😵😵

const [state, setState] = useState(Infinity); // 😈
Enter fullscreen mode Exit fullscreen mode

if that didn't blow your brains apart, I dunno what will.

Next up: The 2nd most famous hook!

useEffect

export function useEffect(callback, args) {
  /** @type {import('./internal').EffectHookState} */
  const state = getHookState(currentIndex++, 3);
  if (!options._skipEffects && argsChanged(state._args, args)) {
    state._value = callback;
    state._args = args;

    currentComponent.__hooks._pendingEffects.push(state);
  }
}
Enter fullscreen mode Exit fullscreen mode

Aha!!! Notice the if block here. We're checking for 2 things.

!options._skipEffects - Preact has an options config, where you can turn off all side effects from running. So to run this useEffect, we have to make sure its safe to run effects.

  1. argsChanged(state._args, args): This one is very interesting. Remember the 2nd argument you pass to useEffect?
useEffect(() => {
  /* Do epic shit */
}, [emojiUpdated]);
Enter fullscreen mode Exit fullscreen mode

Guess what, argsChanged is the function responsible for checking if changes were made in the dependencies passed to useEffect. Here, we pass it state._args, the argument list maintained by Preact for this specific hook, and the 2nd argument is the new set of dependencies. If any changes are detected, this function returns true, and the effect is run again.

As for argsChanged function, its simply this 👇

function argsChanged(oldArgs, newArgs) {
  return (
    !oldArgs ||
    oldArgs.length !== newArgs.length ||
    newArgs.some((arg, index) => arg !== oldArgs[index])
  );
}
Enter fullscreen mode Exit fullscreen mode

Its basically checking if oldArgs even exist or not at first. Why?

Cuz the dependency list passed to useEffect itself could be a state holding an array.

const [deps, setDeps] = useState([]);

useEffect(() => {
  /* Do epic shit */
}, deps);
Enter fullscreen mode Exit fullscreen mode

OFC, a simple reason could be that you didn't pass the array. That is what most people would do rather than this above method 😅.

2nd, its checking if argument list length is different or not. This is a smart move, because if the array size itself is changed, you don't need to go through and check every value.

The cheapest function call is the one you never make ~~ Jason Miller

And finally, when all these conditions are true, we finally check if the values match up using the arr.some method.

From what I can tell, this function is written in a way to stop as soon as it can. You could've written this same function in a way that it would do all these things, and then tell the result. Here, through some clever short circuiting , they made this function pretty efficient.

useLayoutEffect

export function useLayoutEffect(callback, args) {
  /** @type {import('./internal').EffectHookState} */
  const state = getHookState(currentIndex++, 4);
  if (!options._skipEffects && argsChanged(state._args, args)) {
    state._value = callback;
    state._args = args;

    currentComponent._renderCallbacks.push(state);
  }
}
Enter fullscreen mode Exit fullscreen mode

This hook is very, very interesting. If you read the code of useEffect, you'll find that they are exactly the same, except for the very last line.

In useEffect, it is 👇

currentComponent.__hooks._pendingEffects.push(state);
Enter fullscreen mode Exit fullscreen mode

Whereas here it is 👇

currentComponent._renderCallbacks.push(state);
Enter fullscreen mode Exit fullscreen mode

In useEffect, the effects to executed are pushed to a queue that executes asynchronously.

Whereas in useLayoutEffect, the effects are pushed to the render callbacks, making it execute eagerly, as the rendering is going on. That's why its called use*Layout*Effect.

Next up, is another hook that will blow your mind and change the way you write your Refs. Yepp, you guessed it right, its useRef 😎

useRef 😎

This hook's implementation is so cool that I can't help but put the Sunglasses emoji in front of it 😁

export function useRef(initialValue) {
  currentHook = 5;
  return useMemo(() => ({ current: initialValue }), []);
}
Enter fullscreen mode Exit fullscreen mode

If you notice, useRef is just useMemo in disguise, with an object that has one property: current with value null.

So, effectively, you could write your refs as memos

const containerElementRef = useMemo(() => ({ current: null }), []);
Enter fullscreen mode Exit fullscreen mode

Don't take this too seriously though. Its better if element refs are assigned to proper useRef values only, as it is cleaner, the syntax is built around it.

What I wanna point at is, is that a lot of people, especially beginners, equate Ref as the thing that holds DOM references, and that's all it do. Which is not a good thing really.

But when you look at this code and realise that the Ref is just a value that's cached for the lifecycle of the component, clarity sinks in. The mental block and the sense of magic goes away, and you feel fully in control.

useCallback

export function useCallback(callback, args) {
  currentHook = 8;
  return useMemo(() => callback, args);
}
Enter fullscreen mode Exit fullscreen mode

And here's another hook that's just useMemo under the hood. This gives me the lols 😂😂. At this point, I'm simply giggling silently seeing that everything in Preact hooks is just useMemo.

Astronaut at gunpoint: So its all just useMemo...? Astronaut with gun: Always has been

useMemo

Ahh, the star of the show, useMemo!!🤩 Finally!

export function useMemo(factory, args) {
  /** @type {import('./internal').MemoHookState} */
  const state = getHookState(currentIndex++, 7);
  if (argsChanged(state._args, args)) {
    state._value = factory();
    state._args = args;
    state._factory = factory;
  }

  return state._value;
}
Enter fullscreen mode Exit fullscreen mode

This one is pretty simple. Get the state for this specific hook, compare the previous dependencies to the new and update values and factory function passed to it if anything changes.

And this again is so small, it makes me laugh as well as cry. Seriously, going through this codebase gives me huge imposter syndrome everytime. The architecture is so damn well done that code duplication isn't needed anywhere here, so everything is super small. Well done Preacters 🥲

useContext

One of the most favorite hooks of all time, useContext 😍

export function useContext(context) {
  const provider = currentComponent.context[context._id];
  // We could skip this call here, but than we'd not call
  // `options._hook`. We need to do that in order to make
  // the devtools aware of this hook.
  /** @type {import('./internal').ContextHookState} */
  const state = getHookState(currentIndex++, 9);
  // The devtools needs access to the context object to
  // be able to pull of the default value when no provider
  // is present in the tree.
  state._context = context;
  if (!provider) return context._defaultValue;
  // This is probably not safe to convert to "!"
  if (state._value == null) {
    state._value = true;
    provider.sub(currentComponent);
  }
  return provider.props.value;
}
Enter fullscreen mode Exit fullscreen mode

Lots of comments here. If I remove all of them

export function useContext(context) {
  const provider = currentComponent.context[context._id];
  const state = getHookState(currentIndex++, 9);
  state._context = context;
  if (!provider) return context._defaultValue;
  if (state._value == null) {
    state._value = true;
    provider.sub(currentComponent);
  }
  return provider.props.value;
}
Enter fullscreen mode Exit fullscreen mode

Are you kidding me!?!? Just 7 lines in the body, and you have the biggest simplification that came when React hooks launched. What sorcery is this!! 😑😑

Notable points here: If no provider is detected, it returns a default value, thanks to that 1 liner if statement. And if no value is found here, preact subscribes the current component to the context.

useErrorBoundary

export function useErrorBoundary(cb) {
  /** @type {import('./internal').ErrorBoundaryHookState} */
  const state = getHookState(currentIndex++, 10);
  const errState = useState();
  state._value = cb;
  if (!currentComponent.componentDidCatch) {
    currentComponent.componentDidCatch = (err) => {
      if (state._value) state._value(err);
      errState[1](err);
    };
  }
  return [
    errState[0],
    () => {
      errState[1](undefined);
    },
  ];
}
Enter fullscreen mode Exit fullscreen mode

I'm a huge, huge fan of Preact for providing a useErrorBoundary hook. In React, if you want error boundaries, you have to create a class component yourself and set at the root of your component tree. Whereas it ships by default in Preact, which makes my heart flutter 😅

Notable points here: This hook mostly sets the componentDidCatch lifecycle to catch the errors and do what you tell this hook to do. Its more or less same as you yourself making a class component, only you don't have to nest anything here, just drop this hook in any component thats on top of the component tree.

That's it for hooks. I didn't cover useDebugValue and useImperativeHandle, as I have never had to use useDebugValue, and useImperativeHandle is deemed unsafe to use ¯\_(ツ)_/¯

A note on simplicity

Notice how I've been saying the code is super simple. Well, it is super easy to read, because that's how simple it is, but writing it is hard. Simplicity is rarely easy, its always harder to achieve. Writing a good, emotional rollercoaster in 100 words is hard. Throwing out excessive clothes is hard. Having a clean desk is harder than a cluttered desk.

And making 3KB code for what was originally 42KB is hard.

Subtraction is harder than addition, division is harder than multiplication.

Making Preact by no means would've been an easy task, but Jason did it amazingly, and all the contributors that jumped in later made it even greater, while still keeping everything small and simpler. This is a monumental task. Hats off to the Preact team for this effort

Hats off!!

This is it for today!

Signing off!!

Top comments (2)

Collapse
 
dev_emmy profile image
nshimiye_emmy

Great article, I use Preact very often in my side projects

Collapse
 
kayrag profile image
Kayra Güneş

Nice article