How to navigate another page in a single panel using UXP for PS and React.js

Hello,

I want to use React Router DOM to switch between panels in Adobe Photoshop. For example, I have login and operation panels. When clicking on the submit button I want to navigate to the task page instead of starting off with multiple panels as in the attached screenshot.

However, I started developing this using the example and created two panels login and task page. This project seems to have a class-based approach instead of the commonly used functional approach. Is it necessary to manage panels? And isn’t it possible to use React Router DOM?

Screen Shot 2022-08-10 at 18.13.11

Any help would be appreciated. Many thanks!

This may be what you looking for : Manifest v5

Thank you for this documentation. I could not really see any information about how to use a single panel for multiple pages. I want to create a single panel plugin and use this panel for different pages (login page, operation page, etc.)

Your page is just an HTML. You need to re-render it after user logs in, just like you do it on web. I don’t think there’s something special with UXP in this regard

I don’t know if the question was how to use the React router, but if that is the case try this (functional-based) approach:

import { BrowserRouter as Router, Route, Link, Routes } from "react-router-dom";

const Home = () => {
  return (
    <div>
      <h2>Home</h2>
    </div>
  );
};

const About = () => {
  return (
    <div>
      <h2>About</h2>
    </div>
  );
};

const Dashboard = () => {
  return (
    <div>
      <h2>Dashboard</h2>
    </div>
  );
};

const App = () => {
  return (
    <Router>
      <div>
        <ul>
          <li>
            <Link to="/">Home</Link>
          </li>
          <li>
            <Link to="/about">About</Link>
          </li>
          <li>
            <Link to="/dashboard">Dashboard</Link>
          </li>
        </ul>
        <hr />
        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="/about" element={<About />} />
          <Route path="/dashboard" element={<Dashboard />} />
        </Routes>
      </div>
    </Router>
  );
};

export default App;

Think of each page as a separate component and some navigation that will persist on all pages like you will see from this. I used the latest React router (6.3.0)

2 Likes

And if you don’t want to use buttons instead of links you can write it like this:

index.js

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

ReactDOM.render(
  <BrowserRouter>
    <App />
  </BrowserRouter>,
  document.getElementById("root")
);

App.js

import { Routes, Route, useNavigate } from "react-router-dom";

const Home = () => {
  return (
    <div>
      <h2>Home</h2>
    </div>
  );
};

const About = () => {
  return (
    <div>
      <h2>About</h2>
    </div>
  );
};

const Dashboard = () => {
  return (
    <div>
      <h2>Dashboard</h2>
    </div>
  );
};

const App = () => {
  const navigate = useNavigate();
  return (
    <div>
      <nav>
        <button onClick={() => navigate("/")}>Home</button>
        <button onClick={() => navigate("/About")}>About</button>
        <button onClick={() => navigate("/")}>Dashboard</button>
      </nav>
      <hr />
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/about" element={<About />} />
        <Route path="/dashboard" element={<Dashboard />} />
      </Routes>
    </div>
  );
};

export default App;

It is important to wrap the parent component with <BrowserRouter> in order to use the useNavigation hook.

1 Like

Thank you so much for your example. I tried this, it would work perfectly if I was developing a function driven react app. My actual problem is using React Routing with PanelController for the Photoshop plugin. I can not integrate a code piece like yours into this panel controller logic.

Panel Controller

import ReactDOM from "react-dom";

const _id = Symbol("_id");
const _root = Symbol("_root");
const _attachment = Symbol("_attachment");
const _Component = Symbol("_Component");
const _menuItems = Symbol("_menuItems");

export class PanelController {
    
    constructor(Component, { id, menuItems } = {}) {
        this[_id] = null;
        this[_root] = null;
        this[_attachment] = null;
        this[_Component] = null;
        this[_menuItems] = [];

        this[_Component] = Component;
        this[_id] = id;
        this[_menuItems] = menuItems || [];
        this.menuItems = this[_menuItems].map(menuItem => ({
            id: menuItem.id,
            label: menuItem.label,
            enabled: menuItem.enabled || true,
            checked: menuItem.checked || false
        }));

        [ "create", "show", "hide", "destroy", "invokeMenu" ].forEach(fn => this[fn] = this[fn].bind(this));
    }

    create() {
        this[_root] = document.createElement("div");
        this[_root].style.height = "100vh";
        this[_root].style.overflow = "auto";
        this[_root].style.padding = "8px";

        ReactDOM.render(this[_Component]({panel: this}), this[_root]);

        return this[_root];
    }

    show(event)  {
        if (!this[_root]) this.create();
        this[_attachment] = event;
        this[_attachment].appendChild(this[_root]);
    }

    hide() {
        if (this[_attachment] && this[_root]) {
            this[_attachment].removeChild(this[_root]);
            this[_attachment] = null;
        }
    }

    destroy() { }

    invokeMenu(id) {
        const menuItem = this[_menuItems].find(c => c.id === id);
        if (menuItem) {
            const handler = menuItem.oninvoke;
            if (handler) {
                handler();
            }
        }
    }
}

