Maintain timer state during page navigation

This question was asked for Senior Staff UI engineer – Palo Alto Networks in the machine coding round.

The problem statement reads as:

Build an app in React with a sidenav. Sidenav should have two or three pages and should be able to switch between pages. And the first page should have a timer ticking every second and once the user navigates to another page the timer should stop and then should resume when we navigate back to the page.

This question was asked for senior staff role, thus it won’t be as simple to solve.

Reading the problem statement, we can break it down into multiple parts and then tackle it easily.

Adding the pages

Install the react-router library that will helps to handle the client-side browser navigation.

npm i react-router

Next, include the packages and add 3 pages with the components.

// index.js
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { BrowserRouter, Routes, Route } from "react-router";

import Dashboard from "./Dashboard";
import Home from "./Home";
import Settings from "./Settings";
import About from "./About";

const rootElement = document.getElementById("root");
const root = createRoot(rootElement);

root.render(
  <StrictMode>
    <BrowserRouter>
      <Routes>
        <Route element={<Dashboard />}>
          <Route index element={<Home />} />
          <Route path="settings" element={<Settings />} />
          <Route path="about" element={<About />} />
        </Route>
      </Routes>
    </BrowserRouter>
  </StrictMode>
);

Here the <Dashboard> will act as the layout which will have the sidebar for navigation and it will render the inner route components.

We will make use of the <Outlet> component from react-router that acts as a placeholder for the inner routes.

// Dashboard.js
import "./styles.css";

import { Outlet } from "react-router";
import { NavLink } from "react-router";

export default function Dashboard() {
  return (
    <div className="dashboard">
      <aside className="side-bar">
        <NavLink
          to="/"
          className={({ isActive }) => (isActive ? "active" : "")}
        >
          Home
        </NavLink>
        <NavLink
          to="/about"
          className={({ isActive }) => (isActive ? "active" : "")}
        >
          About
        </NavLink>
        <NavLink
          to="/settings"
          className={({ isActive }) => (isActive ? "active" : "")}
        >
          Settings
        </NavLink>
      </aside>

      <main className="main-area">
        <Outlet />
      </main>
    </div>
  );
}

NavLink will helps to navigate through pages.

Also add some styles to create a two-column layout.

// styles.css
.App {
  font-family: sans-serif;
  text-align: center;
}

.dashboard {
  display: flex;
  gap: 8px;
  height: calc(100vh - 20px);
}

.side-bar {
  border: 1px solid;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: 10px;
  gap: 10px;
}

.main-area {
  border: 1px solid;
  flex: 1;
}

Add the remaining pages.

Home

export default function About() {
  return (
    <div className="App">
      <h1>Hello Home</h1>
    </div>
  );
}

Settings

export default function About() {
  return (
    <div className="App">
      <h1>Hello Settings</h1>
    </div>
  );
}

About

export default function About() {
  return (
    <div className="App">
      <h1>Hello About</h1>
    </div>
  );
}

React page with side navigation

Running the timer

Now lets add the basic logic to run a timer in the Home component. We will maintain a state for the count, that will increase every second by 1.

We will run a setTimeout every second, inside the useEffect() hook and it will have the count as dependency. Thus this will go into recursion triggering the setTimeout on every count change, which will be after 1 second.

import "./styles.css";
import { useState, useRef, useEffect } from "react";

export default function Home() {
  const timerRef = useRef();
  const [time, setTime] = useState(0);

  useEffect(() => {
    timerRef.current = setTimeout(() => {
      setTime((prev) => {
        return prev + 1;
      });
    }, 1000);

    return () => {
      clearTimeout(timerRef.current);
    };
  }, [time]);

  return (
    <div className="App">
      <h1>Hello Home</h1>
      <p>Time: {time}</p>
    </div>
  );
}

As soon as the component mounts the timer will start, as Home is at index route, you will see the timer running.
Timer running on Home page

Maintaining the timer state

The most challenging piece is maintaining the timer state.

Now there are multiple ways to do it.
1. Have a centralized state, so that you can pick from where you have left.
2. But if the state is in-memory it will be reset when page reloads.

The best way will be to persist the state.

Because you are interviewing for senior staff role, it expected that you ask all the necessary questions.

The first thing clarifying if it is a server-side rendered page or client-side and depending upon that you will decide your stratergy to persist the timer.

Me assuming this is a client-side application and we are in interview. I will use localstorage to persist the state.

Persisting can be solved with this, but another challenge is updating the local-state before the component is mounted and for that we will make use of useLayoutEffect() hook, using which we will read from localstorage and update the state, which will then increment with the timer as the normal flow.

We will need to use another state as there could be race condition where the useEffect() is fired before useLayoutEffect() state update.

Finally keep upon updating the localstorage with the updated timer value.

import "./styles.css";
import { useState, useRef, useEffect, useLayoutEffect } from "react";

export default function Home() {
  const timerRef = useRef();
  const [time, setTime] = useState(0);
  const [startTimer, setStartTimer] = useState(false);

  // synchronize initially
  useLayoutEffect(() => {
    const timer = window.localStorage.getItem("timer");
    setTime(parseInt(timer) ?? 0);
    setStartTimer(true);
  }, []);

  useEffect(() => {
    if (startTimer) {
      window.localStorage.setItem("timer", time);
      timerRef.current = setTimeout(() => {
        setTime((prev) => {
          return prev + 1;
        });
      }, 1000);
    }

    return () => {
      clearTimeout(timerRef.current);
    };
  }, [time, startTimer]);

  return (
    <div className="App">
      <h1>Hello Home</h1>
      <p>Time: {time}</p>
    </div>
  );
}

This will pause the time when you navigate from the page and resume once you are back to the same page.