When I was introduced to React, I liked it but soon discovered that browsers need to download about 100k of code in order to use it. So I switched to mithril.js which weights only about 10k. In late 2020, I re-invented the wheel and well...
- Less than 3k gzip size (about 5k raw)
- Stateful components using hooks
- Optional development mode
- CSS-less Animations possible
- Fast and efficient, benchmark
- 0 dependencies
- No toolchain neccessary
- TypeScript compatible
- RequireJS compatible
- Server-side rendering possible
- Template files supported (soon)
- Production ready
- Compatible with down to 20th century browsers, basic polyfills included
- Created and actively maintained by a perfectionist
- ... and more (see API)
- minicraft
- Small demonstation app
- TODO app that runs in IE5, ssr variant, simpler variant
- Animated sidebar
- prog
- TicTacToe
- Text encryption toy
- RequireJS
- Clock for MS Windows
Just download a demo file from above and modify it as you like! Also look into the app files in the test
directory. Especially check out minicraft!
Tip
While coding, use lui.dev.js
instead for fancy error detection.
Just add the following line to your HTML file before where lui is used:
<script src="https://cdn.jsdelivr.net/gh/L3P3/lui@2/lui.js"></script>
Tip
To not block page loading, add defer
to the script tags.
Tip
For type information in your code editor, just copy lui-link.js
and index.d.ts
from this repo into your project, rename them to lui.js
and lui.d.ts
and import from the js
file.
There are several features represented in dedicated variants of lui. These features are:
Feature | Description |
---|---|
x | Extended variant, includes some nice ui features not needed by most apps |
r | Loaded by RequireJS, read more |
dev | Includes many error checks and prints helpful messages into the console |
legacy | Supports old browsers down to IE5, read more |
noeval | Does not generate code at runtime, read more |
To see which combinations exist, see the dist branch. If you need a combination that is not yet there, contact me.
When using the standalone file mentioned above, it registers a global lui
object containing all functions mentioned here.
When the app is transpiled/bundled, use a wrapper as explained above.
To bundle lui itself with your app, just import {...} from 'lui'
. Learn more here.
A component is a function which takes props and returns a list of its child nodes. Components can be instantiated as nodes in other components. By recursion, you can build up a dom tree in a very flexible way. I also recommend reading React's explaination.
Here is a very simple component. It takes two props (color
is optional) and it consists of just one node.
function ColoredText({
color = 'red',
text,
}) {
return [
node_dom('span', {
S: {color},
innerText: text,
}),
];
}
To have child components, you have to return nodes. It does not matter where you create nodes as long as you always return them in the same order in components. In order to not use a node, return a boolean or something falsy in its place.
In the above example, we use the span
dom component. If you want to use a custom component like ColoredText
above, just refer to it like this:
function BlueText({
text,
}) {
return [
node(ColoredText, {
color: 'blue',
text,
}),
];
}
If you want to map an array with changing order or length to a list of components, use node_map
instead of calling node
for each item to keep the invariant that nodes must have a stable order.
The component you pass to node_map
gets mounted for each item of the array you pass to it. The component gets a prop I
containing the corresponding array item. If you pass props as the third argument, every child will get them in addition to I
.
Important
Allowed array items are numbers, strings and objects. If you pass objects, they must have an unique id
property. There must not be two items of the same value or id.
The leaves of your component tree are mostly made out of native dom elements. To use such a component, use node_dom
instead of node
. The signature is the same, except for the first argument being a descriptor, similar to css selectors: tagName[attr1=value][attr2][...]
The tagName
is required but number and order of attributes are optional. Having static attributes in the descriptor instead of in the props improves efficiency (re-using of nodes, reduced diffing). For that reason, always sort attributes alphabetically.
Props are strings directly mapped to dom attributes, except these 4 special non-string props:
prop | Description |
---|---|
C: Array<node> |
The nodes that should come into it. Instead of as a prop, you can pass this array as the third argument to the node function. |
D: Object<string, *> |
element.dataset mapping object, to set data- attributes. |
F: Object<string, boolean> |
An object of applied css classes. Each key with a true value will be applied. Others not. |
R: function(HTMLElement) |
This function is given the instance's dom element after it is created. |
S: Object<string, string> |
element.style mapping. |
Tip
For inline css, css classes and data attributes, use these special props!
When you wanna use hooks for component-like abstraction without always having to return nodes as in components, lui offers hook_sub
and hook_map
. They are a mixture of hook_memo
with node
and node_map
, respectively. But that's some high-level shit; average Joe won't really need (nor understand) it.
The component tree's root is defined by just the one and only call to init
. It gets passed your root component.
To set attributes on the root element itself, use hook_dom
. In old lui versions, we returned [host attributes, childs]
in root components but that was stupid.
init(() => {
hook_dom('', {
S: {background: 'black'},
});
return [
node(BlueText, {
text: 'Hello, world!',
}),
];
});
By default, the body is used as the root element. You can also specify another one as the second argument to init
.
Optionally, static props to the root component can also be passed as a third argument.
Warning
After calling init
on a root element, leave it and its content alone. It belongs to lui now.
Instead of defining all the dom elements in your components, you can define them once and then use them as templates. This is especially useful for elements that have many/long attributes. Reference them by their handle, prefixed with #
.
dom_define('my-button', 'button[disabled]');
// ...
node_dom('#my-button', {
innerText: 'Try to click me!',
});
If you are building your application code with JSX support, you could theoretically write components like this:
function YellowText({
text,
}) {
return (
<span style="color: yellow">{text}</span>
);
}
However, you need to set up babel and the jsx plugin properly and I have not tried that yet. The plugin must use node
or node_dom
instead of React.createElement
. And nodes must always be supplied in a flat array. Personally, I am not interested in that feature.
Instead of using object oriented syntax like this.number = 42;
(or this.setNumber(42);
allowing automatic updates) inside components, you call hooks. Actually, from application code, there is no way to access component instances at all! This may be very confusing at first but once you understood it, it will be very easy to work with.
Stateful hooks are identified by their calling order per instance. Keep that order by never placing a hook in an if
, a loop or a switch
body – unless their condition, item order or key will stay the same per instance.
Stateless hooks (hook_assert
and hook_rerender
) may be called anywhere in your component.
Warning
Placing any hook in a callback/if/loop is not allowed. Only use them directly in component functions.
I highly recommend React's documentation about hooks since their concept is very similar.
But while some hooks also exist in React, some do not. And on the other hand, I never used some hooks so I did not implement them. Please note that there are differences in behaviour. For example, in lui deps actually are passed as arguments while in React, they are not.
lui | React |
---|---|
hook_assert |
- |
hook_async |
- |
hook_callback |
useCallback |
hook_delay |
- |
hook_dom |
- |
hook_effect |
useEffect |
hook_map |
- |
hook_memo |
useMemo |
hook_model |
useReducer |
hook_object_changes |
- |
hook_prev |
- |
hook_rerender |
- |
hook_state |
useState |
hook_static |
useRef |
hook_sub |
- |
hook_transition |
- |
For more complex component/app states than simple primitives like number
or boolean
, I highly recommend to use a model, together with transformations. If you already know Redux, then yes, it is just like Redux. If not, see here.
To have a state machine in lui, use hook_model
. To this hook, you pass a set of transformation functions. All of them get the current state and return the new state, no modifications of the current state are allowed in order to allow for efficient change detection. Keep in mind that these transformations must be pure functions, so no side effects are allowed. Their only effects/changes appear in their return value!
You can find a good practical example here. Here is another one, generated by ChatGPT:
const { hook_model, init, node, node_dom } = lui;
const model = {
init: () => 0,
increment: state => state + 1,
decrement: state => state - 1,
};
function Counter() {
const [count, { increment, decrement }] = hook_model(model);
return [
node_dom('div', null, [
node_dom('span', { innerText: `Count: ${count}` }),
node_dom('button[innerText=Increment]', { onclick: increment }),
node_dom('button[innerText=Decrement]', { onclick: decrement }),
]),
];
}
init(Counter);
Just copy this code into ChatGPT and let it explain this, it does a really good job in that!
If a function is passed as a prop (eg. for event handlers), it should be defined outside of the component, so it will not get redefined on each rendering.
If the callback requires something from your component, wrap it in hook_callback
. This way, the child component does not update on each render call since new Function() !== new Function()
.
This may just have a small impact on performance and you may as well just use closures as the React guys are doing it. Modern browsers are quite efficient in frequent function definitions. But older browsers would heavily profit from externalizing function definitions.
In React, you pass an object to dom components via the prop ref
. In lui, you pass a function (eg. a setter) via the prop R
. This way, you can get properties of the element or manipulate it in a way impossible via the props provided by dom components.
When the component relies on some kind of condition or possibly unresolved promise, you may use hook_assert
to interrupt the rendering. Here is an example for the first:
function UserName({
id,
}) {
const name = hook_async(user_name_get, [id]);
hook_assert(name !== null);
const NAME = name.toUppercase();
return [
node_dom('span[className=user-name]', {
innerText: NAME,
title: name,
}),
];
}
Here, user_name_get
is an async function, meaning it returns a promise. When the promise is not yet resolved, this component will just not display anything. Also notice the NAME
line: Without hook_assert
, we would get an error, since we cannot call methods on null
.
When you already know that the condition will be false, just call it like this: hook_assert();
This is similar to return null;
but it can be called in any component (including root) or hook.
By default, every state change gets rendered synchronously. In most cases, this is very useful. When a state is set, the view will already be updated when the setter returns.
However, in some rare cases, multiple (or many) setters are called in one go, causing many redundant rerenders. To prevent this and rerender all changes in one single go, use the defer
method like this:
function someEvent(value) {
defer();
foo_set(value);
bar_set(value);
}
When someEvent
is called, lui will rerender only once instead of twice. The rendering will happen at the next frame. When you need it to happen instantly, call defer_end
after your state mutations. See React's article about merged/batched updates.
For the exact signatures, see the typescript definition file! You should somehow integrate it into your project anyway, so your IDE will show you the types automatically.
The third column says if that function is included in the core variant lui.js
or if you need to use the extended variant luix.js
. (Only needed when using the script tag method above. See variants.)
Function | Description | V |
---|---|---|
defer() |
Disables synchronous rerenders until the next frame. | C |
defer_end() |
Enables synchronous rerenders again and rectifies deferred updates. | C |
dom_define(handle, descriptor, props{}) |
Defines a dom template. | C |
hook_assert(boolean) |
When the condition is falsy, rendering of the current component is interrupted. May be used for error handling or anything else. | C |
hook_async((...deps)=>Promise<T>, deps[], fallback):T |
If you need to wait for some data until it is available, use this instead of hook_memo . As long as the promise is pending, fallback is returned. If no fallback is given, either null or the latest value is returned. |
C |
hook_callback(function, deps[]):function |
Returns a function that never changes. It passes all arguments down to the given function after the deps . Use this when you need to pass a callback as props that needs deps . If that callback is independent of the current component (has no deps ), move the callback out of the component. |
E |
hook_delay(msecs):boolean |
Turns true after the specified delay. |
E |
hook_dom(descriptor, props{}):element |
Alternative to a single node_dom child. Returned childs will be wrapped by this element. Must not be skipped or called twice per component. Can also be used to access the root element. |
C |
hook_effect((...deps)=>destroy, deps[]) |
Run the given function once and every time an deps item changes. That function may return another function that gets called before the effect appears again or when the component gets unmounted. |
C |
hook_map((item, ...deps)=>T, items[], deps[]):T[] |
Like hook_sub but for each data item as in node_map . |
E |
hook_memo((...deps)=>T, deps[]):T |
When you need to do some data transformation, put your transformation code inside this hook and it only gets called when a deps entry changes. |
C |
hook_model({init, ...}):[value, {...}] |
If you use a state that has some logic with it, use this. This replaces hook_reducer . |
C |
hook_object_changes(object):keys[] |
This gives you a list of properties that changed since the last rendering. | E |
hook_prev(current, initial):prev |
If you want to compare something to its version from the previous rendering, use this. At first rendering, initial is returned. |
C |
hook_rerender() |
When this is called, this component will be rendered again next frame, only intended for animations. | C |
hook_state(initial):[value, setter(), getter()] |
A simple component state. The first argument is the initial value. | C |
hook_static(initial):initial |
This is a much cheaper version of hook_memo : What you put in it the first time will always come out of it. |
C |
hook_sub((...deps)=>T, deps[]):T |
Like hook_memo but the getter function may be swapped and it may contain hooks. |
E |
hook_transition(target, msecs):current |
When target changes, the output number will smoothly pass to the new target, taking the specified time for that transition. |
E |
init(RootComponent, root_element, props) |
This mounts the body (or root_element) once, you pass it your root component. | C |
node(Component, props{}, childs[]):node |
This is how you add child components. If the added component accepts childs (C prop), you can pass that as the third argument as an array of nodes. |
C |
node_dom('descriptor', attrs{}, childs[]):node |
When you want to add dom components, use this function. It is very similar to node but needs a descriptor instead. |
C |
node_map(Component, data[], props{}) |
When you want to add a component n times for each entry of an array, this is the (proper) way to go. If the array items are objects, the keys are directly taken from an id property. |
C |
now():number |
The relative point of time of the latest rerendering call. Do not use this as persistent time reference but just inside of run time. Useful for custom animations. | C |
Maybe you want to use lui in a more special case. No problem!
You can simply run npm install https://github.com/l3p3/lui
to install it. Later, when lui ist complete enough, I may add it to npm as well.
Caution
When bundling for production, you should make sure to automatically set DEBUG
to false
in node_modules/lui/src/flags.js
. If that is not done, the result will be bigger and slower.
Important
Please do NOT try to use the uncompiled src/lui.js
in production! It is written specifically for being compiled.
Normally, lui controls the entire page. But it is also possible to dynamically load lui and let it control just a part of the page. This is useful if you work on a big project like Magento and want to use lui.
require.config({
map: {
'*': {
'lui': 'https://cdn.jsdelivr.net/gh/L3P3/lui@2/lui.r.js'
}
}
});
Use lui.r.dev.js
when developing. And here is your widget's file:
define(['lui'], function(lui) {
var init = lui.init;
var node_dom = lui.node_dom;
function Root() {
return [
node_dom('h1[innerText=Moin!]')
];
}
return function(root) {
init(Root, root);
};
});
This variant supports older browsers.
In case you want to support browsers down to Internet Explorer 5, you can use the legacy variant lui.legacy.js
. It has many tiny polyfills included so you do not need to care about that stuff.
Of course, your code still needs to have the most basic syntax, so no arrow functions, no const
or let
, no deconstructing, no trailing commas and so on. Transpilers like closure compiler with the target set to ES3 take care of most of that. But you can out of the box use methods like [].map
, Object.assign
or fn.apply()
. And yes, IE5 does not come with that! In order to not get a second set of polyfills from your transpiler, set it up to not include them.
When using lui's polyfills, note that they are not complete or true to specification at all but just made to make lui itself work. Many methods are not included and some have some restrictions thay may cause issues in unusual use cases. Polyfills already take up about a quarter of the file so I want to keep them small, but pretty fast.
See the demo section for examples.
Some websites forbid libraries generating code at runtime. This library uses this cool feature to speed up some things but if you cannot use lui due to CSP, use this variant.
It may be slightly slower in theory but probably you would not notice the difference in practice.
Check out this example with restrcting CSP: normal lui and noeval lui. See the console and network tab to see what happens in the background.
See #50 for some background.
I am quite sure that no one wants to know anything of this project but if you have ideas or some other kind of feedback, feel free to open an issue, pull request or write me a mail.