Home
Tools
Documents
Search
  • Pricing
  1. Home
  2. Blog
  3. Engineering
  4. Advantages of Using Declarative Frontend Logic to Combat the SetStep Anti-pattern (Part 2)
Advantages of Using Declarative Frontend Logic to Combat the SetStep Anti-pattern (Part 2)

Advantages of Using Declarative Frontend Logic to Combat the SetStep Anti-pattern (Part 2)

by Daniel Knaust

In the previous article, we explored SetStep’s Anti-Pattern limitations. Let’s take a look at how we can overcome them with declarative logic!

☝️ Note: This is part 2 of an article series, so be sure to check out the first article before you continue reading:

Limitations of Scaling a Frontend App with the SetStep Anti-Pattern

Declarative UI Logic to the Rescue!

 

To overcome the mess caused by the SetStep Anti-pattern described in the previous article, here are a few simple concepts to remember:

  • 👉 Minimal-state principle: Only store the minimal possible state. This typically means what the user has selected or received from the backend.
  • 👉 Prefer computed values: All other values should be derived from the state, including even the current step!
  • 👉 Make control flows easy by using early returns: Go for easy and early returns instead of writing convoluted if-else and switch-case statements.

Let’s take a practical look at how these rules play together to create a simple routing logic, rewriting the example below!

And there you go, a much more verbose solution! 😅 Joking aside, yes, it is longer, but also a LOT more extendible and easier to comprehend.

To make this whole thing easier to follow, let’s just look at what happens here by using plain English in 3 simple points:

  1. Once the user finishes a step, it gets marked as completed.
  2. The router function (getStep()) is responsible for returning the first step that is not yet completed.
  3. The renderer displays the component selected by the router function.

Adding a New Step?

 

Since we only store values that relate to a user interaction (e.g., completing a step), we can easily extend how we compute the current step based on those:

And even better, isBusinessUser is now a separate state entry, so we can derive more computed states from it (for example, if we want to show a different thank you page as mentioned in Part One of this article series).

Reactive to Any State by Design

 

We also don’t need to worry about using external or global state in the navigation logic since it’s always computed on the fly!

Having no copying of state with useEffect and no fragmented state-setter logic makes it not just easier to read and maintain but also less likely to result in an inconsistent state.

Bonus: Performance Friendly With Memoization

 

Most of the time, switching to a computed state won’t result in a significant overhead in performance, as it usually contains very cheap-to-compute conditions.

However, if performance is a concern, it’s really easily to add memoization with this pattern:

Clear Responsibility Boundaries for Navigation Logic

 

By following the minimal-state principle, child components only report state changes that are relevant for the router. They don’t bother with managing any navigation logic at all.

This way, we don’t need to worry about fragmented logic across files and components, as there is a single source of truth when it comes to how the current step is determined.

Jumping Between Steps?

 

So far, we’ve focused mainly on navigation in a pretty linear fashion, but what happens if we want to support skipping steps? Or going back to the previous step?

Addressing these can get a bit tricky; the scenario is different, depending on what it is you’re looking to do. Covering all options in detail is out of scope for this article, so I’ll keep it concise.

👉 Use-case #1: Skip a Step

 

Imagine an onboarding flow where the user is guided through a couple of steps before they can start using your app. Each step can be either completed (by completing the task) or skipped.

Skip a step (for example in an onboarding flow)

Skip a step (for example in an onboarding flow)

Solution: The pattern works perfectly; simply track each skipped step in another state value, similar to the isXYCompleted one. And by adjusting the getStep() function, it’ll simply work!

👉 Use-case #2: Backwards Navigation

 

The second most likely scenario one may encounter is allowing a user to go back to the previous step in the flow. For example, let’s say that you’re buying a plane ticket, and you want to change the dates of your flight that you selected in a previous step.

Enabling backwards navigation

Enabling backwards navigation

Solution: Easy! Just un-mark the previously completed step/steps, and thanks to the routing logic, the user will be on the desired step!

👉 Use-case #3: Jumping Between Steps

 

This is where it gets a bit tricky, but it can still work with some adjustments in the code. We’ve all seen this in combination with a progress indicator. By clicking on a step in the flow, the user can easily jump to the corresponding step.

Jumping Between Steps

Jumping Between Steps

Solution: This deserves its own article as, depending on the exact requirements, a lot can be adjusted to maximize readability and overall maintainability. Here are some brief options to consider:

  • It’s a non-linear flow In this case, the pattern may not be the right choice (see Use-Case #4 for example).
  • It’s a linear flow, but users can override the current steps The pattern could still be right, we simply store the user’s override, which can be cleared at any time, falling back to the default routing logic.

Note: Another interesting option that could be useful in some cases is to return an array of available steps (getAvailableSteps()) instead of retuning the single active one (getStep()).

This allows more computation to be done before ultimately retuning the active scene (for example by considering a skipped/overridden step stored in state).

👉 Use-case #4: Menu-like Navigation (Non-linear Flow)

 

In this scenario, you’re offering a menu-like navigation (for example a sidebar). Using the declarative pattern here could add more complexity than benefit.

Menu-like navigation in non-linear flow

Menu-like navigation in non-linear flow

Solution: This pattern is probably not much help in this case. Most likely, the easiest solution is to track the selected menu. In most cases, there’s no need for any getter function (unless more logic is involved in computing the current step).

When To Use It?

 

As with any pattern, this works well in some cases, but can become a burden in others.

👍 We found this to be a really powerful way of organizing business logic for complex user flows (such as a checkout), as it scales very well with ever-changing product needs.

👎 For very simple and non-linear use-cases, the boilerplate can be an overhead. It might not be the ideal choice if you’re looking to implement a menu or sidebar-style navigation.

Key Takeaways

 
  • Look out for code smells such as setStep (or similar). It’s usually an indicator of the state being set implicitly.
  • Focus on only storing the minimal state. In most cases this means only user input and fetched values.
  • Values based on the state should be computed dynamically (can be memoized if needed).
  • A good way of structuring the getter function is by leveraging the early-return pattern.
  • Ideally, you’ll end up with getStep functions, acting as a single source of truth for navigation logic.

Thanks for reading! I hope you find this useful and that it helps you write more maintainable code!

Daniel Knaust
Daniel Knaust
Group Engineering Manager @Smallpdf