Request for Code Review - Accessing State in Theme

I have a requirement to access state when theming extensions. The reason is that I need access to a React theme object which is passed down via context. The examples of theming I have seen are all static and use EditorView.theme(…).

I came up with a solution using a computed EditorView.styleModule however I’m not sure if this is the best way to do it.

As you can see this createTheme function takes a callback which is given the chakra theme object. However I wasn’t able to access the theme facet or the buildTheme function so my work arounds are a bit hacky.

Keen to hear if there is a better way. Or maybe a computed theme option exposed via EditorView is something to consider?

export function createTheme(
  stylesFn: (props: StylesFnProps) => { [selector: string]: StyleSpec },
): Extension {
  // The EditorView.theme function returns an array of extensions, the first element is the theme extension
  // See https://github.com/codemirror/view/blob/6.33.0/src/editorview.ts#L1098-L1103
  const [theme] = EditorView.theme({}) as Extension[];

  return [
    theme,
    EditorView.styleModule.compute([chakraThemeField], (state) => {
      const chakraTheme = state.field(chakraThemeField);

      if (!chakraTheme) {
        throw new Error(
          'No chakraThemeField found, did you forget to add the chakraTheme(...) extension?',
        );
      }

      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      const prefix = (theme as any).value; // Using the internal API of theme facet, not ideal

      if (!prefix) throw new Error('No theme prefix found');

      const spec = stylesFn({ theme: chakraTheme });

      return buildTheme(`.${prefix}`, spec);
    }),
  ];
}

// Taken from https://github.com/codemirror/view/blob/6.33.0/src/theme.ts#L12-L22
function buildTheme(
  main: string,
  spec: { [name: string]: StyleSpec },
  scopes?: { [name: string]: string },
) {
  return new StyleModule(spec, {
    finish(sel) {
      return /&/.test(sel)
        ? sel.replace(/&\w*/, (m) => {
            if (m == '&') return main;
            if (!scopes || !scopes[m])
              throw new RangeError(`Unsupported selector: ${m}`);
            return scopes[m];
          })
        : main + ' ' + sel;
    },
  });
}

I cannot really figure out what you’re trying to do, but I agree that it’s not a good idea (a number of these internals may change and break your code). Why not use EditorView.theme directly and just reconfigure your state when the input changes?

Also be aware that mounted styles aren’t cleaned up, so if you keep dynamically adding style modules, that might be an issue.