Prelude lets you develop web applications in a familiar component-based functional style. It is build with the desire to have a lightweight frontend framework that works using just JavaScript but that nontheless can handle complex web applications without sacrificing on developer expierence.
Prelude works with most popular JavaScript runtimes: Node, Deno, Bun or the borwser.
It is available on NPM and JSR under the package named @wrnrlr/prelude
.
The quickest way to get started with Prelude is uing the Playground app on the homepage.
It offers a IDE complete with a code editor, live preview and a number of examples.
Aternativaly you can develop on your local machine using vite
.
Some Prelude APIs can also be used in the REPL to expore their behaviour interactively.
This is a example of a button that increments a counter when it is clicked.
<!DOCTYPE html>
<title>Counter</title>
<script type="module">
import {h, signal, render} from 'https://esm.sh/@wrnrlr/prelude'
function Counter() {
const n = signal(1)
return h('button', {onClick: e => n(n=>n+1)}, n)
}
render(Counter, document.body)
</script>
Prelude does not use JSX or a templating language to descript html instead we use a DSL called HyperScript.
The h
function is used in either of two ways based on the type of the first argument,
when it is a string it will create a html element like ,
and when it is a function it will create a reactive component.
h('div',{},[
h('label','Name'),h('input',{})
])
Prelude tries to integrate with the existing web APIs as much as possible, handeling user events is no different, use a function to the event callback.
In the following example we listen for onclick
events for a button, and increment the value of the n
signal.
h('button', {onClick:e => n(i => i + 1)}, n)
Be adviced, the event handler MUST always have one argument even if this is not being used, lest HyperScript confuses it for a signal and ignores the events.
// Ok
h('button', {onClick: e => console.log('Ok')})
h('button', {onClick: _ => console.log('Ok')})
// This event handler will be ignored
h('button', {onClick: () => console.log('Wrong')})
A signal is an object that holds a value with a setter to update this value and a getter that returns this value whenever it is updated.
Create signal.
// Create a signal with value one
const n = signal(1)
// Get value from signal
n()
// Set value for signal
n(2)
// Set value with an update function
n(i=>i+1)
// Derived signal
const n2 = () => n() * 2
The effect
function lets you subscribe to signals and perform side-effects whenever the signal chages.
const a = signal(1), b = signal(2)
effect(() => console.log('a+b', a()+b()))
const c = () => a()+b()
effect(() => console.log('c', c()))
a(i => i+1)
The memo
function caches the result of the function passed to it.
const n2 = memo(() => n() * 2)
h(Show, {when:() => n()%2 === 0, fallback:'odd'}, 'even')
It is also possible to conditionally render a component by prefixing it with a JavaScript and-expression, like in the example below,
but using Show
is going to be faster.
h('',show&&'Hi')
h(List, {each:['a','b','c']}, (v,i)=>`${i()}:${v()}`)
The resource()
function lets you define a asynchronous signal.
resource(async ()=>getPosts())
Prelude supports dependency injection with the contect
and useContext
APIs.
const CounterCtx = context()
const useCounter = () => useContext(CounterCtx)
function CounterProvider(props) {
const count = signal(0)
const increment = () => count(i=>i+1)
return h(CounterCtx.Provider, {value:[count,increment]}, props.children)
}
function Counter() {
const [n, increment] = useCounter()
return h('button', {onClick:e=>increment()}, n)
}
function
function App() {
h(CounterProvider, h(Counter))
}
h(Router,[
{path:'/', component:Posts},
{path:'/user', component:Users}
])