Tutorial
Welcome to the Million.js documentation (woop woop 🎉🤑). Let's learn how we can integrate Million.js into our React applications.
Million.js assumes that you're already familiar with and you're using React. If you're not, we recommend checking out react.dev (opens in a new tab) first.
You will learn:
- How to use
block()
to convert React components into blocks - How to use
<For />
for efficiently rendering lists - When to use
block()
and<For />
- The limitations of blocks
What's a block?
Million.js is a library that enables you to create blocks. A block is a special Higher Order Component (HOC) (opens in a new tab) that can be used as a React component but is hyper-optimized for rendering speed.
Blocks are essentially components wrapped by block()
.
import { block } from "million/react";
const LionBlock = block(function Lion() {
return (
<img src="https://million.dev/lion.svg" />
);
})
Blocks can be used just like a normal React component:
export default function App() {
return (
<div>
<h1>mil + LION = million</h1>
<LionBlock />
</div>
);
}
Have a look at the result:
Preview
With that in hand, let's build an app.
Data Grid Example
One use case of blocks is rendering lists of data efficiently. In this example, let's build a data grid in React.
We have access to the prebuilt components <Table />
and <Input />
from our fake user interface (UI) library. We can then store the number of rows we want to display in a useState()
hook.
function App() {
const [rows, setRows] = useState(1);
return (
<div>
<Input value={rows} setValue={setRows} />
<Table>
// ...
</Table>
</div>
);
}
But wait! We made a grid but we have no data. Let's say we can grab some array of arbitrary data from a function called buildData(rows)
:
const data = buildData(100);
// returns [{ adjective: '...', color: '...', noun: '...' }, ... x100]
Now, we can render the data in our table using Array.map()
:
function App() {
const [rows, setRows] = useState(1);
const data = buildData(rows);
return (
<div>
<Input value={rows} setValue={setRows} />
<Table>
{data.map(({ adjective, color, noun }) => (
<tr>
<td>{adjective}</td>
<td>{color}</td>
<td>{noun}</td>
</tr>
))}
</Table>
</div>
);
}
Preview
We can see that it performs pretty well. From 0-100, there's virtually no lag, but once you get higher than 500 or so, there's a noticable delay in rendering.
Cool right? React is great because we can declaratively write great UI and get pretty good performance. But the data grid we just made is a rudimentary example, and is not necessarily representative of most React applications.
More realistic rendering
So, let's introduce some spice. In the following example, we add lotsOfElements
(an array of a lot of blank elements) to each row. We also add a lag radar to monitor page performance.
Try changing the input value up and down from 0 to 1000. Notice how React really struggles when rendering a lot of elements.
Editable example
import { useState } from 'react'; import { Table, Input, lotsOfElements } from './ui'; import { buildData } from './data'; function App() { const [rows, setRows] = useState(1); const data = buildData(rows); return ( <div> <Input value={rows} setValue={setRows} /> <Table showRadar> {data.map(({ adjective, color, noun }) => ( <tr> <td>{adjective}</td> <td>{color}</td> <td>{noun}</td> <td>{...lotsOfElements}</td> </tr> ))} </Table> </div> ); } export default App;
Preview
Just block
it
In the following example, we use block()
and <For />
in order to optimize rendering.
First, we need to abstract the <tr>
into its own component.
data.map(({ adjective, color, noun }) => (
<tr>
<td>{adjective}</td>
<td>{color}</td>
<td>{noun}</td>
<td>{...lotsOfElements}</td>
</tr>
))
// 👇👇👇
function Row({ adjective, color, noun }) {
return (
<tr>
<td>{adjective}</td>
<td>{color}</td>
<td>{noun}</td>
{...lotsOfElements}
</tr>
);
}
Then, we can wrap it with block()
in order to optimize the <Row />
component.
import { block } from "million/react";
const RowBlock = block(
function Row({ adjective, color, noun }) {
return (
<tr>
<td>{adjective}</td>
<td>{color}</td>
<td>{noun}</td>
{...lotsOfElements}
</tr>
);
}
);
Once, we've optimized a row, we need to render it as a list:
data.map(({ adjective, color, noun }) => (
<RowBlock adjective={adjective} color={color} noun={noun}>
));
BUT WAIT! We can actually use Million.js' built-in rendering solution.
Optimized List Rendering
The <For />
component is used to render a list of blocks. It takes an array as the each
prop and a function as its children. The function is called for each item in the array and is passed the item and its index as arguments.
<For />
Component
Syntax: <For each={array}>{(item, index) => Block}</For>
Example: <For each={[1, 2, 3]}>{(item, index) => myBlock({ item, index })}</For>
It's the best way to loop over an array (uses mapArray()
under the hood). As the array changes, <For />
updates or moves items in the DOM rather than recreating them. Let's look at an example:
With this in mind, we can rewrite our table to use <For />
:
import { For } from "million/react";
<For each={data}>
{({ adjective, color, noun }) => (
<RowBlock adjective={adjective} color={color} noun={noun} />
)}
</For>
Now that we've integrated Million.js, let's check the new example out.
Notice when you change the input value, the lag radar shows significantly less lag than the pure React example. With a faster underlying virtual DOM, Million.js can take a lot of the pain out of rendering large lists.
Editable example
import { useState } from 'react'; import { Table, Input, lotsOfElements } from './ui'; import { buildData } from './data'; import { block, For } from 'million/react'; const RowBlock = block( function Row({ adjective, color, noun }) { return ( <tr> <td>{adjective}</td> <td>{color}</td> <td>{noun}</td> {...lotsOfElements} </tr> ); } ); function App() { const [rows, setRows] = useState(1); const data = buildData(rows); return ( <div> <Input value={rows} setValue={setRows} /> <Table showRadar> <For each={data}> {({ adjective, color, noun }) => ( <RowBlock adjective={adjective} color={color} noun={noun} /> )} </For> </Table> </div> ); } export default App;
Preview
Million.js vs. React
The following is a more comprehensive demo using key-based rendering (opens in a new tab) to show how Million.js performance compares to React.
Hitting the limit
This section is a bit more advanced. If you want a list of limitations, check out the Rules of Blocks. Or, if you just want to start integrating Million.js, check out the installation guide.
Blocks are great for rendering large lists, data grids, and many other use cases. Under the hood, they render with the Million.js virtual DOM instead of React.
Deep Dive: How does it work?
(1/6)
Using a block can allow us to capture potential performance gains. However, you should always use best judgement, as blocks are not a silver bullet. Here are some general guidelines to follow:
- Static views: Blocks perform best when there's not that much dynamic data. Since static parts of the React tree need to be unnecessary rerendered when dynamic data changes by React, blocks can directly skip to what's dynamic.
- Nested data: Blocks are great for rendering nested data. Million.js turns tree traversal from
O(tree)
toO(1)
, allowing for fast access and changes.
Looking for the full guidelines? Check out Rules of Blocks.
Next Steps
By now, you know the basics of how to integrate Million.js into your application!
Check out the installation guide to put them into practice and start using blocks.
This page is directly inspired by React's "Quick Start" page (opens in a new tab).