An inside peek at the development of Forma 36 v4

There’s a famous saying about mistakes. “You must learn from the mistakes of others. You will never live long enough to make them all yourself.”

The joke has been attributed to many historical figures over the years, ranging from Eleanor Roosevelt to Groucho Marx. The truth is that it first appeared in the book “Human Engineering” by Harry Myers and Mason M. Roberts in 1932.

Regardless of who said it, I’ve often thought about this statement during the weeks and months leading up to the official release of Forma 36 v4. Because from my perspective, it’s been an amazing journey with many obstacles and challenges, and everyone who contributed to this major milestone learned a great deal along the way.

Forma 36 is Contentful’s open-source design system which we built to reduce the overhead of creating User Interfaces (UI) for custom apps in Contentful – as well as for Contentful itself. This is achieved by providing tools and guidance for digital teams who are building and extending our content platform to customize it to their needs.

The Forma 36 team had many detailed discussions about what we’d be doing for the next major release. We knew that our users were facing some difficulties when working with our API and we really wanted to address their concerns. We had a clear understanding of what areas of improvement could be made, but we weren’t 100% sure how to approach them. In this blog post, I’ll outline how the team dealt with a series of problems, and our key takeaways from the experience.

For v4, the Forma 36 team had three immediate priorities:

  1. A11Y: Our focus for this release was to bring crucial accessibility improvements to our React components. We believe that the web should be for everyone, so v4 brings Web Content Accessibility Guidelines (WCAG) level AAA compliance in all of our React components, except for our color palette which is optimized for level AA. Many components have been updated to fulfill the a11y requirements, so that most accessibility issues experienced by customers were resolved in this release.

  2. Performance: The next priority was to revise the way the library is built. In v4, we’ve split components into separate packages. This means that now you can use only the components you need, without adding the full suite of components to your bundle. For example, let’s say you’re importing just a button component; “tree shaking” will ensure that only this component ends up in your bundle. This makes your bundle size much smaller and improves the performance of your apps. After introducing this change, comparing the import of only one component, the bundle is around 85% smaller than v3! This is a huge performance improvement, especially for customers who have built more than one app.

  3. Developer experience: In v4, we’re providing a bunch of improvements around the API, which is now aligned to our code style guide. This makes it much easier for developers used to working with Contentful’s APIs to build their own apps. Additionally, we dropped the PostCSS and introduced Emotion into our components, so there is no need to import separate CSS files into a project. Better documentation, guidelines, and examples are already in place, and the team is continuously improving them to provide the best experience when building apps.

Now that the release is generally available, I’d like to share with you some observations from behind the scenes.

1. Expect the unexpected

At the beginning of a migration project of this size, when preparing to move from one version to the next, it’s almost impossible to anticipate all the choices you’ll have to make. And that’s okay. We kept our minds open to the surprises that might come our way, and sometimes we needed a bit more spontaneity and experimentation than on a normal project. 

For example, while we were developing components, we realized quite quickly that many of them are actually great candidates to use compound patterns. 

As Kent C. Dodds explains: “Think of compound components like the <select> and <option> elements in HTML. Apart, they don't do too much, but together they allow you to create the complete experience. The way they do this is by sharing implicit state between the components. Compound components allow you to create and use components which share this state implicitly.”

Adopting this principle, we went back and adjusted components we had already migrated to follow the same pattern. This meant fewer imports, more intuitive usage! So now instead of:

import { List, ListItem } from ‘@contentful/forma-36-react-components’

<List>
  <ListItem>...</ListItem>
</List>

Users can just:

import { List } from ‘@contentful/f36-components’

<List>
  <List.Item>...</List.Item>
</List>

This is one of the first things that ended up in our code style guide. The first draft of it was done right at the beginning of work on v4, and it remained a living document during the time we worked on the API since many of our first assumptions changed over time. 

Another of our goals for v4 release was to improve the developer experience. Unfortunately, after introducing polymorphic properties into the Forma 36 API, there was no clear overview of properties in the Visual Studio code anymore.

That meant when hovering over a given element, users couldn’t see what props were available for this component: 😱

Screenshot of the polymorphiccomponent in forma36

We knew that we had to resolve this issue if we wanted to continue providing a great experience for our fellow engineers. Our solution was to create an ExpandProps type:

export type ExpandProps<T> = T extends object
 ? T extends infer O
   ? { [K in keyof O]: O[K] }
   : never
 : T;

That could be used later in your component like this:

export const Flex: PolymorphicComponent<
 ExpandProps<FlexInternalProps>,
 typeof FLEX_DEFAULT_TAG
> = React.forwardRef(_Flex);

In this way, we were able to fix the issue when a given component is used:

