1

In my app I tried to implement a stepper (with forms inside it) using useReducer, useContext and react-router-dom. The example here is closely emulating code in my app: Codesandbox

So, I have a Parent component, which renders routes dynamically from config, and also redirects to the currently active step:

config

const config = [
  {
    path: "one",
    label: "Step One",
    component: Child
  },
  {
    path: "two",
    label: "Step Two",
    component: Child
  },
  {
    path: "three",
    label: "Step Three",
    component: Child
  }
];

Parent.js

export default function Parent({ steps }) {
  const [state, dispatch] = useContextReducer(); // see Codesandbox
  const { path } = useRouteMatch();
  const { id } = useParams();
  const onClick = useCallback(() => {
    dispatch({ type: "RESET" });
  }, [dispatch]);

  useEffect(() => {
    console.log("PARENT RENDERED");
    // constantly being triggered, both on initial redirect and also on every 
    //  change in children (see example)
    return () => console.log("PARENT UNMOUNTED");
  });

  return (
    <>
      <p>Current step: {steps[state.activeStep].label}</p>
      <button type="button" onClick={onClick}>
        Reset
      </button>
      <p></p>
      <Switch>
        {steps.map(({ path: pathName, component: Component }) => (
          <Route
            key="1"
            path={`${path}/${pathName}`}
            render={(routeProps) => (
              <Component {...routeProps} id={id}></Component>
            )}
          ></Route>
        ))}
      </Switch>
      <Route
        path={path}
        render={({ match }) => (
          <Redirect to={`${match.url}/${steps[state.activeStep].path}`} />
        )}
      ></Route>
    </>
  );
}

Child.js

export default function Child({ id }) {
  const [state, dispatch] = useContextReducer(); // see Codesandbox
  const onNext = useCallback(
    function () {
      dispatch({ type: "NEXT" });
    },
    [dispatch]
  );

  const onBack = useCallback(
    function () {
      dispatch({ type: "BACK" });
    },
    [dispatch]
  );

  return (
    <div>
      <button disabled={state.activeStep === 0} type="button" onClick={onBack}>
        Back {id}
      </button>
      <button
        disabled={state.steps.length - 1 === state.activeStep}
        type="button"
        onClick={onNext}
      >
        Next {id}
      </button>
    </div>
  );
}

Parent's wrapper

function IntermediaryRoute() {
  return (
    <ContextReducerProvider init={init}>
      <Parent steps={config}></Parent>
    </ContextReducerProvider>
  );
}

The problem here is that the Parent component being remounted and re-rendered on every change inside Child component. It can be because Parent uses Context which changes on every button click inside Child. But it also can be because of the dynamic Redirect inside it.

I probably should have isolate state and Context Provider in some other component, but it would have been remounted anyway. Because Parent still must use state to render routes dynamically and redirect dynamically to the current active step.

I have no idea for now how to implement this without Parent being re-rendered or at least without being remounted.

In one of the Child in my app I have a heavy table which loads data from api. At some point when I leave and enter Parent component, app hangs up and dies without any errors.

I presume because of the constant remounting of the Parent component - you can see this in Codesandbox.

Any help or tips on how to implement that kind of stepper is appreciated.

arkhemlol
  • 11
  • 2
  • May be this is an intendent behavior, given the fact that I must redirect to a deeper route on each `activeStep` change. My main problem is that *material-table* inside one of the children hanged up the page. It seems that an issue is not with the routes, but with the table itself (lot's of similar issues in their repo today). I am not closing this, because I still want to avoid unnecessary unmounts. – arkhemlol Sep 12 '20 at 19:32

0 Answers0