Skip to content

Conversation

Strangersknowme
Copy link

Description

There are certain cases where we might want only some some steps of the form to be present to form the stepper.

For example, you might have a stepper with two schemas for refunds and exchanges. Now, it is possible that you only have refunds or exchanges or sometimes both, what do you do in that case?

You can use a duct tape solution of toggling between steps using the when method as I described in this issue: #127

Now, you can add a callback function to have the stepper consist of only those steps which matter for the case. No need to duct tape it.

…used which satisfy a certain condition, a list of steps which meet the condition
Copy link

changeset-bot bot commented Apr 27, 2025

⚠️ No Changeset found

Latest commit: 806e778

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

Copy link

vercel bot commented Apr 27, 2025

@Strangersknowme is attempting to deploy a commit to the Damián Ricobelli's projects Team on Vercel.

A member of the Team first needs to authorize it.

@damianricobelli
Copy link
Owner

I haven't forgotten about this. I've had a very busy few weeks, but I'll get back to you soon

@damianricobelli
Copy link
Owner

I've been reviewing this and I don't think filters are a good solution. I'll try to explain in as much detail as possible so you understand my idea.

Let's say you have something like this:

const methods = useStepper({ stepFilter: () => ["first"] });

In an ideal scenario, you would get methods that would only apply to that step. Now let's look at a basic return:

  return (
    <React.Fragment>
      {methods.when("first", (step) => (
        <p>First step: {step.title}</p>
      ))}
    </React.Fragment>
  );

Excellent, this case would work successfully. Now let's add something else.

  return (
    <React.Fragment>
      {methods.when("first", (step) => (
        <p>First step: {step.title}</p>
      ))}
      {methods.when("second", (step) => (
        <p>Second step: {step.title}</p>
      ))}
    </React.Fragment>
  );

This case should now break, since you are filtering by the first step, and the second step should not exist. And from this point on, it will become chaos: imagine that I want the filter to be controlled through a state

  const [myStep, setMyStep] = React.useState("first");
  const methods = useStepper({ stepFilter: () => [myStep] });

The latter case will be executed at runtime, and we reach a dead end: we have no way of inferring the types of the filtered step or steps, so the whole idea of typesafety would be broken instantly.
This whole idea of filtering your steps means that the developer is in a constant struggle to know which steps are valid at any given moment. And the only way you could achieve a successful case with filters is if you have isolated components with their own independent filter. And you know what that means? That you can simply create instances for each use case and get your filter for free, with typesafe and everything you need for your specific use case.

My recommendation for this case is that you create different instances of your use cases so that you can play around with each one freely.

const stepper1 = defineStepper(...)
const stepper2 = defineStepper(...)
...

@Strangersknowme
Copy link
Author

Strangersknowme commented Jun 1, 2025

I think you aren't able to grasp why there is a need for a filtering in the first place. Let me expand on it a bit more.

My point with filtering at the top itself is a design decision for the stepper, so that you describe your stepper once with all the steps which could possibly be there for use in your form which will be chosen at runtime itself depending on the stepFilter callbackFn itself. Just think about it for a second here, when you use the when method to conditionally render a component for the form, you are only controlling the rendering of the component not the steps. What I mean by that is your step is still going to be there when you go to submit the form, albeit empty with the schema that you defined. Why would you send empty schema to your backend for that step's form? This is why a top level filter is important imo.

This case should now break, since you are filtering by the first step, and the second step should not exist.

We filtered the steps being rendered at the top itself, why would you even need to use the when method? Again, this is a way to design your stepper, it's a not meant as a method for conditional rendering of a step. You wouldn't use when for the steps which you know are not going to be there. A developer knows which steps they are going to be needing for the form.

The latter case will be executed at runtime, and we reach a dead end: we have no way of inferring the types of the filtered step or steps, so the whole idea of typesafety would be broken instantly.

I don't work with TypeScript, so I used Chatgpt to understand what this even means and yeah you are right. The type safety would break, you wouldn't know the type of steps. But, on the other hand, because we are doing the filtering as a design decision top down, I don't know if you'd even need type safety for the steps that don't exist? I don't know I am just spitballing here, I have no clue how types are used.

If you still feel like it is not useful and type safety can't be fixed, then that's alright. Let's just keep this PR here, if people come looking for a dynamic stepper, they can use this. I am already using this in my project and it works great.

As a side note, I forgot to add this bit of code in the PR, after I tested in my project. There needs to be a hook present to jump between the dynamic steps. Something like this:

Let's say your conditionalStepIds is the array containing your filtered steps, so whenever your conditionalStepIds change, your step also needs to jump to that step using this:

  // Whenever conditionalStepIds changes, ensure the current step is still in it:
  useEffect(() => {
    // stepper.current.id is the active step’s id
    if (!conditionalStepIds.includes(stepper?.current?.id)) {
      // Jump to the first available step
      stepper.goTo(conditionalStepIds[0]);
    }
  }, [conditionalStepIds, stepper?.current?.id]);

@damianricobelli
Copy link
Owner

Let's go back to basics. Could you show me a simple, reproducible use case with codesandbox or stackblitz where I can see what you're looking for with this dynamic filter?

@Strangersknowme
Copy link
Author

I will do that when I get some free time.

@damianricobelli
Copy link
Owner

@Strangersknowme any updates?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants