umai

umai

Logo by twitter.com/haggle

A small UI library with a familiar API.

bundle.js badge

Install

npm install umai

Usage

import { m, mount } from 'umai';

let count = 1;

const App = () => (
  <div>
    <h1>Count: {count}</h1>
    <button onclick={() => count += 1}>
      increment
    </button>
  </div>
);

mount(document.body, App);

See Examples.

JSX

Note: JSX requires a build step.

If you prefer JSX, you can configure your favorite compiler/bundler to transform JSX to m calls at build time. For esbuild, see Using JSX without React. Also, see test/esbuild.js for an example esbuild configuration.

In files containing JSX, the factory function (m) must be imported.

import { m } from 'umai'; // this is required to use JSX

const MyComponent = () => (
  <div>Hello, JSX!</div>
);

Alternatively, if you'd like JSX-like syntax without a build step, developit/htm pairs nicely with umai.

import htm from 'htm';
import { m } from 'umai';

const html = htm.bind(m);

const MyComponent = () => html`
  <div>Hello, htm!</div>
`;

Hyperscript

You can use umai without JSX or htm with the included hyperscript API m.

import { m } from 'umai';

const MyComponent = ({ name }) => (
  m('div', `Hello, ${name}!`)
);

const App = () => (
  m('div',
    m(MyComponent, {
      name: 'Hyperscript'
    }) 
  )
);

Using the hyperscript API also allows you to use the hyperscript class helper.

Mounting

Use mount to mount your application on an element. mount takes two arguments:

  1. An Element
  2. A stateless component (a function that returns a virtual DOM node)
const el = document.getElementById('app');
const App = () => <p>hello world</p>;
mount(el, App);

Components

umai components (stateless components) are functions that return pieces of your UI. Components accept an object of properties (props) as their first argument.

const User = (props) => (
  <div class="user">
    <h2>{props.name}</h2>
  </div>
);

const List = () => (
  <div class="users">
    <User name="kevin" />
    <User name="rafael" />
    <User name="mike" />
  </div>
);

Passing children

children are passed as part of the props object. They can be used to compose multiple components. This is helpful when creating layouts or wrapping styled elements.

const Layout = ({ title, children }) => (
  <div class="container">
    <h1 class="page-title">{title}</h1>
    {children}
  </div>
);

const UserPage = () => (
  <Layout title="User Page">
    <p>Welcome to the user page!</p>
  </Layout>
);

Redraws & State Management

umai uses global redraws. This means event handlers defined in your app will trigger full component tree re-renders. This simplifies state management so that any variable within the scope of your component is valid state.

let input = '';
let todos = ['take out trash', 'walk the dog'];

