Skip to content

JSX Authoring

Every family tree in this section can also be written as JSX. Nothing about the model changes: a JSX tag calls the same component function, so zod validation, defaults, and identity props behave identically. The plain-function API stays primary; JSX is sugar for people (and reviews) that read component trees best.

Setup

Point your tsconfig.json at the runtime that ships inside brepjs-families:

json
{
  "compilerOptions": {
    "jsx": "react-jsx",
    "jsxImportSource": "brepjs-families"
  }
}

Name the file .tsx and you're done. No React involved: the runtime builds the same plain Element objects family() and el() do.

Components and intrinsics

Family components work as tags directly. The intrinsic vocabulary (Box, Cylinder, Geometry, Group) is exported as typed components, because JSX resolves capitalized tags to imports:

tsx
import { Box, Group, family, resolve, type FamilyChildren } from 'brepjs-families';
import { z } from 'zod';

const wallSchema = z.object({
  length: z.number().positive(),
  height: z.number().positive(),
  thickness: z.number().positive().default(200),
});

const Wall = family(
  'Wall',
  (p: z.output<typeof wallSchema>) => <Box size={[p.length, p.thickness, p.height]} />,
  { props: wallSchema }
);

const Storey = family<{ children?: FamilyChildren }>('Storey', (p) => <Group>{p.children}</Group>);

const tree = resolve(
  <Storey key="ground">
    <Wall key="south" length={4000} height={2700} />
    <Wall key="north" length={4000} height={2700} />
  </Storey>
);

Invalid props throw at element construction, exactly as on the function path. key works on every tag and is what key-path identity derives from, so keep keys on anything a BIM export will touch.

Children idioms

Children reach the render function as props.children, already flattened and cleaned, so the usual React idioms compose:

tsx
<Storey key="g">
  {showPorch && <Wall key="porch" length={2000} height={1100} />}
  {rooms.map((r) => (
    <Room key={r.id} width={r.w} depth={r.d} height={2700} />
  ))}
  <>
    <Wall key="a" length={100} height={100} />
    <Wall key="b" length={100} height={100} />
  </>
</Storey>

Conditionals that evaluate to false/null disappear, nested arrays flatten, and fragments inline without contributing a key-path segment. Declare a children prop as FamilyChildren to accept all of that; resolution hands your render a flat Element[].

Voids and openings

voids, fuse, and transform are props on every intrinsic, so the whole openings model is available in JSX with no change of meaning. A plain element in voids is an anonymous cut, geometry with no identity:

tsx
import { Box, family, resolve, tTranslate, type Element } from 'brepjs-families';

const Bin = family<{ length: number; width: number; height: number; wall: number }>('Bin', (p) => (
  <Box
    size={[p.length, p.width, p.height]}
    voids={[
      <Box
        size={[p.length - 2 * p.wall, p.width - 2 * p.wall, p.height - p.wall]}
        transform={[tTranslate([p.wall, p.wall, p.wall])]}
      />,
    ]}
  />
));

voids is a prop rather than children, and the split is semantic: children are contained by the host and carry their own key paths, voids are subtracted from it. Put an instance of a role: 'fill' family in voids and you get both, because resolution synthesizes an Opening between host and filler:

tsx
const Door = family<{ width: number; height: number; at: number }>(
  'Door',
  (p) => <Box size={[p.width, 300, p.height]} transform={[tTranslate([p.at, 0, 0])]} />,
  { role: 'fill' }
);

const Wall = family<{ voids?: readonly Element[] }>('Wall', (p) => (
  <Box size={[4000, 200, 2700]} voids={p.voids ?? []} />
));

resolve(<Wall key="south" voids={[<Door key="entry" width={1000} height={2100} at={1500} />]} />);
south                     the wall      Voids -> south/voids:entry
south/voids:entry         the opening   Fills -> south/voids:entry/fill
south/voids:entry/fill    the door

One children idiom does not carry over. voids takes a plain Element[] and is not run through children normalization, so a bare {show && <Door … />} entry is a type error rather than a node that disappears. Build the array instead:

tsx
const openings = show ? [<Door key="entry" width={1000} height={2100} at={1500} />] : [];
resolve(<Wall key="south" voids={openings} />);

When to prefer the function form

The two forms produce hash-identical trees, so this is style, not capability. Reach for plain calls when you're generating elements programmatically (loops over data, builders), and JSX when a human reviews the model shape: a storey of keyed walls reads like the building it describes.

Released under the Apache 2.0 License.