Search code examples
javascriptreactjsreact-routerreact-router-dom

Can I call a handler function from my main layout from an outlet in React?


I have a React application with a main layout and child 'outlets'. One of those outlets is a user profile management page where the users image can be updated. The user profile image is also shown on the main layout menu. I wish to be able to call a handler function in my main layout to update the image when the user changes the image in the profile outlet.

Here is my code so far.

App.jsx

import { BrowserRouter as Router, Route, Routes, Outlet } from "react-router-dom";
import { AuthProvider } from "./context/AuthContext";

import HomePage from "./pages/HomePage";
import LoginPage from "./pages/LoginPage";
import Profile from "./pages/Profile";
import PrivateRoutes from "./utils/PrivateRoutes";

//Prime React styles
import "primeflex/primeflex.css";
import "primereact/resources/primereact.min.css";
import "primeicons/primeicons.css";

function App() {
  return (
    <div className="App">
      <Router>
        <AuthProvider>
          <Routes>
            <Route path="/login" element={<LoginPage />} />
            <Route element={<PrivateRoutes />}>  
              <Route path="/Profile" element={<Profile />} />
              <Route path="/" element={<HomePage />} />
            </Route>     
          </Routes> 
        </AuthProvider>
      </Router>
    </div>
  );
}

export default App;

PrivateRoutes.jsx

import { Outlet, Navigate } from 'react-router-dom'
import { useContext } from 'react'
import AuthContext from '../context/AuthContext'
import Layout from '../components/Layout';

const MainLayout = () => (
  <>
    <Layout
      childpage={<Outlet handleButtonClicked={Layout.handleButtonClicked />}
    />
  </>
);

const PrivateRoutes = () => {
  let { user } = useContext(AuthContext)
  return user ? <MainLayout /> : <Navigate to="/login" />
}

export default PrivateRoutes

Layout.jsx

import React, { useContext, useState, useEffect } from "react";

import AuthContext from "../context/AuthContext";
import { NavLink } from "react-router-dom";
import useAxios from "../interceptors/useAxios";

const Layout = ({ childpage }) => {
  let { logoutUser } = useContext(AuthContext);
  const { user } = useContext(AuthContext);
  let [image, setimage] = useState("http://localhost:8000/media/default.jpg");
  const api = useAxios();

  useEffect(() => {
    api.post("/api/profile", { username: user.username }).then((response) => {
      setimage("http://localhost:8000" + response.data.image);  
    });
  }, []); // Empty dependency array ensures this runs only on first load

  //***HANDLER*****
  const handleButtonClicked = () => {
    console.log("Button was clicked");
  };
 
  return user ? (
    <div className="min-h-screen flex relative lg:static surface-ground">
      <div
        id="app-sidebar-5"
        className="bg-gray-900 h-screen hidden lg:block flex-shrink-0 absolute lg:static left-0 top-0 z-1 border-right-1 border-gray-800 w-18rem lg:w-7rem select-none"
      >
        <div className="flex flex-column h-full">
          <div
            className="flex align-items-center justify-content-center flex-shrink-0 bg-indigo-500"
            style={{ height: "60px" }}
          >
            <img
              src="/images/logos/hyper-light.svg"
              alt="hyper-light"
              height={30}
            />
          </div>
          <div className="mt-auto mx-3">
            <hr className="mb-3  border-top-1 border-gray-800" />
            <a
              className="p-ripple my-3 flex flex-row lg:flex-column align-items-center cursor-pointer p-3 lg:justify-content-center hover:bg-gray-800 border-round text-gray-300 hover:text-white transition-duration-150 transition-colors w-full"
            >
            <NavLink to="/profile"  activeclassname="active">
                <div className="p-ripple flex flex-row lg:flex-column align-items-center cursor-pointer p-3 lg:justify-content-center hover:bg-gray-800 border-round text-gray-300 hover:text-white transition-duration-150 transition-colors w-full">
                <img
                src={image}
                className="mr-2 lg:mr-0"
                style={{
                  width: "32px",
                  height: "32px",
                }}
                alt="avatar-f-1"
              />
                </div>
              </NavLink>
            </a>
          </div>
        </div>
      </div>
      <div className="min-h-screen flex flex-column relative flex-auto">
        <div
          className="flex justify-content-between align-items-center px-5 surface-section relative lg:static border-bottom-1 surface-border"
          style={{ height: "60px" }}
        >
          <div className="flex">
    
          </div>

          <ul
            className="list-none p-0 m-0 hidden lg:flex lg:align-items-center select-none lg:flex-row
                    surface-section border-1 lg:border-none surface-border right-0 top-100 z-1 shadow-2 lg:shadow-none absolute lg:static"
          >
            <li>
              <a
                onClick={logoutUser}
                className="p-ripple flex p-3 lg:px-3 lg:py-2 align-items-center text-600 hover:text-900 hover:surface-100 font-medium border-round cursor-pointer
                            transition-duration-150 transition-colors"
              >
                <i className="pi pi-sign-out text-base lg:text-2xl mr-2 lg:mr-0 p-overlay-badge"></i>
                <span className="block lg:hidden font-medium">Log out</span>
              </a>
            </li>
          </ul>
        </div>
      
        <div className="p-5 flex flex-column flex-auto max-h-screen">
           <div className="surface-card p-4 shadow-2 border-round overflow-y-auto"> 
            <main>{childpage}</main>
          </div>
        </div>
      </div>
    </div>
  ) : (
    <div>
      <p>You are not logged in, redirecting...</p>
    </div>
  );
};

export default Layout;

Profile.jsx

import React from 'react';

const Profile = ({ handleButtonClicked  }) => {
  return (
    <button onClick={ handleButtonClicked }>Trigger Handler in Layout</button>
  );
};

export default Profile;

But and error in thrown from PrivateRoutes.jsx that handlebuttonclick is undefined.

I wish to call my handler which is defined in Layout.jsx from Profile.jsx.

Profile.jsx is one of my 'childpages' that is wrapped by Layout.jsx


Solution

  • You can provide the handleButtonClicked callback function to descendent route component via the Outlet component's provided context, accessible in the routed component via the useOutletContext hook.

    Update Layout to render the Outlet directly and provide the callback function.

    import React, { useContext, useState, useEffect } from "react";
    import AuthContext from "../context/AuthContext";
    import { NavLink, Outlet } from "react-router-dom";
    import useAxios from "../interceptors/useAxios";
    
    const Layout = () => {
      ...
    
      const handleButtonClicked = () => {
        console.log("Button was clicked");
      };
      
      return user ? (
        <div className="....">
          ...
          <div className="....">
            ...
          
            <div className="....">
              <div className="...."> 
                <main>
                  <Outlet context={{ handleButtonClicked }}
                </main>
              </div>
            </div>
          </div>
        </div>
      ) : (
        ...
      );
    };
    
    export default Layout;
    
    const MainLayout = () => (
      <>
        <Layout />
      </>
    );
    
    import React from 'react';
    import { useOutletContext } from 'react-router-dom';
    
    const Profile = () => {
      const { handleButtonClicked } = useOutletContext();
    
      return (
        <button onClick={handleButtonClicked}>
          Trigger Handler in Layout
        </button>
      );
    };
    
    export default Profile;