const Todo = () => (
  <div>
    <input
      type="text"
      value={input}
      oninput={(ev) => input = ev.target.value}
    />

    <button onclick={() => { todos.push(input); input = ''; }}>
      add todo
    </button>

    <ul>
      {todos.map(todo =>
        <li>{todo}</li>
      }
    </ul>
  </div>
);

Triggering manual redraws is also possible using redraw. This is helpful when dealing with effects or asynchronous operations.

import { m, redraw } from 'umai'; 

let time = '⏰ starting...';

setInterval(() => {
  time = new Date().toLocaleTimeString();
  redraw(); // this tells umai to rerender
}, 1000);

const Clock = () => (
  <div>
    <h1>{time}</h1>
  </div>
);

If your event handler returns a promise, redraw is automatically called for you when the promise has settled.

import { m } from 'umai';
import { fetchUsers } from './api.js';

let users = [];

const getUsers = () => {
  fetchUsers()
    .then(res => users.push(res)); // no need to call redraw!
};

const Dashboard = () => (
  <div>
    {!users.length &&
      <p>There are no users!</p>
    }

    {users.length && users.map(user =>
      <p>{user.name}</p>
    )}

    <button onclick={getUsers}>
      Retrieve Users
    </button>
  </div>
);

Stateful Components

Stateful components are functions that return stateless components.

const StatefulComponent = (initialProps) => {
  let localVariable = 'hello world';

  return (props) => (
    <div>
      {localVariable}
    </div>
  );
};

In the example above, the inner function (the stateless component) is run on every re-render, whereas the outer function (initializing localVariable) is only run once when the component initializes.

Here is an example of a Counter component that contains its own state. We can take advantage of initialProps to set the initial count.

const Count = ({ initialCount }) => {
  let count = initialCount;

  return () => (
    <div>
      <h1>Count: {count}</h1>
      <button onclick={() => count += 1}>
        increment
      </button>
    </div>
  );
};

Now that this component is stateful, I can mount multiple Count components in my app, each containing their own state.

const App = () => (
  <div>
    <Count initialCount={0} />
    <Count initialCount={10} />
    <Count initialCount={100} />
  </div>
);

dom property

DOM nodes are passed to the dom handler immediately upon being created.

const Description = () => (
  /* logs `p` Node to the console */
  <p dom={(node) => console.log(node)}>
    hello world
  </p>
);

You may optionally return a function that will be invoked upon Node removal.

const Description = () => (
  <p dom={(node) => {
    console.log('created p node!');
    return () => console.log('removed p node!');
  }}>
    hello world
  </p>
);

When used with stateful components, the dom property may be used to store references to DOM elements (similar to ref/useRef in React).

const Scrollbox = () => {
  let containerEl;

  return ({ loremIpsum }) => (
    <div dom={(node) => containerEl = node}>
      {loremIpsum}

      <button onclick={() => containerEl.scrollTop = 0;}>
        scroll to top
      </button>
    </div>
  );
};

dom is also useful for third-party library integration. See examples for working examples.

import { m } from 'umai';
import Chart from 'chart.js';

const ChartApp = () => {
  let chart;

  const onMount = (node) => {
    chart = new Chart(node, { /* chart.js options */ });

    return () => {
      // cleanup on node removal
      chart.destroy();
    };
  };

  return () => (
    <canvas dom={onMount} />
  );
};

Memoization

Memoization allows you to skip re-rendering a component if its props are unchanged between re-renders. umai provides a convenience utility for memoizing components using shallow equality checks.

import { m, memo } from 'umai';

// User will not re-render if all props are strictly equal `===`
const User = memo((props) => (
  <div>
    {props.name}
  </div>
));

If you'd like more control over when to re-render, all components are passed their old props as a second argument. You can use this in conjunction with m.retain to return the old virtual DOM node.

import { m } from 'umai';

const User = (props, oldProps) => {
  if (props.name === oldProps.name)
    return m.retain(); // return the old virtual DOM node

  return (
    <div>
      {props.name}
    </div>
  );
};

Keys

Use key for rendering lists where the DOM element order matters. Prefer strings or unique ids over indices when possible.

import { emojis } from './emojis.js'; 

const food = [
  { id: 1, name: 'apple' },
  { id: 2, name: 'banana' },
  { id: 3, name: 'carrot' },
  { id: 4, name: 'doughnut' },
  { id: 5, name: 'egg' }
];

const FoodItem = (initialProps) => {
  const emoji = emojis[initialProps.name];
  
  return ({ name }) => (
    <p>{emoji} = {name}</p>
  );
};

const App = () => (
  <div>
    {food.map((item) =>
      <FoodItem
        key={item.id}
        name={item.name}
      />
    )}
  </div>
);

Fragments

umai features minimal fragment support. There are two main caveats to keep in mind:

  • Keyed fragments are not supported
  • Components must return virtual DOM nodes.
const Users = () => (
  <>
    <p>kevin<p>
    <p>rafael</p>
  </>
);

const App = () => (
  <div>
    {/* ✗ Not OK! umai components must return a virtual DOM node. */}
    <Users />
    
    {/* ✓ OK! A factory function that returns a fragment. */}
    {Users()}
  </div>
);

If you are using the hyperscript API (m), arrays are interpreted as fragments.

const Users = () => [
  m('p', 'kevin'),
  m('p', 'rafael')
];

const App = () => (
  m('div',
    Users()
  )
);

/*
The above renders:
  <div>
    <p>kevin</p>
    <p>rafael</p>
  </div>
*/

Class Utilities

Both className and class are valid properties when defining element classes. If both are present, className takes precedence.

Class String Builder

You may pass an object as an element class name where the keys correspond to CSS class names. umai will construct a class string based on the boolean values of each object property. This is helpful when conditionally applying CSS styles, and complements CSS Modules and utility CSS libraries nicely.

const Modal = ({ isOpen = true }) => (
  <div class={{ 'modal--open': isOpen, 'bg-green': false, 'font-xl': true }}>
    ...
  </div>
);

// The above will render:
// <div class="modal--open font-xl">...</div>

Hyperscript Class Helper

If you are using the hyperscript API, you may append classes delimited with . as part of the element tag.

const Todo = () => (
  m('div.todo.font-sm',
    '...'
  )
);

// The above will render:
// <div class="todo font-sm">...</div>

This can also be used with the class string builder to define classes that should always be present.

const Modal = ({ isOpen = true }) => (
  m('div.font-xl', { class: { 'modal--open': isOpen, 'bg-green': false } },
    '...'
  )
);

// The above will render:
// <div class="modal--open font-xl">...</div>

Using raw HTML

Note: Make sure to sanitize HTML generated by user input.

const html = '<em>this should be emphasized</em>';

const Comment = ({ userName }) => (
  <article>
    <span innerHTML={html} />
  </article>
);

// The above will render:
// <article><span><em>this should be emphasized</em></span></article>

Examples

Credits

umai is a hard fork of hyperapp. Credit goes to all Hyperapp maintainers.

umai is heavily inspired by Mithril.js.