How to make React component aware of theme switch?

My problem is, that I can not detect correct theme on a dialog after it’s switched.

Steps to reproduce:

  1. Add provided component to the panel - it will render a button
  2. Click the button - it will show a dialog with active theme
  3. Close dialog
  4. Switch theme
  5. Click button again - still shows old theme

I want it to show current theme

This is a very simplified version of what I have (kept it in a single file to not pollute the code)

// DialogThemeTest.tsx

import ReactDOM from "react-dom";
import photoshop from "photoshop";

const Theme = {
    DARKEST: "darkest",
    DARK: "dark",
    LIGHT: "light",
    LIGHTEST: "lightest",
}

const ThemeMap = new Map([
    ["kPanelBrightnessDarkGray", Theme.DARKEST],
    ["kPanelBrightnessMediumGray", Theme.DARK],
    ["kPanelBrightnessLightGray", Theme.LIGHT],
    ["kPanelBrightnessOriginal", Theme.LIGHTEST],
])

const getTheme = () => {
    const result = photoshop.action.batchPlay(
        [
            {
                "_obj": "get",
                "_target": [
                    {"_property": "kuiBrightnessLevel"},
                    {"_ref": "application", "_enum": "ordinal", "_value": "targetEnum"}
                ],
                "_options": {"dialogOptions": "dontDisplay"}
            }
        ],
        {"synchronousExecution": true});

    const pinned = result[0].kuiBrightnessLevel._value;

    return ThemeMap.get(pinned) ?? Theme.DARK
}

const DialogContent = ({dialog}) =>
    <>
        <sp-body>{getTheme()}</sp-body>
        <sp-footer>
            <sp-button onClick={() => dialog.close()}>Close</sp-button>
        </sp-footer>
    </>

const dialogController = (Component) => {
    let dialogElement

    return async () => {
        if (!dialogElement) {
            dialogElement = document.createElement("dialog");
            ReactDOM.render(Component({dialog: dialogElement}), dialogElement);
        }

        document.body.appendChild(dialogElement);
        await dialogElement.uxpShowModal({title: "dialogThemeTest"});
        dialogElement.remove();
    }
}

const dialogOpener = () => dialogController(DialogContent)

export default () =>
    <sp-button onClick={dialogOpener()}>
        Check theme
    </sp-button>

I was thinking about unmounting the dialog, but it gives me error it was rendered with different version of React :confused:

Could you maybe post a screenshot or video demonstrating the behavior? I think I didn’t quite understand the full use case yet.

Also, you could try to put a console.log into the getTheme function. My first suspicion is that this gets only called once at the start.

And you’re right. It is called only once. I believe it’s because DialogContent is already in a variable and you can pass it to dialog controller what you want - it already has the result of getTheme(). So that’s the part I can’t figure out how to force re-render or reinitiate that content.

I also believe it should work with proper unmount by calling ReactDOM.unmountComponentAtNode(dialogElement) right before dialogElement.remove(), but then I get this error “The node you’re attempting to unmount was rendered by another copy of React” (scroll down to the very last comment)

Here’s the video

Unmounting dialogElement will not solve the issue, which is that the DialogContent component does not re-render. There are multiple ways to do that, one of that is updating its props. Interestingly, even if you create a new dialog element every time, this doesn’t trigger a re-render as React thinks it’s the same object. Passing the current theme however works fine:

const DialogContent = ({dialog, theme}) => {
  return <>
        <sp-body>{theme}</sp-body>
        <sp-footer>
            <sp-button onClick={() => dialog.close()}>Close</sp-button>
        </sp-footer>
    </>
}

const dialogController = (Component) => {
    let dialogElement
    return async () => {
        dialogElement = document.createElement("dialog");
        ReactDOM.render(Component({dialog: dialogElement, theme: getTheme()}), dialogElement);
        document.body.appendChild(dialogElement);
        await dialogElement.uxpShowModal({title: "dialogThemeTest"});
        dialogElement.remove();
    }
}

If you rather want to encapsulate the theme fetching logic in the DialogContent as you did before, I’d probably make use of the key property that React uses to reference component instances.

const DialogContent = ({dialog}) => {
  return <>
        <sp-body>{getTheme()}</sp-body>
        <sp-footer>
            <sp-button onClick={() => dialog.close()}>Close</sp-button>
        </sp-footer>
    </>
}

const dialogController = (Component) => {
    let dialogElement
    return async () => {
        dialogElement = document.createElement("dialog");
        ReactDOM.render(Component({dialog: dialogElement, key: Math.random()}), dialogElement);
        document.body.appendChild(dialogElement);
        await dialogElement.uxpShowModal({title: "dialogThemeTest"});
        dialogElement.remove();
    }
}

Instead of Math.random, you could also assign a unique UUID for the dialog every time. There’s probably a more elegant solution to solve all this, but this was the first thing that came to my mind.

Thans once again :slight_smile: I’ll stick with the key solution at least until I become aware of a better one, because I think it’s more correct - when I close the dialog, I want it to re-rendered next time I open it. This actually might also help solving another very similar issue I’m having (will check that later)

This is the bit I changed to (always creating new dialogElement and uuid() on dialog open)

return async () => {
        const dialogElement = document.createElement("dialog")
        ReactDOM.render(Component({key: uuid(), dialog: dialogElement}), dialogElement)

        document.body.appendChild(dialogElement);

        await dialogElement.uxpShowModal(dialogOpts)
        dialogElement.remove()
    }