A Simple JavaScript rendering library with a React-compatible API

Tortie

Simple JavaScript rendering library with a React-compatible API

I built this mostly to understand how React works under the hood. Currently it supports only a small subset of the React API (3KB minified).

Features

  • Function components with hooks (useState, useEffect)
  • No dependencies
  • Built with TypeScript

See it live here.

Install

npm install --save tortie-core

API

render(virtualNode, domRoot)

Updates the domRoot contents using the virtual DOM tree at virtualNode. Subsequent render calls will try to make as few writes as possible to the real DOM tree.

createVirtualElement or e

Creates a virtual DOM node. It can be used to render regular DOM elements:

createVirtualElement(tagName, attributes, ...children);

To render, pass the virtual node to the render function:

render(
  e('div', { className: 'kittens' }, e('h1', null, ' Nora')),
  document.body
);

Which renders:

<div class="kittens">
  <h1>Nora</h1>
</div>

It can also render function components:

createVirtualElement(functionComponent, props, ...children);

const Kitten = ({ name }) => {
  return e('div', { className: 'kitten' }, e('h1', null, name));
};

render(
  e('div', { className: 'kittens' }, e(Kitten, { name: 'Nora' })),
  document.body
);

Which renders:

<div class="kittens">
  <div class="kitten">
    <h1>Nora</h1>
  </div>
</div>

Event Handling

Use the onClick or onInput prop:

const Kitten = ({ name }) => {
  return e(
    'div',
    { className: 'kitten' },
    e('h1', null, name),
    e('button', { onClick: () => console.log(`${name} woke up!`) }, 'Wake up')
  );
};

render(
  e('div', { className: 'kittens' }, e(Kitten, { name: 'Nora' })),
  document.body
);

Which renders:

<div class="kittens">
  <div class="kitten">
    <h1>Nora</h1>
    <button>Wake up</button>
  </div>
</div>

Pressing the button will print Nora woke up! to the console.

useState(initialValue)

Adds local state to the component and provides a setter to update the state. Calling the setter triggers a rerender of the component subtree:

const Kitten = ({ name }) => {
  const [isAwake, setIsAwake] = useState(false);

  return e(
    'div',
    { className: 'kitten' },
    e('h1', null, name),
    e('button', { onClick: () => setIsAwake(true) }, 'Wake up'),
    isAwake && e('span', null, `${name} woke up!`)
  );
};

render(
  e('div', { className: 'kittens' }, e(Kitten, { name: 'Nora' })),
  document.body
);

Which renders:

<div class="kittens">
  <div class="kitten">
    <h1>Nora</h1>
    <button>Wake up</button>
  </div>
</div>

Pressing the button will update the DOM to:

<div class="kittens">
  <div class="kitten">
    <h1>Nora</h1>
    <button>Wake up</button>
    <span>Nora woke up!</span>
  </div>
</div>

useEffect(callback, dependencies)

Calls callback whenever the dependencies array changes after the current component render. Useful for running side effects during render:

const Kitten = ({ name }) => {
  const [isAwake, setIsAwake] = useState(false);

  useEffect(() => {
    console.log(isAwake ? `${name} woke up!` : `${name} is asleep.`);
  }, [isAwake]);

  return e(
    'div',
    { className: 'kitten' },
    e('h1', null, name),
    e('button', { onClick: () => setIsAwake(true) }, 'Wake up')
  );
};

render(
  e('div', { className: 'kittens' }, e(Kitten, { name: 'Nora' })),
  document.body
);

Which renders:

<div class="kittens">
  <div class="kitten">
    <h1>Nora</h1>
    <button>Wake up</button>
  </div>
</div>

On first render, the component prints Nora is asleep. to the console.

Pressing the button will print Nora woke up! to the console. Future button presses will not trigger the effect since we added isAwake to the dependency array.

GitHub

View Github