index.js

import React from "react";

import "./styles.css";
import { PanelController } from "./controllers/PanelController.jsx";
import { CommandController } from "./controllers/CommandController.jsx";
import { About } from "./components/About.jsx";
import { Demos } from "./panels/Demos.jsx";
import { MoreDemos } from "./panels/MoreDemos.jsx";


import { entrypoints } from "uxp";

// opens about 
const aboutController = new CommandController(({ dialog }) => <About dialog={dialog}/>, { id: "showAbout", title: "React Starter Plugin Demo", size: { width: 480, height: 480 } });

// opens Demo panel
const demosController =  new PanelController(() => <Demos/>, {id: "demos", menuItems: [
    { id: "reload1", label: "Reload Plugin", enabled: true, checked: false, oninvoke: () => location.reload() },
    { id: "dialog1", label: "About this Plugin", enabled: true, checked: false, oninvoke: () => aboutController.run() },
] });


// opens demo panel 2
const moreDemosController =  new PanelController(() => <MoreDemos/>, { id: "moreDemos", menuItems: [
    { id: "reload2", label: "Reload Plugin", enabled: true, checked: false, oninvoke: () => location.reload() }
] });


entrypoints.setup({
    plugin: {
        create(plugin) {
            /* optional */ console.log("created", plugin);
        },
        destroy() {
            /* optional */ console.log("destroyed");
        }
    },
    commands: {
        showAbout: aboutController
    },
    panels: {
        demos: demosController,
        moreDemos: moreDemosController
    }
});

How can I use the route components when I use the photoshop panel controller?

Alright, I see now. Well, you can try this functional-based approach for the usage of the PanelController.

PanelController.js

import ReactDOM from "react-dom";

export const PanelController = (component, { menuItems, invokeMenu }) => {
  var root = null;
  var attachment = null;

  const controller = {
    create() {
      root = document.createElement("div");
      root.style.height = "100vh";
      root.style.overflow = "auto";
      root.style.padding = "8px";
      ReactDOM.render(component, root);
      console.log("Panel created");
    },
    destroy() {
      console.log("Panel destroyed");
    },
    show({ node }) {
      if (!root) {
        controller.create();
      }
      if (!attachment) {
        attachment = node; // the <body>
        attachment.appendChild(root);
      }
      console.log("Panel shown");
    },
    hide() {
      if (attachment && root) {
        attachment.removeChild(root);
        attachment = null;
      }
      console.log("Panel hidden");
    },
    menuItems,
    invokeMenu,
  };
  return controller;
};

index.js

import { entrypoints } from "uxp";
import App from "./App";
import showAbout from "./commands/showAbout";
import { PanelController } from "./controllers/PanelController";

const flyout = {
  menuItems: [
    /* ... */
  ],
  invokeMenu(id) {
    /* ... */
  },
};

entrypoints.setup({
  commands: { showAbout },
  panels: {
    controllerpanel:PanelController(<App />, { ...flyout }),
  },
});

Also, it is important that in the manifest.json you configure the entrypoints ids. So the controllerpanel is the id that can be found inside manifest entrypoints.

But again if it is the only task to show/hide content (pages) you can do it even without React router. Just a simple conditional rendering will do. Look at the example:

import { useState } from "react";

const About = () => {
  return (
    <div>
      <h1>About</h1>
    </div>
  );
};

const Dashboard = () => {
  return (
    <div>
      <h1>Dashboard</h1>
    </div>
  );
};

const App = () => {
  const [contentId, setContentId] = useState("");

  const handleShowContent = (e) => {
    setContentId(e.target.id);
  };

  return (
    <div>
      <nav>
        <button onClick={handleShowContent} id="about">
          About
        </button>
        <button onClick={handleShowContent} id="dashboard">
          Dashboard
        </button>
      </nav>
      {contentId.toLowerCase() === "about" && <About />}
      {contentId.toLowerCase() === "dashboard" && <Dashboard />}
    </div>
  );
};

export default App;

Thank you I applied this logic to my project but I somehow can not make it work because of the BrowserRouter component. Have you ever implemented a working example with uxp&react routing? I mean is this possible?

I really don’t know what you are trying to do. I think that you need to decide what is the simplest possible way to achieve that. No need to overengineer. If it is just to show/hide content you can do it on a single panel and no need to use multiple panels, if you need to share some data between the panels I would recommend using the Redux Toolkit for that, etc.

Yes. I think the simplest way to develop a Photoshop UXP plugin that behaves like a single-page web application is to use navigation and conditional rendering. Conditional rendering provides you with the expected functionality although this approach may prevent you from following the best coding practices from time to time. Thank you @bbogdan!

1 Like

You’re welcome. Conditional rendering is totally legit way if you are using React. Everything really depends on the type of the functionality you are trying to get. From my personal experience, every time something is over engineered it usually fails somewhere because it requires even more code to prevent bugs and unexpected behavior. That’s why simpler approach is better for the development and also for the end user.

1 Like