Skip to content
weftGitHub

LearnDescribing the UI

Your first component

Components are the pieces you build a weft interface from. A button, a product card, a whole page: each is a component, and bigger components are made of smaller ones. If you've written React, you already know how they look, because a weft component is the same thing on the surface: a function that returns TSX. What happens underneath is different, and this page shows you both.

Two greetings

Hello, Ana. First visit.

Hello, Ben. Visit 4.

import { Card } from '@weft-js/warp'function Greeting(props: { name: string; visits: number }) {  return (    <p>      Hello, <strong>{props.name}</strong>.{' '}      {props.visits > 1 ? `Visit ${props.visits}.` : 'First visit.'}    </p>  )}export default function Welcome() {  return (    <Card title="Two greetings">      <Greeting name="Ana" visits={1} />      <Greeting name="Ben" visits={4} />    </Card>  )}

Try this: Nothing to press: read the code. Greeting runs once for each place it is used.

Components: pieces of UI you can reuse

On the web, HTML gives you tags such as <article>, <h1> and <ul> to build a page from:

html
<article>  <h1>My first component</h1>  <ol>    <li>Components: UI building blocks</li>    <li>Defining a component</li>    <li>Using a component</li>  </ol></article>

That markup is a page's structure: the article, its heading, a short table of contents. As a site grows, the same structures appear again and again: a card for every product, a row for every message, the same header on every page. A component lets you write a piece of markup once, with the logic that belongs to it, give it a name, and use it wherever you need it.

You can mix your own components with plain HTML tags and with components other people wrote. weft comes with warp, a set of ready components such as Button, Card and Input, and the demos on these pages use them alongside plain tags.

Defining a component

A component is a JavaScript function whose name starts with a capital letter and which returns markup. Here is one:

tsx
export default function Profile() {  return <img src="https://i.imgur.com/MK3eW3As.jpg" alt="Katherine Johnson" />}

You build it in three steps.

Step 1: Export the component

export default makes the function the file's main export, so another file can import it. A route file in src/routes must export its page this way. The next page, Importing and exporting components, covers exports in full.

Step 2: Define the function

function Profile() { } defines a function called Profile.

Step 3: Add the markup

The function returns <img /> with src and alt attributes. It looks like HTML, but it is TSX: markup inside TypeScript. The return can fit on one line, or span several inside parentheses:

tsx
export default function Profile() {  return (    <div>      <img src="https://i.imgur.com/MK3eW3As.jpg" alt="Katherine Johnson" />    </div>  )}

Using a component

Once Profile exists, other components can use it. Here Gallery uses it three times, with a different name and field each time. Values you hand a component like this are called props, and Passing props covers them.

Amazing scientists
KKatherine Johnson, mathematician
MMario Molina, chemist
HHedy Lamarr, inventor
import { Badge, Card, Row, Stack } from '@weft-js/warp'// One component, defined once at the top level of the file.function Profile(props: { name: string; field: string }) {  return (    <Row gap="related">      <Badge emphasis="solid" tone="accent">{props.name[0]}</Badge>      <span>        <strong>{props.name}</strong>, {props.field}      </span>    </Row>  )}// Another component that uses it three times.export default function Gallery() {  return (    <Card title="Amazing scientists">      <Stack gap="related">        <Profile name="Katherine Johnson" field="mathematician" />        <Profile name="Mario Molina" field="chemist" />        <Profile name="Hedy Lamarr" field="inventor" />      </Stack>    </Card>  )}

Try this: Add a fourth Profile to Gallery. Profile is written once and used as many times as you like.

What the browser sees

Notice the difference in case:

  • <figure> and <strong> are lowercase, so they are HTML tags.
  • <Profile /> starts with a capital P, so it is the Profile component.

Profile itself contains more HTML: a <figure>, a <span>, a <figcaption>. In the end the browser sees only HTML elements. There is no <Profile> element in the page; the component has done its work and left its markup behind.

Nesting and organising components

Components are ordinary functions, so you can keep several in one file. That's handy while they are small and related. When a file gets crowded, move a component to its own file and import it. Because Profile is used inside Gallery, Gallery is called the parent and Profile the child: this parent and child relationship is the shape of your UI, and Your UI as a tree looks at it closely.

TSX is markup with expressions

Inside TSX, curly braces open a window back into JavaScript. Any expression can go there: {props.name}, a ternary, a function call. Attributes take expressions the same way:

