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...
- Under 4k code size (<2k compressed)
- Stateful components using hooks
- Will be compatible with down to Internet Explorer 5
- Offers development mode
- Can be bundled together with your application
- Conditional css classes on elements
- Not terribly inefficient
- 0 dependencies
- Almost production ready
Small demonstation app, check out its source!
Just download the demo file above and modify it as you like!
When you are developing your app, use lui.dev.js
instead to get debugging stuff enabled.
Before serious consideration, check out the issues to see if there is some crucial feature not working (yet).
There are several ways to include lui into your project:
When you want to automatically include the latest version, just add the following to your HTML file:
<script src=https://l3p3.de/shr/lui.js></script>
When load speed, reliability or privacy is important enough, you should mirror the released file whereever you want. You can find it and some details on the releases page.
I will add lui to npm later so that you can simply npm install
it. The following method is not really recommended but a temporary workaround.
When you want to store everything in just one file (eg. by using closure compiler), just place src/lui.js
anywhere reasonable and import from it whenever you need to.
Please do NOT try to use the uncompiled src/lui.js
in production!
When using the standalone file mentioned above, that script file registers a global lui
object containing all functions mentioned here.
If you bundle lui together with your app, you can access these functions by a simple import
statement.
A component is a function which takes props and returns a list of its child components. By recursion, you can build up a html tree in a very flexible way. I also recommend reading React's explaination.
Here is a very simple component. It takes two props (one of which is optional) and it contains just one node.
function ColoredText({
color = 'red',
text
}) {
return [
node_html(
'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
html 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_list
instead of calling node
for each item to keep the invariant that nodes must have a stable order.
The leaves of your component tree are mostly made out of native html elements. To use such a component, use node_html
instead of node
. The signature is the same, except for the first argument being a descriptor, similar to css selectors: tag#id[attr=value][attr]
The tag is required but #id
or '[attr]' are optional. Having attributes in the descriptor instead of the props brings tiny performance improvements. The selector cannot be changed, so the attributes specified in it are somewhat constant which enables some optimizations.
Props are directly mapped to html attributes, except these 4 special 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. |
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 html element after it is created. |
S: Object<string, string> |
Pretty much the same as element.style, keys are the css properties, values their values. |
The component tree's root is defined by just the one and only call to init
. It gets a callback which then returns the body props and its childs.
The (virtual) body component is an html component, so the same rules as above apply to the props object returned by the root component. Except for the C
prop since body childs must be returned separately.
What we pass to init is pretty much a component without incoming props, a different return value and [early exit][#early-exit] is prohibited.
init(() => {
return [
{
S: {background: 'black'}
},
[
node(BlueText, {text: 'Hello, world!'})
]
];
});
This approach is neccessary since there is no body
component to use.
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_html
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;
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_first
and hook_rerender
) may be called anywhere in your component.
Using any hook in a callback is probably a very bad idea.
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_async |
- |
hook_callback |
useCallback |
hook_delay |
- |
hook_effect |
useEffect |
hook_first |
- |
hook_memo |
useMemo |
hook_object_changes |
- |
hook_prev |
- |
hook_reducer |
- |
hook_reducer_f |
useReducer |
hook_rerender |
- |
hook_state |
useState |
hook_static |
useRef |
hook_transition |
- |
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 html 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 html components.
If a component decides that it should not (yet) mount anything, it may return null;
(early-exit) at any time, even before some hooks were reached.
Function | Description |
---|---|
init(Body):void |
This mounts the body once, you give it the so-to-say body component. But unlinke actual components, you return the props for the body element and its content. So Body looks like this: function():[body_props: Object, body_content: Array<node>] |
node(Component, props: ?Object=, childs: ?Array<node>=):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. |
node_html(descriptor: string, props, childs):node |
When you want to add html components, use this function. It is very similar to node but needs a descriptor instead. |
node_list(Component, props: Object, data: Array) |
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. |
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. |
hook_async(function(...deps):Promise<T>, deps: ?Array):T |
If you need to wait for some data until it is available, use this instead of hook_memo . |
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. |
hook_delay(msecs: number):boolean |
Turns true after the specified delay. |
hook_effect(function(...deps):destroy, deps: ?Array):void |
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. |
hook_first():boolean |
This just tells you if this is the first time the component is being rendered. |
hook_memo(function(...deps):T, deps: ?Array):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. |
hook_object_changes(object):Array<string> |
This gives you a list of properties that changed since the last rendering. |
hook_prev(T, initial: T):T |
If you want to compare something to its version from the previous rendering, use this. At first rendering, initial is returned. |
hook_reducer(Array<function>):[value, dispatch] |
If you use a state that has some logic with it, use this. |
hook_reducer_f(reducer, initializer):[value, dispatch] |
This is a bit simpler than the array approach above. |
hook_rerender():void |
When this is called, this component will be rendered again next frame, only intended for animations. |
hook_state(T):[value, setter, getter] |
A simple component state. The first argument is the initial value. |
hook_static(T):T |
This is a much cheaper version of hook_memo : What you put in it the first time will always come out of it. |
hook_transition(target: number, msecs: number):number |
When target changes, the output number will smoothly pass to the new target, taking the specified time for that transition. |
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 or write me a mail.