Back to Million

Behind the block()

website/pages/blog/behind-the-block.en-US.mdx

3.1.79.2 KB
Original Source

import Image from 'next/image'; import { Steps, Callout } from 'nextra-theme-docs'; import { CarbonAds } from '../../components/ad';

<div className="flex justify-center"> <Image src="/behind-the-block.png" width={350} height={130} /> </div> <div className="flex flex-col items-center gap-4">

Behind the block(){:jsx}

<small>AIDEN BAI JUN 1 2023</small>

</div>

If you've used Million.js for a while, you've probably heard of the block(){:jsx} function.

jsx
function MyComponent() {
  // ...
}

const MyBlock = block(MyComponent);

export default function App() {
  return <MyBlock />; // ✨ it works! ✨
}

Wrapping a React component with block(){:jsx} creates a block. A block is a special Higher Order Component (HOC) that can be used as a React component but are hyper-optimized for rendering speed by rendering using Million.js.

But how can this be? How can we use blocks inside of React? Isn't Million.js a completely different virtual DOM?

<CarbonAds />

Anatomy of block(){:jsx}

Once you've created a block and use it as a React component, the following will occur during rendering:

<Steps>

React renders <Loader />{:jsx} component

Initially, React is responsible for rendering the <Loader />{:jsx} component. This process involves creating the necessary DOM elements and applying any initial properties or styles. During this phase, React is managing the lifecycle and state of the component, allowing for rich features such as state management, lifecycle methods, and more.

React mounts <Loader />{:jsx} and puts the DOM element in the ref

Following the rendering process, React then mounts the <Loader />{:jsx} component. This involves inserting the component into the DOM and making it visible to the user. At this point, React also updates the ref with the DOM element. A ref in React is a way to hold local state that doesn't invoke rendering, and in this case, it's being used to store a reference to the DOM element.

Million.js renders <App />{:jsx} into the ref

Finally, the ref is handed over to Million.js, a fast, lightweight virtual DOM. Using the DOM reference stored in the ref, Million.js renders the <App />{:jsx} component directly into this element. This allows Million.js to manage the <App />{:jsx} component separately from React, leading to potential performance benefits and isolation of responsibilities.

</Steps>

This pattern allows us to "control" the DOM element without React knowing about it. React will only know about the <Loader />{:jsx} component, and Million.js will only know about the <App />{:jsx} component.

Implementing block(){:jsx}

With this in mind, we can create a basic implementation of this pattern.

