javascriptreactjsreact-hooksreact-router

How come visiting child component loses functionality?


I have an app that has a 3 JSX files:

  1. one for controlling states

    import React from "react";
    import CreateRisk from "./CreateRisk";
    import TablePage from "./TablePage";
    import { Link } from "react-router";
    
    const CommonAncestor = () => {
      // Lazy initialize state from localStorage
      const [allRisks, setAllRisks] = React.useState(() => {
        return JSON.parse(localStorage.getItem("allrisks")) || [];
      });
    
      // Side-effect to persist state to localStorage
      React.useEffect(() => {
        localStorage.setItem("allrisks", JSON.stringify(allRisks));
      }, [allRisks]);
    
      // Handler to update state
      const addRisk = (risk) => {
        setAllRisks((allRisks) => allRisks.concat(risk));
      };
    
      return (
        <>
          ...
          <CreateRisk addRisk={addRisk} />
          ...
          <TablePage rows={allRisks} />
          ...
        </>
      );
    };
    
    export default CommonAncestor;
    
  2. one for creating items

    import { nanoid } from "nanoid";
    import { Link } from "react-router";
    
    function CreateRisk({ addRisk }) {
      function handleSubmission() {
        addRisk({
          id: nanoid(),
          riskname: document.getElementById("rname").value,
          riskdesc: document.getElementById("rdesc").value,
          riskimpact: document.getElementById("rimpact").value,
          test: "test",
        });
      }
    
      return (
        <div>
          <input type="text" id="rname" placeholder="Risk Name" />
          <input type="text" id="rdesc" placeholder="Risk Description" />
          <input type="text" id="rimpact" placeholder="Risk Impact" />
          <button onClick={handleSubmission}>Store my data</button>
        </div>
      );
    }
    
    export default CreateRisk;
    
  3. one for viewing created items as a table.

    import { Link } from "react-router";
    function TablePage({ rows }) {
      return (
        <div>
          <h1>TablePage</h1>
          <div>{JSON.stringify(rows)}</div>
        </div>
      );
    }
    
    export default TablePage;
    

And the main App and index files:

import "./styles.css";
import { Link } from "react-router";
import CommonAncestor from "./CommonAncestor";

export default function App() {
  return (
    <div className="App">
      <h1>Home Page</h1>
      <Link to="/">Home -</Link>
      <Link to="/CreateRisk">Create Risk -</Link>
      <Link to="/TablePage">Table Page -</Link>
      <CommonAncestor />
    </div>
  );
}
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { createBrowserRouter, RouterProvider } from "react-router";
import CreateRisk from "./CreateRisk.jsx";
import TablePage from "./TablePage.jsx";
import CommonAncestor from "./CommonAncestor.jsx";
import App from "./App";

const router = createBrowserRouter([
  {
    path: "/",
    element: <App />,
  },
  {
    path: "/CommonAncestor",
    element: <CommonAncestor />,
  },
  {
    path: "/CreateRisk",
    element: <CreateRisk />,
  },
  {
    path: "/TablePage",
    element: <TablePage />,
  },
]);

createRoot(document.getElementById("root")).render(
  <StrictMode>
    <RouterProvider router={router} />
  </StrictMode>
);

Here is a code sandbox: https://codesandbox.io/p/sandbox/flamboyant-roman-forked-8hw8x2

When viewing the controlling files component(1), the functionality of adding items and viewing the table works. When I go directly to the pages for creating items / viewing the table the functionality does not. Why is this?


Solution

  • Issue

    You've defined all the state and handlers in CommonAncestor and pass them down as props to CreateRisk and TablePage directly, and this works so long as you are on the route that renders CommonAncestor, e.g. the "/" home page route. The problem then is that when you navigate to one of the other routes rendering CreateRisk and TablePage the props are no longer passed.

    Solution

    Colocate the state and handlers again to a "common ancestor" to be passed down to the descendent components. In this case treat App as the common ancestor and convert it to a layout route component where the state and handlers can be passed via a React context, the Outlet component's context in this case.

    Example:

    App

    import React from "react";
    import { Link, Outlet } from "react-router";
    
    export default function App() {
      const [allRisks, setAllRisks] = React.useState(() => {
        return JSON.parse(localStorage.getItem("allrisks")) || [];
      });
    
      // Side-effect to persist state to localStorage
      React.useEffect(() => {
        localStorage.setItem("allrisks", JSON.stringify(allRisks));
      }, [allRisks]);
    
      // Handler to update state
      const addRisk = (risk) => {
        setAllRisks((allRisks) => allRisks.concat(risk));
      };
    
      return (
        <div className="App">
          <h1>Home Page</h1>
          <Link to="/">Home -</Link>
          <Link to="/CreateRisk">Create Risk -</Link>
          <Link to="/TablePage">Table Page -</Link>
    
          <Outlet context={{ allRisks, addRisk }} />
        </div>
      );
    }
    

    CreateRisk

    import { nanoid } from "nanoid";
    import { useOutletContext } from "react-router";
    import { Link } from "react-router";
    
    function CreateRisk() {
      const { addRisk } = useOutletContext();
      function handleSubmission() {
        addRisk({
          id: nanoid(),
          riskname: document.getElementById("rname").value,
          riskdesc: document.getElementById("rdesc").value,
          riskimpact: document.getElementById("rimpact").value,
          test: "test",
        });
      }
    
      return (
        <div>
          <input type="text" id="rname" placeholder="Risk Name" />
          <input type="text" id="rdesc" placeholder="Risk Description" />
          <input type="text" id="rimpact" placeholder="Risk Impact" />
          <button onClick={handleSubmission}>Store my data</button>
        </div>
      );
    }
    
    export default CreateRisk;
    

    TablePage

    import { useOutletContext } from "react-router";
    
    function TablePage() {
      const { allRisks } = useOutletContext();
      return (
        <div>
          <h1>TablePage</h1>
          <div>{JSON.stringify(allRisks)}</div>
        </div>
      );
    }
    
    export default TablePage;
    

    Update the router to nest the other routes as children routes under "/".

    const router = createBrowserRouter([
      {
        path: "/",
        element: <App />,
        children: [
          {
            index: true,
            element: <CommonAncestor />,
          },
          {
            path: "/CreateRisk",
            element: <CreateRisk />,
          },
          {
            path: "/TablePage",
            element: <TablePage />,
          },
        ],
      },
    ]);
    
    createRoot(document.getElementById("root")).render(
      <StrictMode>
        <RouterProvider router={router} />
      </StrictMode>
    );
    

    App contains the state and passes it down to nested children routes via the Outlet context, and the nested route components access the values via the useOutletContext hook.