cherry-soda
v0.1.2-experimental
Published
Just another (super cool) JavaScript web framework.
Downloads
2
Readme
Yet another JavaScript framework that nobody needs. It has an SSR-first approach, and uses stateful, functional JSX
components to build apps. The components are rendered on the server, but contain state change handlers that are executed
in the browser. Instead of bundling the full component, cherry-soda extracts and bundles only the necessary code (the
event handler with its lexical scope, a template for client-side rendering, and styles) which can drastically reduce
bundle size. Therefore, by default (i.e. without using state change handlers), there is no client side JavaScript
whatsoever.
Currently, cherry-soda only runs on bun, Node compatibility is planned.
Warning Cherry-soda is experimental. Everything is subject to change.
Test the waters, dip a toe
If you just to test out cherry-soda, you can run the examples. For that you need to have Bun
installed. Then, clone the repository, install the dependencies with bun i
. Use cherry-soda's CLI to run an example:
cli/index dev example/cherry-soda-template/index.jsx
Get started
In a new Bun project install cherry-soda with bun i cherry-soda
, and add files src/index.js
and src/App.js
:
// src/index.js
import App from './App'
export function main() {
return <App/>
}
// src/App.js
export default function App() {
return (
<h1>Hello world!</h1>
)
}
index.js
is the main entry point for cherry-soda. It will look for an exported function main()
and will
use the returned value to render HTML. App.js
is an example component.
Then, add the cherry-soda JSX runtime to your tsconfig.json
/ jsconfig.json
:
{
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "cherry-soda"
}
}
Run cherry-soda dev src/index.js
to start the dev server. Then, visit localhost:3000
.
Alternatively, you can use the cherrySoda()
function in your own server to render the app. This also
automatically serves the asset files (JavaScript, CSS, images, etc.).
For Bun.serve:
// main.js
import cherrySoda from 'cherry-soda'
const cherrySodaApp = cherrySoda('src/index.js')
Bun.serve({
async fetch(req) {
const url = new URL(req.url)
if (url.pathname.startsWith('/api'))
return new Response() // your custom responses
return await cherrySodaApp(req)
},
port: 3000,
})
Guides
Add client-side code
In a function component typically all code is executed on the server. To execute code on the client you can use
the doSomething()
function. The callback provided will only be executed on the client. You can provide
states and/or refs to listen to in an array as the second parameter. If given, the callback will be called every time a
state or ref changes. To clean up, the callback may return a function, which will be called before the callback is
called immediately before a state change.
To refer to an element that the component returns you can use a ref with createRef()
, which you will
also need to pass in the array.
Inside doSomething()
a ref will be the actual node of the DOM. States can also be passed in the
dependency array. A state will be passed to the function as an array of the state value and a function to change the
state.
Here is an example to illustrate all those features:
import {createRef, createState, doSomething} from 'cherry-soda'
export default function Counter() {
// create a state with an initial value `0`
/* the returned value is an "State" object that can be used
* as a child, prop value, as is or with ".use()"
*/
const count = createState(0)
// two refs for the two buttons
const addButton = createRef()
const subtractButton = createRef()
/* "doSomething" takes the function (client-side code) as the first
* and an array of dependencies as the second parameter.
* The dependencies will be fed into the function in the same order as provided.
* The "count" state gets converted into a (client-side) state
* and a function to change the state's value.
*/
doSomething(([count, setCount], addButton, subtractButton) => {
/* "addButton" and "subtractButton" are now just DOM elements
* and not refs anymore.
*/
addButton.addEventListener('click', () => {
setCount(count + 1)
})
subtractButton.addEventListener('click', () => {
setCount(Math.max(count - 1, 0))
})
}, [count, addButton, subtractButton])
return <>
{/* The ref object must be passed here as prop "ref" to assign this node */}
<button ref={addButton}>+</button>
{/* The state object can be used here just like that.
It'll be converted to a number (or rather a string) internally. */}
<span>Count: {count}</span>
<button ref={subtractButton}>-</button>
</>
}
Route on the server, and the client will route, too
Section you app with <Island>
s
Reference
Rendering and function components
Rendering
To render an app, you can use the cherrySoda()
function. It returns a request handler for Bun.serve()
and handles
compiling / building and watching all the files belonging to your app.
Parameters:
entry: string
Absolute path to the entry file
Returns:
(req: Request) => Response
Entry file
Every cherry-soda app has a single entry file. This file exports a function main()
, which returns the main function
component (usually called <App/>
). If this component does not yield a <html>
tag, cherry-soda will automatically
wrap the resulting HTML in a standard document.
Function components
Apps are built with stateful function components. Each component is a function that accept props as a parameter and
return JSX element/s. All code in a function component gets executed on the server.
Internally, function components are called once on startup in production mode, and immediately after they are changed in
development mode. This can cause unexpected effects for example when a function component writes to a database. This is
why you should use sideEffect()
for any non-deterministic server-side code and code that must be
executed during the render.
If you want to execute code for a component in the browser, use
doSomething()
.
Cherry-soda collects the code given as the callback to doSomething()
at build time and bundles it into a single file
together with code from other doSomething()
s and cherry-soda's runtime.
This function lets you execute code in the browser. The function callback
and its lexical scope get extracted by
cherry-soda's compiler and bundled into the frontend JavaScript. All refs and states that are used
in the callback
should be passed in the recallOn
array.
The values you passed in the array will be passed in the same order into the callback
function on the client. If a ref
is passed into the array, the passed value for the function will be the matching HTML element. If a state is passed into
the array, the passed value for the function will be an array with the value as the first entry and a function for
changing the state value as the second entry.
The callback function may return another function. This (returned) function will be called before a state changes
value (after calling the function to change the state value). You can use this function to clean up if you need to.
Parameters:
callback: (...args: any[]) => void | Function
A function that is executed on the client. The function may return another function. The returned function will be called anytime the component's elements are removed from the DOM.recallOn: (State | Ref)[]
An array of states, whose values are listened to and trigger the callback when they change.
This function lets you execute code on render-time. On startup, or when a file is changed in development, cherry-soda
compiles the app's components into templates, which are used for rendering. This essentially bakes any dynamic content
into the template. Use states to include dynamic content into your app. You can set the value of the state
with doSomething()
on the client, or with sideEffect()
on the server.
For setting state values inside the callback function, sideEffect()
accepts an array of states that are updated by the
callback as its second argument. These states are passed as arrays with the value as the first entry and a function for
changing the state to the callback, similarly to doSomething()
.
Parameters:
callback: (...args: any[]) => void
A function with the code that is executed everytime the app is rendered.recallOn: State[]
An array of states that should be passed to callback (as[value, setValue()]
pairs).
Refs
Refs are a way to work with the DOM nodes that your function components return. To use, get a reference instance by
calling the createRef()
function, and pass it to the desired element with the ref
parameter. When you
pass the ref object in the recallOn
array of doSomething()
cherry-soda will pass the actual DOM
element to the callback on the client. You can also pass one ref to multiple elements.
If you do that, cherry-soda will pass a HTMLCollection
containing the respective DOM elements inside
the doSomething()
callback.
Returns a new Ref
. Pass this to an element like so:
import {createRef} from 'cherry-soda'
function Component() {
const myRef = createRef()
return <div ref={myRef}/>
}
Returns:
Ref
A newRef
instance.
States
You can create states with createState()
. This will return a State
object that holds a value.
Passing this state into doSomething()
or sideEffect()
will convert it into an array
in which the first entry in the state object and the second entry is a function for changing the value of the state.
Creates a State
object with the given value.
Parameters:
initialValue: any
The initial value for this state.
Returns:
State
AState
object withinitialValue
as its value.
State
(Server-Side)
The State
object holds the initial value of the state and can be passed into doSomething()
or sideEffect()
or used in the DOM by using it like a value:
import {createState} from 'cherry-soda'
function Component() {
const myState = createState('foo')
return <div id={myState}>
{myState}
</div>
}
Sometimes, you don't want to use the states value directly, but a transformed version of it. To do that, use
the .use()
method. It takes a callback which itself receives the state value as a parameter:
import {createState} from 'cherry-soda'
function Component() {
const elementType = createState('a')
const label = 'foo'
return <>
{elementType.use(tagName => {
if (tagName === 'a')
return <a>{label}</a>
else
return <span>{label}</span>
})}
</>
}
You can also use multiple states in one .use()
by concatenating them with .and()
. The state's values are passed in
the order they were concatenated in:
import {createState} from 'cherry-soda'
function Component() {
const revenue = createState(5)
const expenses = createState(3)
return <>
Profit: {revenue.and(expenses).use((a, b) => a - b)}
</>
}
Fun fact: using
myState
is the same asmyState.use()
is the same asmyState.use(value => value)
Location and Routing
Essential built-in components
Cherry-soda provides some built-in components that help you manage the basic document structure.
<Html>
Renders a <html>
element and manages the lang attribute (if you're building a multilingual app).
<Head>
Renders a <head>
element and manages the loading of scripts and assets. It can also automatically generate metadata
for SEO, and icons from your given configuration. You can pass your own elements into <Head>
. These will just be
rendered inside the <head>
and replace the generated elements if any.
For example:
import {Html, Head, Body} from 'cherry-soda'
function App() {
return (
<Html>
<Head>
{/* title element overrides the default <title>Title</title> */}
<title>My App</title>
</Head>
</Html>
)
}
<Body>
Renders a <body>
element.
<Bundle>
Renders the necessary <link>
/ <style>
, and <script>
elements to import generated JavaScript and CSS. Use this if
you do not want to manage them yourself and also do not want to use <Head>
.