Contents
Core Concepts
Understanding the core concepts of Carats will help you build applications more effectively.
JJSX
JJSX enables a pure HTML & JavaScript experience similar to PHP. It allows you to write JavaScript code inside TSX files similar to React, but without React hooks or React DOM. Because components are plain functions that return HTML, the same component renders identically on the server and in the browser.
Key Features
- Pure HTML & JavaScript syntax
- No React dependencies
- Automatic server and client-side rendering
JJSX compiles to a function call, so a component is an ordinary function that returns an HTML string. There is no virtual DOM to diff and no component instance to keep alive, which is why the same function is safe to run on the server and again in the browser.
export default function Greeting(props: { name: string }) {
return <p>Hello {props.name}</p>
}
Facets
Facets are the routing system in Carats. They are a single object that maps route patterns to page components, and the same object is used by both the client and the server, so a route can never drift between the two. Facets are defined in src/client/facets.cara.ts and may declare route parameters, suspense states and whether in-app routing is enabled.
Defining Facets
import { defineFacets } from '@carats/render';
import ErrorPage from './pages/_error';
import NotFound from './pages/_not_found';
import Home from './pages/home';
import Profile from './pages/profile';
export default defineFacets({
routes: {
'/': Home,
'/profile': Profile
},
suspense: {
error: ErrorPage,
notFound: NotFound
},
inAppRouting: true
});
defineFacets accepts a partial definition, so routes and suspense are both optional. The reference template only declares error and notFound; omit loading unless a page genuinely suspends.
A facet is also the unit of data fetching. A route in `routes` is the route a culet must be registered under, and a page that is wrapped with `Burnish` receives the value that culet returned.
Culets
Culets are server-side functions that provide data to components. They are defined in src/server/culets and seated in the server entrypoint. Components that need server-side data are burnished, which makes the framework fetch from their culet automatically. A culet is matched to a page by its route string, so the route of the culet and the route of the facet must be identical.
Defining a Culet
// src/server/culets/profile.ts
import { culet } from '@carats/ssr';
export default culet<User>('/profile', (req) => {
return {
id: '1',
name: 'John Doe',
username: 'johndoe',
email: 'john@example.com'
};
});
With a dynamic route, the parameter is read from `req.params`:
// src/server/culets/trade.ts
import { culet } from '@carats/ssr';
export default culet<TradeData>('/trade/:symbol', async (req) => {
const symbol = req.params.symbol.toUpperCase();
const quote = await fetchQuote(symbol);
return { symbol, price: quote.price };
});
Seating Culets
A culet defined in its own file is created with culet(route, handler) and then registered with seat(). seat returns the culet unchanged, so seating it is a pure registration step. Do not register the same culet a second time with another culet() call.
// src/server/entrypoint.ts
import { defineServerEntry, seat } from '@carats/ssr';
import facets from '../client/facets.cara';
import getTradeData from './culets/trade';
seat(getTradeData);
export default defineServerEntry(facets);
Server Props
Server props are the values a culet returns. The framework resolves them during server-side rendering, serializes them into the page, and passes them to the burnished component as its first argument. The component never fetches anything itself.
Server props arrive as the first argument of a burnished component:
import { Burnish } from '@carats/render';
import Layout from '../_layout';
export default Burnish<TradeData>(function (data) {
this.head = <title>Trade {data.symbol}</title>;
return (
<Layout>
<h1>{data.symbol}</h1>
</Layout>
);
});
A page that is not burnished simply receives no props, which is the normal case for a static page.
Rendering Model
Every page is rendered on the server first. The HTML template is filled in, the page component runs, and the resulting markup is sent to the browser together with the resolved server props. In the browser the same facets are mounted and the app takes over. A component function therefore runs in both places, which is why side effects must be deferred to afterMount or beforeMount.
The practical consequence is that statements in a component body run twice in a normal page load: once on the server, once in the browser. Anything that touches `document`, reads `window` or performs a side effect has to be deferred to a hook, otherwise server-side rendering will throw.
Client Runtime
@carats/csr mounts the facets, intercepts internal link clicks when in-app routing is enabled, and exposes the serialized server props. window.carats.ssp.for holds the route the current server props belong to, and window.carats.ssp.data holds the props themselves. Prefer reading them through the framework rather than parsing the HTML, and use goTo for programmatic navigation.
import { goTo } from '@carats/csr';
import { afterMount } from '@carats/hooks';
import './search-bar.sass';
export default function SearchInput() {
afterMount(() => {
const form = document.getElementById('search-bar');
const submit = (e: Event) => {
e.preventDefault();
const q = (document.getElementById('search-input') as HTMLInputElement).value;
goTo(`/search?q=${q}`);
};
form?.addEventListener('submit', submit);
return () => form?.removeEventListener('submit', submit);
});
return (
<form id="search-bar">
<input id="search-input" name="q" type="text" />
</form>
);
}
`goTo` navigates the same way an intercepted link click does, so no document reload happens when in-app routing is enabled.