Composable onboarding

This past weekend, I was once again gearing up to launch the new onboarding for Cushion. The onboarding transitions between each step by sliding the text and form fields out while the new ones slide in. Early on, I assumed too much about future modal designs, so I created separate header and content slots to specify the text for the header slot and specify the fields for the content slot. This, however, forced me to keep all of the steps’ text and fields in a single OnboardingWindow component that had two separate “tracks” to transition the text and the fields.


Because I wanted to explore a few variations of the transition, I built the onboarding flow itself without testing, expecting to write proper tests when everything was more solidified. I didn’t realize at the time, however, that the way I’d end up structuring it to work for the transition would make it even more difficult to test—and if I had written tests from the start, the tests themselves would’ve guided me towards a better structure, as they always do. Since I’m no longer in a rush with Cushion and would rather invest time now to get things right, rather than suffer later for poor decisions, I decided to restructure the onboarding.


The initial goal with restructuring the onboarding was to make it easy to test, which ended up also meaning composable. To test each step on its own, I extracted each step (including both its text and fields) into its own component. Then, the OnboardingWindow simply consisted of a stack of these components. If I ever add any new steps or change the order of the steps, it’s simply a matter of adding a new step component or moving these components around. Now, the OnboardingWindow’s only job is to switch between steps—instead of also handling all of the logic for each steps’ form. This is dead-simple to test without needing to test all the individual forms at the same time.

Breaking up the steps into individual components also helped me fine-tune the data that I sent to the API. Because I previously had all of the fields were in a single component, I would send the entire payload from the entire onboarding to the API. This is a terrible idea because if any error occurs for a field that isn’t visible yet, the user would be so confused—and I would be so embarrassed to explain why that happened. This is where my utility methods come in handy.

Each step has its own form data, which is based on the user model, but I don’t want to include all of the user’s data in the payload. Because of this, I use my pick method to select only the fields in the step for the payload. For example, the first step includes fields for “nickname”, “business name”, and “favorite color”, so my payload for that step might look like this, instead of the entire user payload:

  "nickname": "Cosmo Kramer",
  "business_name": "Kramerica",
  "color": "FF0099"

Aside from this approach being the right way to do it, the data is much easier to test. If I’m testing a step with only these fields, I should only need to test these fields—I shouldn’t need to worry about whether this step overwrites some random field that’s included if the user changed it elsewhere.

Now that I have the steps as separate, tested components that I can compose within my onboarding window, I have one last thing I need to do—test the entire flow. Up until now, I’ve only written unit tests, which works fine for testing each component on its own, but when you have a sequence of events that also interact with an API, this gets into integration/end-to-end testing territory.

While I don’t have a ton of experience with e2e testing, an onboarding flow is the perfect place to start. With the current Cushion, I rely on a QA checklist for substantial changes that might have adverse affects, but that’s not sustainable—I can’t spend ~15 minutes manually going through every step only to need to do it again if I make any changes. This is where e2e testing comes in. If I can take this opportunity to finally set up e2e testing and make it a light-lift to test new flows, I won’t think twice about taking the time to test them. That’s easier said than done, but as I said before, I’m determined to do this the right way.