Article thumbnail

Build useStepper hook in React

7m
hooks
steps
routing

Intro

Meet useStepper - a revolutionary React custom hook for managing step-based workflows. Seamlessly integrate for intuitive navigation, enhancing user experience.

Prelude

Let's say you have a simple screen with steps. It can look more or less like the one in the gif below:

Loading

An example of usage in an application

We'll write a useStepper hook with the basic functionality to change the step and protect us from potential bugs.

In this hook we'll encapsulate common logic for changing steps, so later we don't have to repeat ourselves and we'll make it type-safety.

We want to avoid the following:

  • Using the wrong key
  • Passing invalid props to the component
  • Moving beyond the range of steps
  • Displaying the wrong step
  • Making a mistake in the navigation logic

1. The usage of useStepper

For a start, look at how we will use our hook. Pay attention to the hints that appear thanks to TypeScript.

Loading

See how it looks in the IDE

Loading

What is happening in the posted image and code?

We declared four steps. Each of them is different - look at props and return statements.

Then we used const assertion, which makes a read-only tuple.

After that, we created two auxiliary types that will be needed for our hook - the first is a simple type alias, and the second is a union.

At the very end, we used our useStepper API.

The is() function checks if the current step matches the passed key. If it does, there is a type-guard underneath, which marks the component as the one that matches the key.

Therefore, later when we want to pass on not allowed properties to the component, we'll be blocked from doing that.

2. Let's start with interfaces

We'll use the types-first approach, in which we focus on creating contracts at the beginning (before we start implementation) - so let's do that here:

Loading

We used the notation ...any[] because our Component function will take any number of arguments, not just a single props object (we do not want to limit developers).

Now let's define what our future useStepper must receive as parameters. The first will be the key to the step we want to start with, and the second will be a list of steps.

Loading

We used 2 generic types (S, R) - our step must have a key and a Component, but it can have something more. That's why we want to use generic types so that this "something more" can be remembered later.

In addition, we will use these two generics, later on, to accurately describe what the hook will return and what each function will take.

What will the hook return?

Loading

The step type we pass to the hook will be returned as the first element, and as the second element, we will return a list of functions to interact with our hook.

So what does UseStepperActions look like?

Loading

Only the first function needs to be discussed. An example of its use is:

Loading

Now only export left and that's it.

Loading

Of course, we put all interfaces in a separate file - to preserve the Separation of Concerns principle and to separate interfaces from implementation.

You have all the interfaces in one file under this link.

3. First, let's use TDD

We're not going to paste a huge file with tests here - we'll link it below. We'll focus on why we want to write the tests first and then the implementation.

Why did we choose this way?

Because it will greatly speed up our work and make the solution more stable. We won't even have to start the browser - we'll write tests and implement the hook until tests will be green. Then we just need one manual test for final confirmation.

Loading

Now let's write two simple tests. The first will check if the assigned initial step has been taken into account, and the second will verify the execution of the set() function.

Loading

We used the renderHook function from @testing-library/react-hooks to be able to test our useStepper without additional component creation.

You have all the tests in one file under this link.

4. Making useStepper tests green

Now it remains to import the interfaces created earlier and add implementation to make our current tests pass.

Loading

Thanks to previously created types and tests - implementation was child's play. All we had to do was push the keyboard buttons, and TypeScript with the power of TDD, guided us by the hand. Tests are green ✅.

You have the whole implementation of the hook in a file under this link.

5. We discuss the is() function

There is one more thing left to discuss - the is() function. We'll skip others because first, last, previous, and next are not worth to discussing - they are not fancy.

Loading

The notation at the end means: if the specified step has the same key as the expected key then the step type is equal to the specified key.

Instead of doing something like this everywhere:

Loading

we'll write:

Loading

This function is not "indispensable" - rather fancy syntactic sugar.

You have the whole implementation of the hook in a file under this link.

Full example

At this link, you have the end result. Types, tests, implementation and re-exports.

Summary

Now you know how to handle the steps on the user interface interestingly. The created solution protects against many basic mistakes that can be made when implementing such functionality.

It is worth noting that the logic itself is simple - the complexity is introduced by TypeScript (as always). So it is up to you whether you want to use this solution with or without TS.

If you enjoyed it, be sure to visit us on Linkedin where we regularly upload content from programming.

I create content regularly!

I hope you found my post interesting. If so, maybe you'll take a peek at my LinkedIn, where I publish posts daily.

Comments

Add your honest opinion about this article and help us improve the content.

created: 05-06-2023
updated: 05-06-2023