Logo by twitter.com/haggle
A small UI library with a familiar API.
npm install umai
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.
A good way to get started is to clone the umai-vite template using degit.
npx degit https://github.com/kevinfiol/umai-vite/ hello-world
cd hello-world
npm install
npm run dev
The template is also accessible via StackBlitz.
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.
- For Vite, see the
tsconfig.json
andvite.config.ts
files in the umai-vite template.
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>
`;
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.
Use mount
to mount your application on an element. mount
takes two arguments:
- An Element
- 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);
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>
);
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>
);
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 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 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} />
);
};
Objects following the CSSStyleDeclaration interface are valid style
attributes. The following two examples are functionally equivalent:
// Using a string attribute
const Header = () => (
<header style="background-color: red; font-family: monospace">
Welcome
</header>
);
// Using a CSSStyleDeclaration object
const Header = () => (
<header style={{ backgroundColor: 'red', fontFamily: 'monospace' }}>
Welcome
</header>
);
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>
);
};
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>
);
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>
*/
Both className
and class
are valid properties when defining element classes. If both are present, className
takes precedence.
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>
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>
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>
umai
is a hard fork of hyperapp. Credit goes to all Hyperapp maintainers.
umai
is heavily inspired by Mithril.js.