Expose a Custom Hook to Children (children only) in React

487 Views Asked by At

I'm not sure the title is correct, so let me try to explain what I'm trying to achieve.

Let's say I have a flow in my application that has 3 steps in it, so I create a component (let's call it Stepper) with 3 child components where each child is a component that renders the corresponding step.

I want to expose a custom hook to the child components of Stepper, let's call it useStepper.

This is how Stepper would look like (JSX-wise):

export const Stepper = (props) => {

...some logic

  return (
   <SomeWrapper>
      {props.children}
   </SomeWrapper>
  );
};

so I can make components like this:

export SomeFlow = () => {
   return (
      <Stepper>
         <StepOne />
         <StepTwo />
         <StepThree />
      </Stepper>
   );
};

Now this is how I want things to work inside Stepper's children, let's take StepThree as an example:

export const StepThree = () => {
   const exposedStepperData = useStepper();

   ... some logic

   return (
      ...
   );
};

Now, it's important that the Stepper will be reusable; That means - each Stepper instance should have its own data/state/context that is exposed through the useStepper hook. Different Stepper instances should have different exposed data.

Is it possible to achieve this? I tried to use Context API but I was not successful. It's also weird that I couldn't find anything about it on the internet, maybe I searched wrong queries as I don't know what patten it is (if it exists).

Note:
I achieved a similar behavior through injected props from parent to its children, but it's not as clean as I want it to be, especially with Typescript.

1

There are 1 best solutions below

5
On

I recently came across something like this, it was solved by pouring all the components/steps in an array and let the hook manage which component/step to show. If you want it to be more reusable you could pass in the children to the array.

I hope this helps you in the right direction

useStepper.ts

import { ReactElement, useState } from "react";

export const useStepper = (steps: ReactElement[]) => {
  const [currentStepIndex, setCurrentStepIndex] = useState(0);

  const next = () => {
    setCurrentStepIndex((i: number) => {
      if (i >= steps.length - 1) return i;
      return i + 1;
    });
  };

  const back = () => {
    setCurrentStepIndex((i: number) => {
      if (i <= 0) return i;
      return i - 1;
    });
  };

  const goTo = (index: number) => {
    setCurrentStepIndex(index);
  };

  return {
    currentStepIndex,
    step: steps[currentStepIndex],
    steps,
    isFirstStep: currentStepIndex === 0,
    isLastStep: currentStepIndex === steps.length - 1,
    goTo,
    next,
    back,
  };
};

Stepper.tsx

// const { currentStepIndex, step, isFirstStep, isLastStep, back, next } =
//  useStepper([<StepOne />, <StepTwo />, <StepThree />]);

const { currentStepIndex, step, isFirstStep, isLastStep, back, next } =
  useStepper([...children]);

return (
  <div>
    {!isFirstStep && <button onClick={back}>Back</button>}
    {step}
    <button onClick={next}>{isLastStep ? "Finish" : "Next"}</button>
  </div>
);