<Callout type="info"> Note: This is not the actual implemention, rather more a conceptual code sample. [View the source here.](https://github.com/aidenybai/million/blob/674b13047665009f8ab1281e77a00a017ddea6e9/packages/react/block.ts#L45) </Callout> <Steps>

Creating a HOC factory

An HOC factory consumes some React component and spits out our <Loader />{:jsx} component. The <Loader />{:jsx} component is responsible for rendering the DOM element and passing it to Million.js.

jsx
const block = (ReactComponent) => {
  return function Loader(props) {
    return /*... */;
  };
};

Grabbing the DOM element with useRef(){:js}

We can use the useRef(){:js} hook to grab the DOM element.

jsx
const block = (ReactComponent) => {
  return function Loader(props) {
    const el = useRef(); // stores the DOM element

    return <div ref={el}></div>;
  };
};

Create an effect to render Million.js

Now, we put it all together. We create a <Effect />{:jsx} component that runs an effect on mount. This effect is responsible for rendering the <App />{:jsx} component into the DOM element. We use useCallback(){:js} to create a stable closure reference to the effect.

Notice how there are Million.convert(){:js} and Million.render(){:js} calls. These are not real, but they essentially create blocks and render it into the DOM element.

jsx
const block = (ReactComponent) => {
  const MillionComponent = Million.convert(ReactComponent);
  return function Loader(props) {
    const el = useRef();

    // 3. Million.js renders <App /> into the ref
    const effect = useCallback(() => {
      // useCallback is used as a stable closure reference
      Million.render(MillionComponent, el.current);
    }, []);

    // 2. React mounts <Loader /> and puts the DOM element in the ref
    return (
      <>
        <div ref={el}></div>
        <Effect effect={effect} />
      </>
    );
  };
};

// Effect is a component that runs an effect on mount
function Effect({ effect }) {
  useEffect(effect, []);
  return null;
}
</Steps>

Compiler, you're a wizard! 🧙

One major limitation of the runtime implementation is that it requires the user to pass in a stateless component. This is because the internal block implementation has a number of limitations. However, we can use the compiler to get around this limitation.

Let's say we have an <Emotion />{:jsx} component that has some isSad state, and based on that state, it renders a 😢 or 😂 emoji.

jsx
function Emotion() {
  const [isSad, setIsSad] = useState(true);
  return <div>{isSad ? '😢' : '😂'}</div>;
}

const EmotionBlock = block(Emotion);

The compiler can extract out the isSad state and convert it into a prop that Million.js can understand.

jsx
function Emotion_jsx({ _0 }) {
  return <div>{_0}</div>;
}

const Emotion_jsx_block = block(Emotion_component);

function EmotionBlock() {
  const [isSad, setIsSad] = useState(true);
  return <Emotion_jsx_block _0={isSad ? '😢' : '😂'} />;
}

But what if we had another React component inside of <Emotion />{:jsx}?

jsx
function SadEmoji() {
  return '😢';
}

function HappyEmoji() {
  return '😂';
}

function Emotion() {
  const [isSad, setIsSad] = useState(true);
  return <div>{isSad ? <SadEmoji /> : <HappyEmoji />}</div>;
}

const EmotionBlock = block(Emotion);

Similarly, this is extracted, but during rendering when it meets a component boundary, it will create a "React render scope." Essentially, it delegates the responsibility of rendering the component to React.

jsx
function SadEmoji() {
  return '😢';
}

function HappyEmoji() {
  return '😂';
}

function Emotion_jsx({ _0 }) {
  return <div>{_0}</div>;
}

const Emotion_jsx_block = block(Emotion_component);

function EmotionBlock() {
  const [isSad, setIsSad] = useState(true);
  return (
    <Emotion_jsx_block
      _0={renderReactScope(isSad ? <SadEmoji /> : <HappyEmoji />)}
    />
  );
}

As you can see, the compiler is able to extract out the state and render it from a parent element. It can also recognize when it hits a component boundary and delegate the responsibility of rendering to React.

Not just Million.js

While this article details how Million.js takes advantage of this pattern, it is not limited to just Million.js.

For any modern framework that can render into a DOM element, you can use the <Loader />{:jsx} and HOC pattern to render foreign framework components inside of React.

A very similar concept is the "islands architecture", which allows you to incapsulate any framework into static HTML. This is a bit different, instead of rendering into static HTML, it renders into a React tree.

<div className="flex justify-center"> <Image src="/foreign-tree.png" width={350} height={500} /> </div>

Why not a compatibility layer?

JavaScript frameworks like Preact and Inferno have compatibility layers that allow them to masquerade as React components but with better performance. This has a lot of benefit, as it allows projects and engineering teams to move very fast without having to rewrite their entire codebase.

But it comes at a cost. Compatibility layers always have to play catch up. When React adds a new feature, the compatibility layer has to add support for it. Maintaining the same behavior is near impossible, especially emulating the same behavior and benefits of the React concurrency model.

Closing Thoughts

By using specific different rendering methodologies on a component-by-component basis, we can take advantage of the best of both worlds and use the right tool for the right job. Hopefully one day, we'll see more frameworks adopt this pattern. Because performance shouldn't be a tradeoff for migration.

Discuss on Twitter | Edit on GitHub

Acknowledgements

Thank you to Ryan Carniato for creating an initial Solid.js inside React proof-of-concept that inspired this article.

Looking for more? Check out another interesting read from Yongjun Park .