Skip to content

An (Advanced) Guide to Code Components

Davo Galavotti edited this page Sep 12, 2019 · 1 revision

Hard fork from the paper doc An (Advanced) Guide to Code Components by Steve Ruiz

An (Advanced) Guide to Code Components

It’s true that the code components you write in Framer X are just React components, but that’s not really the whole story.

Code components in Framer X involve some special concerns. The Framer library provides some special components, such as Frame and Stack, that set a bar for what users of your components will expect. It also provides Property Controls for setting props from the canvas, and these can complicate your component’s props. And while creating a component for the canvas is easy enough, chances are you’ll be eventually create code components for use both on the canvas and inside of other code components. And there’s some art to creating a component that behaves just as well on the canvas as it does when created from code.

After writing many of these components myself, I’ve recognized a few patterns and best practices to that make it easy to make sure that things go smoothly. If you’re ready to go deeper into Framer’s code components, then this article should spare you some of the bruises I got bumping my way in the dark.

One last note: as the title suggests, this article is for more advanced users. If you’re new to React or to TypeScript, you’ll encounter some new terms and ideas without much context. That said, you might still learn a bit about how Framer’s components work by seeing how they break, too.

Code Component Boilerplate

First, here’s the sample code that you can modify for your own components. Below the fold, I’ll walk through the different patterns included in this boilerplate and why they’re there.

https://gist.github.com/steveruizok/3b10b31e1d059d2981bb0436dd6d7c09

https://gist.github.com/steveruizok/3b10b31e1d059d2981bb0436dd6d7c09

Imports

At the start of your file, you’ll need to import a few items from the Framer library. Here are the minimum, assuming you’ll be using a Frame as your component’s top-level container. (More on this below).

import * as React from "react"
import {
    Frame,
    FrameProps,
    addPropertyControls,
    ControlType,
} from "framer"

If you’re using other components from Framer’s library, then you’ll need to import them, too. For example, if your project involves Stack, add Stack to the list of imports.

import * as React from "react"
import {
    Frame,
    FrameProps,
    Stack, 
    addPropertyControls,
    ControlType,
} from "framer"

Props

Next, add a type for your component’s props.

type Props = -
    Partial<{
        // optional props
        photo: string
    }> & {
        // required props
        name: string
    }

Your components props will have three parts: the props of your container, a set of optional props, and a set of required props. Heres how I structure my props.

This example assumes that you have a Frame as your component’s top-level container. If your component uses some other component as a container, import that component’s props and use them instead. For example, if your project returned a Stack as its top-level container, then you’d add StackProperties to your list of imports and use those props instead.

import * as React from "react"
import { Stack, StackProperties, Frame } from "framer"

type Props = Partial<StackProperties> & {
    items: string[]
}

export function StackExample(props: Props) {
    const { items, ...rest } = props
    return (
        <Stack {...rest}>
            {items.map((item, index) => (
                <Frame key={index}>{item}</Frame>
            ))}
        </Stack>
    )
}

StackExample.defaultProps = {
    items: [],
}

Now on to your actual component.

To start, use the Props type alias to “type” the component’s props. This will help you out as you write your component, as well as if you (or anyone else) create this component later on from the JSX of another component.

export const UserCard = (props: Props) => {
      return <Frame/>
}

Container Props

In Framer, designers will expect your component to respond at least to the same props as Frame, as these include props for interaction, animation and events. For this reason, even if your component returns only HTML or components from outside of Framer’s library, it’s best to ensure that the top-level container is either a Frame, Stack, or some other of Framer’s components.

https://gist.github.com/steveruizok/f285c4c2f103cbc36c7d6546f036b3cb

https://gist.github.com/steveruizok/f285c4c2f103cbc36c7d6546f036b3cb Next, destructure out your component’s optional and required props, using the rest parameter to soak up all of the remaining props, which will be the props of our container. You then pass all of these props to our container using the spread operator.

This pattern prevents our custom props from being passed into our container (which would cause an error) while still allowing the component to accept the entire range of props, including animations and interactions, that the container supports. And it does so without much code, too.

Managing Prop Overrides

