A core principle behind Subframe’s code export flow is that business logic (like making an API call or managing React state) belongs in code, not a design tool. Thus, Subframe does not export any business logic, other than UI-specific interactions like animating an accordion open / close.

Let’s consider a hypothetical music streaming app called Spoofy to demonstrate integrating business logic into Subframe pages and components.

We’ve made a CodeSandbox project for the examples below so you can play around with the code.

For our example app, we’ll build a simple magic link login form. This is what we want the end-result to look like:

We’ve finished copying the page code from Subframe, so now let’s add some basic functionality.

  1. We add a fake fetch function that simulates a network request and a handleSubmit function that handles the form submission.
  2. We also want to update our Button to show that we’re currently submitting the form, so we’ll maintain a submitting state variable and pass it to the Button in the disabled and loading props.

You can see the changes we made in the highlighted lines below:

import { DialogLayout } from "@/ui/layouts/DialogLayout"
import { TextField } from "@/ui/components/TextField"
import { Button } from "@/ui/components/Button"

function App() {
  return (
    <DialogLayout open={true}>
      <div className="flex h-full w-full flex-col items-center gap-6 bg-default-background px-6 py-12">
        <span className="text-heading-2 font-heading-2 text-default-font">Magic Link Login</span>
        <form className="flex w-full flex-col items-start gap-2">
          <TextField className="h-auto w-full flex-none" label="Email">
            <TextField.Input required name="name" />
          </TextField>
          <Button
            type="submit"
            className="h-8 w-full flex-none"
            variant="brand-secondary"
            icon="FeatherSend"
            iconRight={null}
            disabled={false}
            loading={false}
          >
            Send Magic Link
          </Button>
        </form>
      </div>
    </DialogLayout>
  )
}

export default App

We now have an interactive login form!

Example 2 - Interactive track cards

We now want to build a feature where we show the user their most-listened-to tracks of the past week and allow them to favorite tracks so they can easily find them later.

When adding business logic to Subframe-designed components, we recommend not editing the generated code inside the src/ui/ directory directly, but instead wrapping the component.

We built a TrackCard component and FavoriteTracks page in Subframe and brought them to our codebase:

Now we’ll add the functionality to be able to favorite tracks:

  1. We’ll wrap the original TrackCard component with a new component that will manage the simple business logic.
  2. We’ll use this wrapper component in our FavoriteTracks page which manages the data, the favorite state, and provides the onClick handlers.
import { HTMLAttributes, useMemo } from "react"
import { TrackCard as SubframeTrackCard } from "@/ui"
import { Image } from "./Image"

export interface TrackCardProps extends HTMLAttributes<HTMLDivElement> {
  title: string
  artist: string
  length: number // in seconds
  genre: string
  coverImage: string
  isFavorite: boolean
  onFavoriteClick: () => void
}

function lengthToString(length: number): string {
  const minutes = Math.floor(length / 60)
  const seconds = length % 60
  return `${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`
}

const TrackCard = ({ title, artist, length, genre, coverImage, isFavorite, onFavoriteClick }: TrackCardProps) => {
  // Convert track length in seconds to MM:SS format
  const formattedLength = useMemo(() => lengthToString(length), [length])
  // Determine the icon based on favorite status
  const icon = useMemo(() => (isFavorite ? "FeatherHeartOff" : "FeatherHeart"), [isFavorite])

  return (
    <SubframeTrackCard
      title={title}
      artist={artist}
      length={formattedLength}
      favoriteButtonSlot={<SubframeTrackCard.FavoriteButton icon={icon} onClick={onFavoriteClick} />}
      imageSlot={<Image src={coverImage} width={500} height={500} />}
      genre={genre}
    />
  )
}

export default TrackCard

And we’re done! The end result looks like this:

You may have noticed that we’re using slots for our image and favorite button. We’ll cover the reasons for this in Props and Slots.