Pushstate and popstate not working with condtionally rendered components

I am conditionally rendering components(screens) as user will be guided through different screens. I am trying to implement a functionality where user can use back and forward buttons of browser to navigate between these different screens or components. I am struggling to implement this. Any suggestions will be appreciated.

link to sandbox: https://codesandbox.io/s/history-push-and-pop-state-listening-forked-1keiz?file=/src/App.js

      const [show1, setShow1] = useState(true);
      const [show2, setShow2] = useState(false);
      const [show3, setShow3] = useState(false);

      let stateForHistory = {
       show1: false,
       show2: false,
       show3: false
     };

      const handleClick = () => {
       setShow1(false);
       setShow2(true);

      if(show2)
       setShow2(false)
       setShow3(true)
    };

      //saving state onmount
      useEffect(() => {
       window.history.pushState(stateForHistory, "", "");
     }, []);

     useEffect(() => {
      window.addEventListener("popstate", (e) => {
      let { show1, show2, show3 } = e.state || {};
      if (!show1 && show2) {
        setShow1(true);
        setShow2(false);
      }
    });
    return () => {
      window.removeEventListener("popstate", null);
    };
  });

  return (
    <div className="App">
      <div  id="screen-1">
        {show1 && <Screen1 />}
     </div>
     <div  id="screen-2">
       {show2 && <Screen2 />}
     </div>
     <div id="screen-3">
       {show3 && <Screen3 />}
     </div>

  <button onClick={handleClick} id="btn">
    Next
  </button>
</div>
  );
}

Answer

I would suggest using a routing package to handle the navigation aspect, your code can focus on the screen. What you are describing sounds similar to a stepper.

index

Wrap the App in a router.

import ReactDOM from "react-dom";
import { BrowserRouter as Router } from "react-router-dom";
import App from "./App";

const rootElement = document.getElementById("root");
ReactDOM.render(
  <Router>
    <App />
  </Router>,
  rootElement
);

App

Define a route with a path parameter to match a “step” or screen.

function App() {
  return (
    <div className="App">
      <Switch>
        <Route path="/step/:step">
          <Stepper />
        </Route>
        <Redirect to="/step/1" />
      </Switch>
    </div>
  );
}

Create a screen stepper component to listen for changes in the route, specifically to the step parameter, and conditionally render the screen.

const ScreenStepper = () => {
  const history = useHistory();
  const { step } = useParams();
  const { path } = useRouteMatch();

  const nextStep = (next) => () =>
    history.push(generatePath(path, { step: Number(step) + next }));

  const renderStep = (step) => {
    switch (step) {
      case 3:
        return <Screen3 />;

      case 2:
        return <Screen2 />;

      case 1:
        return <Screen1 />;
      default:
    }
  };

  return (
    <>
      <h1>Step: {step}</h1>
      {renderStep(Number(step))}
      <button disabled={Number(step) === 1} onClick={nextStep(-1)} id="btn">
        Previous
      </button>
      <button onClick={nextStep(1)} id="btn">
        Next
      </button>
    </>
  );
};

You can expand/customize upon this to limit the screen count, or render the screen routes from an array, etc…

Edit pushstate-and-popstate-not-working-with-condtionally-rendered-components