While it’s important to pass incoming props to your component’s container, you may also want to style or customize this container in a way that can’t be edited through props.

If you’re only setting defaults and want to allow a user the freedom to override these props, place them before the {...rest} spread.

export const UserCard = (props: Props) => {
    const { name, photo, ...rest } = props
    return <Frame borderRadius="100%" {...rest} >{name}</Frame>
}

However, if you want to make these props forced and impossible for a user to override, add the props after the {...rest} spread.

export const UserCard = (props: Props) => {
    const { name, photo, ...rest } = props
    return <Frame borderRadius="100%" {...rest} image={photo} >{name}</Frame>
}

In the examples above, the component would appear as a circle by default, but this borderRadius prop could be changed through Framer's overrides or in JSX. However, any override for the image prop would have no effect – the container’s image would always receive its value from props.photo.

Style

If we want to set a style on our container, we’ll need to make sure that the container’s style also still receives the content of props.style. Add style to your destructured props and then spread it back in at the end of your declared style object.

export const UserCard = (props: Props) => {
    const { name, photo, style, ...rest } = props
    return (
        <Frame {...rest} style={{ fontSize: 16, fontWeight: 600, ...style }} >{name}</Frame>
    )
}

This step is important because Framer relies on the style prop as part of its layout system. If a Frame is in a Stack and has a fractional height, it will receive a style value containing values for flexGrow and flexBasis and either height or width depending on the direction of the Stack.

This doesn’t cause any problems on the canvas, because component instances are wrapped in a component container (a feature of Framers canvas) and this container handles that style without passing it to the component.

However, when using a Stack in code, the component will need to incorporate these properties as shown above or else it will break the layout.

Dynamic Size

Unlike regular DOM elements like <div/>, Framer’s elements won’t grow or shrink to fit their content. This can make it problematic to use elements like Frame as your component’s container, especially if that component is meant to be included in a Stack.

Here’s a solution that I’ve used for components that need to resize based on their child content.

A big caveat here: there is currently no way to manipulate the height or width of the component’s container on the canvas. This limits the use of this dynamic size pattern to components that will be created through code, such as tabs, tokens, chips, or other in-line elements.

DefaultProps

Adding defaultProps creates a safety net for missing props when the component is created from code.

UserCard.defaultProps = {
  name: "Ivan Garcia-Kamp"
}

In Framer, defaultProps are also a way to determine the component’s intrinsic size. This size will set the component’s dimensions when it is first created on the canvas. Resized components may also be reset to their intrinsic size.

UserCard.defaultProps = {
  height: 320,
  width: 240,
  name: "Ivan Garcia-Kamp"
}

PropertyControls

Finally, property controls determine how a user can interact with the component on the canvas using Framer’s user interface. While there are some advanced tricks and patterns with property controls, that might need to wait for a different article.

addPropertyControls(UserCard, {
    name: {
        title: "Name",
        type: ControlType.String,
        defaultValue: "Ivan Garcia-Kamp",
    },
    photo: {
        title: "Photo",
        type: ControlType.Image,
    },
})

Performance

Like other React environments, getting the most out of a Framer X project is mostly a game of preventing expensive renders. While the majority of this work falls to our team’s engineers, there are some tricks available to you as you write your component.

RenderTarget

Framer X provides a helper, RenderTarget, for learning about where the component is rendering.

Components in Framer X may render in a few different places: either on the canvas, in a preview –whether Framer’s built-in Preview Window or in an exported Web Preview — or as thumbnail in the components list. And a component may also be rendering when the user is exporting images from their project.

In each case, you might want to render the component differently, skip more complex parts of the component’s code, or hide the component altogether.

switch (RenderTarget.current()) {
    case RenderTarget.thumbnail:
        // The component is rendering in the components list
        break
    case RenderTarget.canvas:
        // The component is rendering on the canvas
        break
    case RenderTarget.export:
        // The component is rendering for export
        break
    case RenderTarget.preview:
        // The component is rendering on the canvas
        break
    default:
        // ...
}

The RenderTarget helper also includes a property, hasRestrictions, that will flag true if the component is rendering in an environment where the app may stop rendering the component if it takes too long to render. (This is normally the canvas, but only in certain projects).

