DEV Community

Cover image for Bettered stepper handling in React
Alexey Lysenko
Alexey Lysenko

Posted on

Bettered stepper handling in React

Often in a React project, we need to do some kind of stepper with rendering components one after another or so. Let's take a look at the simple example.

function SomeModal() {
  [isFirstStep, setIsFirstStep] = React.useState(true);
  return (
    <div>{isFirstStep ? <FirstStepComponent /> : <SecondStepComponent />}</div>
  );
}
Enter fullscreen mode Exit fullscreen mode

This is a trivial example to just get the point, and I think you solving such a task all the time. This binary checking works well if we have 2 steps to show. The funny things happening when we need more than 2 steps. Often some times to handle the stepper state, we solve using some kind of object with the active steps saved in, and then conditional rendering the current step. The problem is that we need to pass the handle function to all the components we need to manipulate our steps state. And sometimes it can look very messy.
Let's build a custom hook and wrapped it to the context to abstract all the manipulation and make our code reusable and clean.

live example of the final solution
https://codesandbox.io/s/zealous-moore-9yrbn?file=/src

First of all, let's build a custom hook that will control our stepper

use-stepper.tsx
Enter fullscreen mode Exit fullscreen mode
import * as React from 'react';

type StepId = string;

export type Step = {
  id: StepId;
  order: number;
};

type UseStepperProps = {
  steps: Step[];
  initialStep: StepId;
};

function byStepId(stepId: StepId) {
  return (step: Step) => {
    return step.id === stepId;
  };
}

function sortByOrder(stepOne: Step, stepTwo: Step) {
  return stepOne.order - stepTwo.order;
}

function getId(step: Step) {
  return step.id;
}

export function useStepper(props: UseStepperProps) {
  const indexes = React.useMemo(
    () => props.steps.sort(sortByOrder).map(getId),
    [props.steps],
  );
  const [currentStep, setCurrentStep] = React.useState(() =>
    props.steps.find(byStepId(props.initialStep)),
  );

  function nextStep() {
    const nextIndex = indexes.indexOf(currentStep.id) + 1;

    if (nextIndex >= indexes.length) {
      return;
    }

    const nextStep = props.steps[nextIndex];
    setCurrentStep(nextStep);
  }

  function goToStep(stepId: StepId) {
    const step = props.steps.find(byStepId(stepId));

    if (process.env.NODE_ENV !== 'production') {
      if (!step) {
        throw new Error(`Step Id "${stepId}" is not 
      registered`);
      }
    }

    if (step) {
      setCurrentStep(step);
    }
  }

  function prevStep() {
    const prevIndex = indexes.indexOf(currentStep.id) - 1;

    if (prevIndex < 0) {
      return;
    }

    const prevStep = props.steps[prevIndex];
    setCurrentStep(prevStep);
  }

  function isCurrentStep(stepId: StepId) {
    return stepId === currentStep.id;
  }

  return {
    currentStep,
    nextStep,
    prevStep,
    goToStep,
    isCurrentStep,
  };
}


Enter fullscreen mode Exit fullscreen mode

What going on here? We will describe steps as an object with the strings of id and order of a current showing step (will show this below) and use prevStep, goToStep, currentStep.. functions to manipulate the step we render.

Ok let's move on to create your step context, we wrap our steps components in and use the hook.

stepper-context.tsx
Enter fullscreen mode Exit fullscreen mode
import * as React from 'react';
import { useStepper } from '..';

export const StepperContext = React.createContext<ReturnType<typeof useStepper>>(
  undefined,
);

export function useStepperContext() {
  const value = React.useContext(StepperContext);

  if (value === undefined) {
    throw new Error('Stepper Context is undefined');
  }

  return value;
}
Enter fullscreen mode Exit fullscreen mode

We create a context for passing our values from useStepper and useStepperContext to use them in future components.

One more thing, we need to develop stepper.tsx component, it will wrapped up our components and will manage rendering under the hood.

stepper.tsx
Enter fullscreen mode Exit fullscreen mode
import * as React from 'react';
import { StepperContext, useStepperContext } from '..';
import { useStepper } from '..';

type StepId = string

type StepType = {
  id: StepId;
  order: number;
};

type StepperProps = React.PropsWithChildren<{
  steps: StepType[];
  initialStep: StepId;
}>;

export function Stepper(props: StepperProps) {
  const value = useStepper(props);
  return (
    <StepperContext.Provider value={value}>
      {props.children}
    </StepperContext.Provider>
  );
}

type StepperStepProps = {
  step: StepId;
  component: React.ComponentType<any>;
};

export function Step(props: StepProps) {
  const stepperContext = useStepperContext();
  return stepperContext.isCurrentStep(props.step) ? <props.component /> : null;
}

Enter fullscreen mode Exit fullscreen mode

It's done, now we can use this to run our steps like this just past our custom components inside the custom components, and use a hook for manage components rendering:


import * as React from "react";
import { Stepper, Step } from "..";
import { useStepperContext } from "..";

const STEPS = [
  { id: "first-step", order: 1 },
  { id: "second-components-step", order: 2 },
  { id: "id-for-the-third-step", order: 3 }
];

const FirstStep = () => {
  const stepperContext = useStepperContext();
  return (
    <div>
      <p>First step </p>
      <button onClick={stepperContext.nextStep}>Next</button>
    </div>
  );
};

const SecondStep = () => {
  const stepperContext = useStepperContext();
  return (
    <div>
      <p>Some second step</p>
      <button onClick={stepperContext.prevStep}>Prev</button>
      <button onClick={stepperContext.nextStep}>Next</button>
    </div>
  );
};

const ThirdStep = () => {
  const stepperContext = useStepperContext();
  return (
    <div>
      <p>Third step</p>
      <button onClick={stepperContext.prevStep}>Prev</button>
    </div>
  );
};

export function ContainerWithSteps() {
  return (
    <Stepper steps={STEPS} initialStep="first-step">
      <Step step="first-step" component={FirstStep} />
      <Step step="second-components-step" component={SecondStep} />
      <Step step="id-for-the-third-step" component={ThirdStep} />
    </Stepper>
  );
}

Enter fullscreen mode Exit fullscreen mode

You can check the live example here
https://codesandbox.io/s/zealous-moore-9yrbn?file=/src

Top comments (0)