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.