Render Functions & JSX
Kdu recommends using templates to build applications in the vast majority of cases. However, there are situations where we need the full programmatic power of JavaScript. That's where we can use the render function.
If you are new to the concept of virtual DOM and render functions, make sure to read the Rendering Mechanism chapter first.
Basic Usage
Creating Knodes
Kdu provides an h()
function for creating knodes:
import { h } from 'kdu'
const knode = h(
'div', // type
{ id: 'foo', class: 'bar' }, // props
[
/* children */
]
)
h()
is short for hyperscript - which means "JavaScript that produces HTML (hypertext markup language)". This name is inherited from conventions shared by many virtual DOM implementations. A more descriptive name could be createKnode()
, but a shorter name helps when you have to call this function many times in a render function.
The h()
function is designed to be very flexible:
// all arguments except the type are optional
h('div')
h('div', { id: 'foo' })
// both attributes and properties can be used in props
// Kdu automatically picks the right way to assign it
h('div', { class: 'bar', innerHTML: 'hello' })
// props modifiers such as .prop and .attr can be added
// with '.' and `^' prefixes respectively
h('div', { '.name': 'some-name', '^width': '100' })
// class and style have the same object / array
// value support that they have in templates
h('div', { class: [foo, { bar }], style: { color: 'red' } })
// event listeners should be passed as onXxx
h('div', { onClick: () => {} })
// children can be a string
h('div', { id: 'foo' }, 'hello')
// props can be omitted when there are no props
h('div', 'hello')
h('div', [h('span', 'hello')])
// children array can contain mixed knodes and strings
h('div', ['hello', h('span', 'hello')])
The resulting knode has the following shape:
const knode = h('div', { id: 'foo' }, [])
knode.type // 'div'
knode.props // { id: 'foo' }
knode.children // []
knode.key // null
Note
The full KNode
interface contains many other internal properties, but it is strongly recommended to avoid relying on any properties other than the ones listed here. This avoids unintended breakage in case the internal properties are changed.
Declaring Render Functions
When using templates with Composition API, the return value of the setup()
hook is used to expose data to the template. When using render functions, however, we can directly return the render function instead:
import { ref, h } from 'kdu'
export default {
props: {
/* ... */
},
setup(props) {
const count = ref(1)
// return the render function
return () => h('div', props.msg + count.value)
}
}
The render function is declared inside setup()
so it naturally has access to the props and any reactive state declared in the same scope.
In addition to returning a single knode, you can also return strings or arrays:
export default {
setup() {
return () => 'hello world!'
}
}
import { h } from 'kdu'
export default {
setup() {
// use an array to return multiple root nodes
return () => [
h('div'),
h('div'),
h('div')
]
}
}
TIP
Make sure to return a function instead of directly returning values! The setup()
function is called only once per component, while the returned render function will be called multiple times.
If a render function component doesn't need any instance state, they can also be declared directly as a function for brevity:
function Hello() {
return 'hello world!'
}
That's right, this is a valid Kdu component! See Functional Components for more details on this syntax.
Knodes Must Be Unique
All knodes in the component tree must be unique. That means the following render function is invalid:
function render() {
const p = h('p', 'hi')
return h('div', [
// Yikes - duplicate knodes!
p,
p
])
}
If you really want to duplicate the same element/component many times, you can do so with a factory function. For example, the following render function is a perfectly valid way of rendering 20 identical paragraphs:
function render() {
return h(
'div',
Array.from({ length: 20 }).map(() => {
return h('p', 'hi')
})
)
}
JSX / TSX
JSX is an XML-like extension to JavaScript that allows us to write code like this:
const knode = <div>hello</div>
Inside JSX expressions, use curly braces to embed dynamic values:
const knode = <div id={dynamicId}>hello, {userName}</div>
create-kdu
and Kdu CLI both have options for scaffolding projects with pre-configured JSX support.
Although first introduced by React, JSX actually has no defined runtime semantics and can be compiled into various different outputs. If you have worked with JSX before, do note that Kdu JSX transform is different from React's JSX transform, so you can't use React's JSX transform in Kdu applications. Some notable differences from React JSX include:
- You can use HTML attributes such as
class
andfor
as props - no need to useclassName
orhtmlFor
. - Passing children to components (i.e. slots) works differently.
Kdu's type definition also provides type inference for TSX usage. When using TSX, make sure to specify "jsx": "preserve"
in tsconfig.json
so that TypeScript leaves the JSX syntax intact for Kdu JSX transform to process.
Render Function Recipes
Below we will provide some common recipes for implementing template features as their equivalent render functions / JSX.
k-if
Template:
<div>
<div k-if="ok">yes</div>
<span k-else>no</span>
</div>
Equivalent render function / JSX:
h('div', [ok.value ? h('div', 'yes') : h('span', 'no')])
<div>{ok.value ? <div>yes</div> : <span>no</span>}</div>
k-for
Template:
<ul>
<li k-for="{ id, text } in items" :key="id">
{{ text }}
</li>
</ul>
Equivalent render function / JSX:
h(
'ul',
// assuming `items` is a ref with array value
items.value.map(({ id, text }) => {
return h('li', { key: id }, text)
})
)
<ul>
{items.value.map(({ id, text }) => {
return <li key={id}>{text}</li>
})}
</ul>
k-on
Props with names that start with on
followed by an uppercase letter are treated as event listeners. For example, onClick
is the equivalent of @click
in templates.
h(
'button',
{
onClick(event) {
/* ... */
}
},
'click me'
)
<button
onClick={(event) => {
/* ... */
}}
>
click me
</button>
Event Modifiers
For the .passive
, .capture
, and .once
event modifiers, they can be concatenated after the event name using camelCase.
For example:
h('input', {
onClickCapture() {
/* listener in capture mode */
},
onKeyupOnce() {
/* triggers only once */
},
onMouseoverOnceCapture() {
/* once + capture */
}
})
<input
onClickCapture={() => {}}
onKeyupOnce={() => {}}
onMouseoverOnceCapture={() => {}}
/>
For other event and key modifiers, the withModifiers
helper can be used:
import { withModifiers } from 'kdu'
h('div', {
onClick: withModifiers(() => {}, ['self'])
})
<div onClick={withModifiers(() => {}, ['self'])} />
Components
To create a knode for a component, the first argument passed to h()
should be the component definition. This means when using render functions, it is unnecessary to register components - you can just use the imported components directly:
import Foo from './Foo.kdu'
import Bar from './Bar.jsx'
function render() {
return h('div', [h(Foo), h(Bar)])
}
function render() {
return (
<div>
<Foo />
<Bar />
</div>
)
}
As we can see, h
can work with components imported from any file format as long as it's a valid Kdu component.
Dynamic components are straightforward with render functions:
import Foo from './Foo.kdu'
import Bar from './Bar.jsx'
function render() {
return ok.value ? h(Foo) : h(Bar)
}
function render() {
return ok.value ? <Foo /> : <Bar />
}
If a component is registered by name and cannot be imported directly (for example, globally registered by a library), it can be programmatically resolved by using the resolveComponent()
helper.
Rendering Slots
In render functions, slots can be accessed from the setup()
context. Each slot on the slots
object is a function that returns an array of knodes:
export default {
props: ['message'],
setup(props, { slots }) {
return () => [
// default slot:
// <div><slot /></div>
h('div', slots.default()),
// named slot:
// <div><slot name="footer" :text="message" /></div>
h(
'div',
slots.footer({
text: props.message
})
)
]
}
}
JSX equivalent:
// default
<div>{slots.default()}</div>
// named
<div>{slots.footer({ text: props.message })}</div>
Passing Slots
Passing children to components works a bit differently from passing children to elements. Instead of an array, we need to pass either a slot function, or an object of slot functions. Slot functions can return anything a normal render function can return - which will always be normalized to arrays of knodes when accessed in the child component.
// single default slot
h(MyComponent, () => 'hello')
// named slots
// notice the `null` is required to avoid
// the slots object being treated as props
h(MyComponent, null, {
default: () => 'default slot',
foo: () => h('div', 'foo'),
bar: () => [h('span', 'one'), h('span', 'two')]
})
JSX equivalent:
// default
<MyComponent>{() => 'hello'}</MyComponent>
// named
<MyComponent>{{
default: () => 'default slot',
foo: () => <div>foo</div>,
bar: () => [<span>one</span>, <span>two</span>]
}}</MyComponent>
Passing slots as functions allows them to be invoked lazily by the child component. This leads to the slot's dependencies being tracked by the child instead of the parent, which results in more accurate and efficient updates.
Built-in Components
Built-in components such as <KeepAlive>
, <Transition>
, <TransitionGroup>
, <Teleport>
and <Suspense>
must be imported for use in render functions:
import { h, KeepAlive, Teleport, Transition, TransitionGroup } from 'kdu'
export default {
setup () {
return () => h(Transition, { mode: 'out-in' }, /* ... */)
}
}
k-model
The k-model
directive is expanded to modelValue
and onUpdate:modelValue
props during template compilation—we will have to provide these props ourselves:
export default {
props: ['modelValue'],
emits: ['update:modelValue'],
setup(props, { emit }) {
return () =>
h(SomeComponent, {
modelValue: props.modelValue,
'onUpdate:modelValue': (value) => emit('update:modelValue', value)
})
}
}
Custom Directives
Custom directives can be applied to a knode using withDirectives
:
import { h, withDirectives } from 'kdu'
// a custom directive
const pin = {
mounted() { /* ... */ },
updated() { /* ... */ }
}
// <div k-pin:top.animate="200"></div>
const knode = withDirectives(h('div'), [
[pin, 200, 'top', { animate: true }]
])
If the directive is registered by name and cannot be imported directly, it can be resolved using the resolveDirective
helper.
Functional Components
Functional components are an alternative form of component that don't have any state of their own. They are rendered without creating a component instance, bypassing the usual component lifecycle.
To create a functional component we use a plain function, rather than an options object. The function is effectively the render
function for the component.
The signature of a functional component is the same as the setup()
hook:
function MyComponent(props, { slots, emit, attrs }) {
// ...
}
Most of the usual configuration options for components are not available for functional components. However, it is possible to define props
and emits
by adding them as properties:
MyComponent.props = ['value']
MyComponent.emits = ['click']
If the props
option is not specified, then the props
object passed to the function will contain all attributes, the same as attrs
. The prop names will not be normalized to camelCase unless the props
option is specified.
Functional components can be registered and consumed just like normal components. If you pass a function as the first argument to h()
, it will be treated as a functional component.