Screenshot of how we were able to fix the issue when a given component is used

This should be very useful to your team if they’re struggling with a similar issue.

2. Take inspiration from others

Initially, we observed that our approach to form elements – components that allow users to build forms in the interface – could be confusing for users. We really wanted to come up with something different to make their lives easier. 

Up until v3 of Forma 36, each of the form elements had two versions; the “naked” component and the version with properties responsible for label, help test, validation message, etc. With v4, we really wanted to take a simpler approach, providing our users with flexibility while also establishing good practices around building forms. We decided to go back to the beginning and try to figure out everything afresh. 

We found a lot of inspiration in the approach of ChakraUI and other amazing UI libraries. Following their approach, we provided a FormControl component that’s responsible for the state of the inputs in the form. We also provide a more granular approach to form elements. This way, users can compose forms in the most optimal way. 

So now instead of:

import { Form, TextField } from ‘@contentful/forma-36-react-components’

<Form>
  <TextField
    labelText="E-mail address"
    helpText="Please enter your email address"
    formLabelProps={{ requiredText: 'required' }}
    required
  />
</Form>

Users can:

import { Form, FormControl, TextInput } from ‘@contentful/f36-components’

<Form>
  <FormControl isRequired>
    <FormControl.Label>E-mail address</FormControl.Label>
    <TextInput />
    <FormControl.HelpText>Please enter your email address</FormControl.HelpText>
  </FormControl>
</Form>

3. Give yourself space to fail

Some things never go to plan. There’s always a risk that you're making the wrong decision, and it will cost the team more time and effort later. And that’s absolutely fine. It’s important to do the best you can, understand the risk and be ready for damage limitation exercises if necessary.

As the first step in our development process, we created a new package f36-components, with a new tag next-v4 in the npm registry. This allowed us to test new components and adjust the API if needed. During this phase, we were doing a lot of internal testing. Every time we merged new changes to the branch, a new version of the package was released automatically with a different hash at the end, but the version itself stayed the same, eg. 4.0.1-beta.2705, 4.0.1-beta.2706, 4.0.1-beta.2707, and so on.

As soon as we were confident that the API was in good shape, we decided to make it available to our community and start testing with them. We released a beta version under the new beta tag in the npm with the same publishing flow as before — an automatically published version on every commit. But because we committed to the branch quite often, it led to some issues with too many versions in the npm — for example, wrong versions were installed while testing in the CodeSandbox.

Also in our apps, which we updated to the new beta version, we were using tags instead of exact versions in package.json (like: “@Contentful/f36-components”: “beta”). We did it to avoid manually updating packages versions every time we released a new version. But it caused confusion: we couldn’t be sure what exact version the current build was using, and we encountered some issues with the package’s manager cache when we switched from one tag to another.

In the end, we decided to use only the latest tag and publish every new package manually.

Our advice to anyone in the same situation is this:

  • If it’s already an existing npm package and the latest tag should have a preview version, then create a new tag in npm, for example, beta.  If it’s a new package, like it was in our case (we changed the name from forma-36-react-components to f36-components, so it was a completely new package in the npm), you can publish everything under the latest tag.

  • Stick to SEM versioning rules at all costs to avoid spaghetti 🍝  in the npm. Maybe even publish manually with simple version changes, for example, 4.0.0-beta.0, 4.0.0-beta.1, etc.

  • Try to use exact versions in your package.json instead of the tag. For example 4.0.0-beta.0, not beta.

What particularly helped us was the fact that we decided to change the naming of the package from forma-36-react-components to f36-components. This gave us a bit more flexibility when it came to testing and updating client applications, and also eliminated the versioning issue — we didn’t risk breaking anything for anyone on production. If you’re able to do something similar, then my recommendation is that it’s great and totally worth considering.

We thought we’d manage to complete this migration smoothly. We'd put a lot of effort into planning, but ultimately it took up more of our time than we expected. One team of six engineers needed many months to bring all the elements together, finish migration of the components, set up tooling, prepare guidelines and documentation for users, and then test everything and fix issues. 

Perhaps we extended the scope too much, and it might be a reason why it took us a bit longer, but we agreed to that risk together with our stakeholders. We are certain about one thing — without a dedicated team, we wouldn’t have been able to do it. With a project this big, you need to have a group of people who are focused and committed, and this is definitely something that we’ve learned during our v4 journey.

4. Stay connected to your audience

The Forma 36 react component library is driving Contentful products, but it’s also a powerful tool for engineers who build their apps to extend Contentful. This is a very important requirement which we always have to take into consideration. In that regard, we planned our testing strategy ahead with external users in mind.

