JSX Templating
Sapling utilizes JSX for defining UI components and rendering HTML content. This approach allows you to write HTML-like syntax directly within your JavaScript or TypeScript code, providing a powerful and familiar way to build interfaces. Sapling leverages Hono's JSX implementation (@hono/hono/jsx
) for this purpose. It is based on Hono's JSX implementation, which you can learn more about here.
Basic JSX Syntax
JSX lets you write markup that looks similar to HTML:
import { FC } from "@hono/hono/jsx"; // Optional for typing components
// Define a simple functional component
function Greeting({ name }: { name: string }) {
// Return JSX elements
return (
// Tailwind CSS classes are used for styling
<div className="flex flex-col items-center justify-center h-screen">
<h1 className="text-2xl font-bold">Hello, {name}!</h1>
</div>
);
}
// Usage:
// <Greeting name="World" />
Key differences from HTML:
- Attribute names use camelCase (e.g.,
className
instead ofclass
,onClick
instead ofonclick
). - JavaScript expressions can be embedded using curly braces
{}
. - Components are typically defined as functions or classes.
Dynamic Content Interpolation
Embed JavaScript expressions directly within your JSX using curly braces:
function UserProfile({ user }: { user: { name: string; age: number } }) {
const profileUrl = `/users/${user.name.toLowerCase()}`;
return (
<div>
<h2>{user.name}</h2>
<p>Age: {user.age}</p>
<a href={profileUrl}>View Profile</a>
</div>
);
}
Safety Features
JSX automatically escapes interpolated string values to prevent XSS attacks when rendering them as text content.
const userInput = '<script>alert("XSS")</script>';
// Renders as plain text: <div><script>alert("XSS")</script></div>
<div>{userInput}</div>
Note: This automatic escaping only applies to content within curly braces {}
used as element children or attribute values. Setting HTML content directly requires specific methods (see dangerouslySetInnerHTML
).
Components
Break down your UI into reusable components. Components are typically functions that accept props
(properties) as an argument and return JSX.
import { FC } from "@hono/hono/jsx"; // Optional but recommended for type safety
interface HeaderProps {
siteTitle: string;
}
const Header: FC<HeaderProps> = ({ siteTitle }) => {
return <header><h1>{siteTitle}</h1></header>;
};
const Page: FC = () => {
return (
<div>
<Header siteTitle="My Awesome Site" />
<main>Page Content</main>
</div>
);
};
Fragments
If you need to return multiple elements from a component without adding an extra wrapper div
, use Fragments:
import { Fragment } from "@hono/hono/jsx";
function UserInfo({ name, email }) {
return (
<Fragment>
<h2>{name}</h2>
<p>{email}</p>
</Fragment>
// Short syntax: <>...</> also works
// <>
// <h2>{name}</h2>
// <p>{email}</p>
// </>
);
}
Rendering Raw HTML
Sometimes you need to render HTML content that comes from an external source (like a CMS or markdown renderer) without escaping. Use this feature with extreme caution as it can expose your application to XSS attacks if the content is not properly sanitized beforehand.
JSX provides the dangerouslySetInnerHTML
prop for this:
import { renderMarkdown } from "@sapling/markdown"; // Example markdown renderer
async function Article({ markdownContent }: { markdownContent: string }) {
// Assume renderMarkdown sanitizes the output or the source is trusted
const htmlContent = await renderMarkdown(markdownContent);
return (
<article
dangerouslySetInnerHTML={{ __html: htmlContent }} // Content is inserted as-is
/>
);
}
Key Points:
- The prop name includes "dangerously" to remind you of the risks.
- It accepts an object with a
__html
key, whose value is the raw HTML string. - Always sanitize HTML content before using
dangerouslySetInnerHTML
unless it comes from a completely trusted source.
Conditional Rendering
Use standard JavaScript conditional logic (like ternary operators or &&
) within your JSX:
function ConditionalContent({ isLoggedIn }: { isLoggedIn: boolean }) {
return (
<div>
{isLoggedIn
? <button>Logout</button>
: <button>Login</button>
}
{/* Render only if logged in */}
{isLoggedIn && <p>Welcome back!</p>}
</div>
);
}
List Rendering
Render lists of items by mapping over an array and returning JSX for each item. Remember to include a unique key
prop for each list item to help efficiently update the DOM.
function ItemList({ items }: { items: { id: string; text: string }[] }) {
return (
<ul>
{items.map(item => (
<li key={item.id}>{item.text}</li> // Use a stable, unique key
))}
</ul>
);
}
Best Practices
- Component Composition: Build complex UIs by composing smaller, reusable components.
- Props for Data Down: Pass data down from parent to child components via props.
- Type Safety: Use TypeScript with
FC
or interface definitions for props to catch errors early. - Keys for Lists: Always provide stable
key
props when rendering lists. - Minimize Raw HTML: Avoid
dangerouslySetInnerHTML
whenever possible. If used, ensure rigorous sanitization. - Keep Components Focused: Aim for components that do one thing well.
Security Considerations
- JSX automatically escapes dynamic text content, preventing many common XSS vulnerabilities.
dangerouslySetInnerHTML
bypasses this escaping. Only use it with trusted, sanitized HTML.- Always validate and sanitize any user-generated content before rendering it, especially if using
dangerouslySetInnerHTML
. - Be cautious when rendering content fetched from external APIs or third-party sources.