if (RenderTarget.hasRestrictions) {
  // ...
}

Memoization: useMemo and useCallback

Less specific to Framer, feel free to use React’s [useMemo](https://reactjs.org/docs/hooks-reference.html#usememo) and [useCallback](https://reactjs.org/docs/hooks-reference.html#usecallback) to cache the results of operations that aren’t necessary to re-calculate on each render.

const clonedChildren = React.useMemo(() => props.pages.map(page =>
        React.cloneElement(page, {
            ...page.props,
            top: 0,
            left: 0,
            height: "100%",
            width: "100%",
            size: "100%",
        })
    ), [props.pages]) 

If you’re passing a callback function to a component’s child, you’ll want to memoize that function (with useCallback) to prevent it from being seen as “new” by the child component.

Remember that a component will update whenever it receives “new” props. Confusingly, a “new” prop isn’t necessarily a different prop — two props might be identical in value but created at a different time.

In the example below, we have a Lockup component that returns a second component, Logo. When we tap on the Logo component, we’d run the toggle callback, which would update the toggled state. This would cause the component to update, too.

export function Lockup(props) {
    const { color, ...rest } = props
    const [toggled, setToggled] = React.useState(false)
   
    const toggle = () => {
        setToggled(!toggled)
    }

    return (
        <Frame background={toggled ? color : "none"}>
            <Logo onTap={toggle} />
        </Frame>
    )
}

The question is, would Logo update too?

In this example, the answer is yes.

We’re only passing a single prop to Logo , the toggle callback, and this callback never changes between updates. When the Lockup component updates, the prop sent to Logo is identical to its value on the previous update; however, because we’ve created the function during the update, React will still recognize it as a “new” prop.

In order to prevent this extra and unnecessary update, we’d need to use the useCallback hook to preserve a particular toggle callback so that each time the Lockup component updates, it can re-use the exact same function each time.

export function Lockup(props) {
    const { color, ...rest } = props
    const [toggled, setToggled] = React.useState(false)
   
    const toggle = React.useCallback(() => {
        setToggled(!toggled)
    }, [])

    return (
        <Frame background={toggled ? color : "none"}>
            <Logo onTap={toggle} />
        </Frame>
    )
}

The useCallback and useMemo hooks are there to give you some control over when something is calculated and when it is “new”. They let us put something into the system’s memory and then re-use a reference to that information on subsequent updates.

In both cases, it’s important to know that React doesn’t compare the content of two props in order to determine whether they’re the same. Instead, it really comes down to whether the two are literally the same — whether the two variables are pointing to the same place in system memory.

Exports

Framer will only pick up named exports of standard React component types. Default exports will not work. Decorated components will likely break as well.

If you’re using a library (such as mobx) that relies on decorating components, then your component file will need to return a standard component that returns the decorated component.

https://gist.github.com/steveruizok/c57908ab22bb36a11246b7e538fef469

https://gist.github.com/steveruizok/c57908ab22bb36a11246b7e538fef469

Writing a “social” component

Even if your component works, it may not work well with others. Prototyping in Framer X requires designers to move data around between components, so that events inside of one component may influence other components in the prototype.

Events

If your component maintains an internal state, it’s best to always share that state using an event callback prop.

https://gist.github.com/steveruizok/f0f1237c0cb04d1001bbdc0a3e1eb326

https://gist.github.com/steveruizok/f0f1237c0cb04d1001bbdc0a3e1eb326

Leaving out events will make it difficult (or even impossible) to use your component in a prototype, where state must be “lifted” into a Data object or higher state to which other components may respond.

Controlling components

In addition to sharing state changes with events, it can also be very useful to write your component so that it may receive a new state through its props.

https://gist.github.com/steveruizok/530aef965eb9b6c493dd03b06b714d56

https://gist.github.com/steveruizok/530aef965eb9b6c493dd03b06b714d56

This pattern allows a component to both send its state “up” to a higher state using an event and receiving it back “down” from that state through props.

Wrapping Up

Writing components in Framer X is fun! (TODO) 🎉

Clone this wiki locally