tsx
export function Greeting(props: { name: string }) {  return <p class="greeting">Hello, {props.name}.</p>}

Both class and className work, and so do React's camelCase names such as tabIndex and htmlFor. The rules of TSX have their own page, Writing markup with TSX, and braces get theirs in JavaScript in TSX with braces.

The body runs once

This is the one big difference from React. weft calls a component function once, when that part of the page first appears, and never calls it again. When a value changes later, nothing re-renders: the compiler has already wired each {expression} and each attribute to the DOM node it fills, so that one node is written and nothing else runs.

That's why there are no hook rules and no dependency arrays in weft. Code in the body runs once, top to bottom, and sets things up. The live parts are the expressions in your markup, plus anything you put in derived or watch.

It also means that a value computed in the body is computed once:

tsx
import { derived } from '@weft-js/core'export function Total(props: { price: number; qty: number }) {  // const total = props.price * props.qty  would be worked out once and never change: the compiler reports W001.  const total = derived(() => props.price * props.qty)  return <p>Total: {total.value}</p>}

Work values out in the markup itself ({props.price * props.qty}) or with derived (see Derived values). If you compute from a prop or a piece of state in the body, the compiler stops you with W001 and offers the derived version as a fix. Components run once is the page on this idea, with the mistakes it catches.

Deep diveWhat the compiler does with a component

weft compiles each .tsx file ahead of time with its own compiler (written in Rust, and shipped as WebAssembly too, so you never need Rust installed). For a component it does roughly this:

  1. The static markup becomes an HTML template, parsed by the browser once and cloned for each use. Cloning a template is far cheaper than building elements one at a time.
  2. Each {expression} in the markup becomes a small binding attached to the exact text node or attribute it fills. The binding records what it read (props.name, count.value) and runs again when one of those changes.
  3. Each prop you pass becomes a getter on the props object, unless it is a plain literal. <Price dollars={bill.value / 2} /> hands Price an object whose dollars works out bill.value / 2 when it's read, so Price sees the new value without running again.
  4. The component itself is called once, with nothing tracked while it runs. Reads in the body don't subscribe the body to anything, which is why a read there can't update and why the compiler flags it.

React does the opposite: it calls your component again on every change, compares the new markup with the old, and writes the difference. Both give you a page that follows your data. weft gets there without running your component again or comparing anything, so an update costs the same however big the component is.

Recap

  • A component is a function whose name starts with a capital letter and which returns TSX.
  • Use it as a tag, <Profile />, inside other components. Define every component at the top level of a file.
  • export default makes it the file's main export, and a route file in src/routes exports its page that way.
  • The body runs once. {expressions} and attributes in TSX stay up to date by themselves.
  • Compute in the markup or with derived, not in the body (W001).

Try it

Challenge 1

Add a third greeting

In the first demo on this page, add a Greeting for Cy, on their second visit.

Show the solution
tsx
<Greeting name="Cy" visits={2} />

visits is more than 1, so it reads "Visit 2."

Challenge 2

Export the component

This file defines a component but nothing else can use it, and a route file must export its page. Fix it.

<!-- example: skip the starting point, deliberately missing its export -->

tsx
function Profile() {  return <img src="https://i.imgur.com/lICfvbD.jpg" alt="Aklilu Lemma" />}
Show the solution
tsx
export default function Profile() {  return <img src="https://i.imgur.com/lICfvbD.jpg" alt="Aklilu Lemma" />}

export default makes Profile the file's main export, so another file can write import Profile from './Profile'.

Challenge 3

Fix the return statement

This component shows nothing. Why?

<!-- example: skip the starting point, deliberately broken -->

tsx
export default function Profile() {  return    <img src="https://i.imgur.com/jA8hHMpm.jpg" alt="Katsuko Saruhashi" />}
Show the solution

JavaScript ends a return at the line break, so the function returns undefined and the <img> below is never reached. Keep the markup on the return line, or wrap it in parentheses that open on that line:

tsx
export default function Profile() {  return (    <img src="https://i.imgur.com/jA8hHMpm.jpg" alt="Katsuko Saruhashi" />  )}

Challenge 4

Your own component

Write a Congratulations component from scratch that shows an <h1> saying "Good job!", and export it.

Show the solution
tsx
export default function Congratulations() {  return <h1>Good job!</h1>}