In the first phase of the development, we planned the testing session with internal users — front-end engineers who work on a daily basis with Forma 36 building new features in Contentful products. 

As soon as we finished the migration of all the components, we started the testing sessions. We knew that the docs weren’t perfect, the API still has some issues, but we felt that we needed to validate what we have with real users. 

Engineers took part in the testing sessions where they recreated a given mockup with the new v4 components. We collected their feedback and transformed it into actionable items in our backlog. We discovered a lot during this phase of tests and it was really valuable to connect with our audience and gather this data. The next phase, which was kind of overlapping with the one presented above, was to migrate some of our applications to use v4 of the library. We decided that a great candidate for that was the Compose app. Within this phase, we were able to collect data and issues from production. And we found some issues during this phase as well! 

Of course, you might think that this is risky, but keep in mind that we didn’t migrate all the components at once. We migrated piece by piece and pushed every time to production. This way, v4 was introduced to Compose gradually and gave us time to address issues along the way. As soon as we released the beta, we started the next phase of testing of the API. Now we had much better documentation, many issues were already found and fixed internally, and we decided that we were ready to give the beta to our community. 

Within the beta, we already had all the codemods, migrations guides and documentation in place, so folks could migrate their project easily to v4. Also, the team was on hand to support anyone who would be interested in migrating their projects to v4 — we were standing by, happy to attend the call, pair a programming session or any other help that the community needed. 

The main goal for us was to make sure we were providing all the tools and guidelines needed to migrate an app from version 3 to 4.

5. Make migration simple

Forma 36 v4 is a breaking change from v3. This was unavoidable, and we knew that we needed to invest time and resources to make the transition between versions as smooth as possible for our users. 

And then we realized something; the best migration is the one that you don’t have to do yourself!

That’s why we devoted a lot of time into codemods for most of our components — and it was well worth the effort. With those codemods, we believe the migration from version 3 to version 4 will take very little time for anyone, whether internal or external users. 

Codemod is a tool to assist you with large-scale codebase refactors that can be partially automated but still require human oversight and occasional intervention. You can use it to transform existing code into anything you want. In our case, into the new version of the given component. With just one command in the terminal, you can now migrate almost your entire project to use v4 of Forma 36. 

In order to create codemods we used a very powerful tool called jsCodeshift. It was a challenge for us, since the documentation of jsCodeshift is not optimized and there aren't many working examples out there, so you really have to know what you want to achieve. But we made it! 

Have a look at how powerful it is and what was possible to do based on one of our form elements examples:

Version 3 of text input:

<TextField
 id="inputId"
 name="email"
 labelText="Label text"
 value="some value"
 helpText="Some help text"
 className="text-field-class-name"
 testId="text-field-test-id"
 onChange={() => {}}
 onBlur={() => {}}
 textInputProps={{
   type: 'text',
   placeholder: 'placeholder',
   disabled: conditionalIsDisabled ? true : false,
   maxLength: 10,
   testId: 'text-input-test-id',
   inputRef: ref
 }}
 validationMessage="Some validation message"
 required
/>;

Version 4 of text input, after the codemod magic:

<FormControl
 className="text-field-class-name"
 testId="text-field-test-id"
 id="inputId"
 isRequired
 isDisabled={conditionalIsDisabled ? true : false}>
 <FormControl.Label>Label text</FormControl.Label>
 <TextInput
   name="email"
   value="some value"
   onChange={() => {}}
   onBlur={() => {}}
   type="text"
   placeholder="placeholder"
   maxLength={10}
   testId="text-input-test-id"
   ref={ref} />
 <Flex justifyContent="space-between">
   <FormControl.HelpText>Some help text</FormControl.HelpText>
   <FormControl.Counter />
 </Flex>
 <FormControl.ValidationMessage>Some validation message</FormControl.ValidationMessage>
</FormControl>;

Pretty powerful, right? If you need any inspiration or maybe our code can help your project, go ahead and check it out on GitHub.

In addition to this tooling, there are, of course, migration guides and documentation. We’ve tried to provide users with everything that they might need in order to migrate their projects to the new version. Switch your project to use Forma 36 v4 with just one command today. Then you can enjoy and build amazing stuff!

Wrapping up

Let us know your experiences with this new release. If you have any feedback to share, or any mistakes that you’ve learned from, we’d love to hear about it.

  • Reach out to us on GitHub: submit an issue  and we will help you out.

  • Use an anonymous Google form to give us your feedback.

  • Join our public community Slack and ask a question on the #forma36 channel. 

We’re here to support you!!

About the author

Don't miss the latest

Get updates in your inbox
Discover new insights from the Contentful developer community each month.
add-circle arrow-right remove style-two-pin-marker subtract-circle