admin管理员组

文章数量:1352869

I've got a React application, in which I need to display an HTML Popup with some dynamic content generated by the server. This content also contains some JSX markup which I want to be rendered into real ponents.

<MyButton onClick={displayInfo}/>

...

async displayInfo() {
    let text = await fetch(...)  

    console.log(text) // "<SomeComp onClick={foo}><OtherComp..... etc

    let ponent = MAGIC(text)

    ReactDom.render(ponent, '#someDiv')
}

Due to how my server app is structured, ReactServerDOM und hydration is not an option.

Is there a way to implement this on the client side only?

I've got a React application, in which I need to display an HTML Popup with some dynamic content generated by the server. This content also contains some JSX markup which I want to be rendered into real ponents.

<MyButton onClick={displayInfo}/>

...

async displayInfo() {
    let text = await fetch(...)  

    console.log(text) // "<SomeComp onClick={foo}><OtherComp..... etc

    let ponent = MAGIC(text)

    ReactDom.render(ponent, '#someDiv')
}

Due to how my server app is structured, ReactServerDOM und hydration is not an option.

Is there a way to implement this on the client side only?

Share Improve this question asked May 27, 2022 at 11:25 goggog 11.3k2 gold badges28 silver badges42 bronze badges 3
  • 1 Maybe this could be helpful – BENARD Patrick Commented May 27, 2022 at 11:31
  • 3 really strange if you ask me. Why dont you just fetch the raw data and pass it as props to an client ponent? – Ilijanovic Commented May 29, 2022 at 12:05
  • @Ifaruki: because I don't know in advance which ponents are going to be used in the popup. This is decided dynamically on the server side. – gog Commented May 29, 2022 at 12:15
Add a ment  | 

3 Answers 3

Reset to default 8 +150

As others mentioned, this has code smell. JSX is an intermediary language intended to be piled into JavaScript, and it's inefficient to pile it at run time. Also it's generally a bad idea to download and run dynamic executable code. Most would say the better way would be to have ponents that are driven, not defined, by dynamic data. It should be possible to do this with your use case (even if the logic might be unwieldy), though I'll leave the pros/cons and how to do this to other answers.

Using Babel

But if you trust your dynamically generated code, don't mind the slower user experience, and don't have time/access to rewrite the backend/frontend for a more efficient solution, you can do this with Babel, as mentioned by BENARD Patrick and the linked answer. For this example, we'll use a version of Babel that runs in the client browser called Babel Standalone.

Using Dynamic Modules

There needs to be some way to run the piled JavaScript. For this example, I'll import it dynamically as a module, but there are other ways. Using eval can be briefer and runs synchronously, but as most know, is generally considered bad practice.

function SomeComponent(props) {
  // Simple ponent as a placeholder; but it can be something more plicated
  return React.createElement('div', null, props.name);
}
async function importJSX(input) {
  // Since we'll be dynamically importing this; we'll export it as `default`
  var moduleInput = 'export default ' + input;
  var output = Babel.transform(
    moduleInput,
    {
      presets: [
        // `modules: false` creates a module that can be imported
        ["env", { modules: false }],
        "react"
      ]
    }
  ).code;
  // now we'll create a data url of the piled code to import it
  var dataUrl = 'data:text/javascript;base64,' + btoa(output);
  return (await import(dataUrl)).default;
}

(async function () {
  var element = await importJSX('<SomeComponent name="Hello World"></SomeComponent>');
  var root = ReactDOM.createRoot(document.getElementById('root'));
  root.render(element);
})();
<script src="https://unpkg./@babel/standalone@7/babel.min.js"></script>
<script src="https://unpkg./react@18/umd/react.development.js" crossorigin></script>
<script src="https://unpkg./react-dom@18/umd/react-dom.development.js" crossorigin></script>
<div id="root"></div>

Adding a Web Worker

For a better user experience, you'd probably want to move the pilation to a web worker. For example:

worker.js

importScripts('https://unpkg./@babel/standalone@7/babel.min.js');
self.addEventListener('message', function ({ data: { input, port }}) {
  var moduleInput = 'export default ' + input;
  var output = Babel.transform(
    moduleInput,
    {
      presets: [
        // `modules: false` creates a module that can be imported
        ["env", { modules: false }],
        "react"
      ]
    }
  ).code;
  port.postMessage({ output });
});

And in your main script:

var worker = new Worker('./worker.js');
async function importJSX(input) {
  var piled = await new Promise((resolve) => {
    var channel = new MessageChannel();
    channel.port1.onmessage = (e) => resolve(e.data);
    worker.postMessage({ input, port: channel.port2 }, [ channel.port2 ]);
  });
  var dataUrl = 'data:text/javascript;base64,' + btoa(piled.output);
  return (await import(dataUrl)).default;
}

(async function () {
  var root = ReactDOM.createRoot(document.getElementById('root'));
  var element = await importJSX('<div>Hello World</div>');
  root.render(element);
})();

This assumes React and ReactDOM are imported already, and there's a HTML element with id root.

Skipping JSX and Compilation

It's worth mentioning that if you're generating JSX dynamically, it's usually only slightly more plex to instead generate what that JSX would pile to.

For instance:

<SomeComponent onClick={foo}>
  <div id="container">Hello World</div>
</SomeComponent>

When piled for React is something like:

React.createElement(SomeComponent,
  {
    onClick: foo
  },
  React.createElement('div',
    {
      id: 'container'
    },
    'Hello World'
  )
);

(See https://reactjs/docs/jsx-in-depth.html for more information on how JSX gets piled and https://babeljs.io/repl for an interactive website to pile JSX to JS using Babel)

While you could run Babel server-side to do this (which would add additional overhead) or you could find/write a pared-down JSX piler, you also probably can also just rework the functions that return JSX code to ones that return regular JavaScript code.

This offers significant performance improvements client-side since they wouldn't be downloading and running babel.

To actually then use this native JavaScript code, you can export it by prepending export default to it, e.g.:

export default React.createElement(SomeComponent,
  ...
);

And then dynamically importing it in your app, with something like:

async function displayInfo() {
    let ponent = (await import(endpointURL)).default;
    ReactDom.render(ponent, '#someDiv');
}

As others have pointed out, Browsers don't understand JSX. Therefore, there's no runtime solution you can apply that will actually work in a production environment.

What you can do instead:

  1. Write a bunch of pre-defined ponents in the front-end repository. (The JSX with the server).
  2. Write a HOC or even just a ponent called ComponentBuilder will do.
  3. Your <ComponentBuilder /> is nothing but a switch statement that renders one or more of those pre-defined ponents from step 1 based on what ever you pass to the switch statement.
  4. Ask the server to send keywords instead of JSX. Use the server sent keyword by passing it to the switch statement. You can see the example below.
  5. Now your server decides what to render. You don't need a runtime solution.
  6. Stick the <ComponentBuilder /> where ever you want to render ponents decided by the server.
  7. Most importantly, you won't give away a front-end responsibility into the hands of the backend. That's just "very difficult to maintain" code. Especially if you start to bring in styles, state management, API calls, etc.
// Component Builder code

const ComponentBuilder = () => {
  const [apiData, setApiData] = useState()

  useEffect(() => {
    // make api call.
    const result = getComponentBuilderDataFromAPI()

    if (result.data) {
      setApiData(result.data)
    }
  }, [])

  switch (apiData.ponentName) {
    case 'testimonial_cp':
      return <Testimonials someProp={apiData.props.someValue || ''} />
    case 'about_us_cp':
      return <AboutUs title={apiData.props.title || 'About Us'} />
    // as many cases as you'd like
    default:
      return <Loader />
  }
}

This will be a lot more efficient, maintainable and the server ends up deciding which ponent to render.

Just make sure not to mix up responsibilities. What is to be on the front end must be on the front end.

eg: don't let the server send across HTML or CSS directly. Instead have a bunch of predefined styles on the front-end and let the server just send the className. That way the server is in control of deciding the styles but the styles themselves live on the front-end. Else, it bees really difficult to make changes and find bugs.

The server sends a bunch of CSS properties. You have a bunch of ponent styles. There are a bunch of global styles. You also use Tailwind or Bootstrap. You will not know what exactly is setting the style or even who's controlling it. A nightmare to debug and maintain! Saying this from experience.

The above solution should work for you provided you can make the required changes to your architecture! Because it's working in a production environment as I write this answer.

Actually, you don't need a piler, but a parser.

Here's the demo, and the process is parsing string to dom, and transform dom to react elements, then render to react tree.

const { Parser } = require("htmlparser2");
const { DomHandler } = require("domhandler");
const React = require('react');
const rawHtml = "<SomeComp onClick={foo}><div>foo</div></SomeCompo>";
const handler = new DomHandler((error, nodes) => {
  if (error) {
    // Handle error
  } else {
    // Parsing pleted, do something

    // TODO: store all available ponent here
    const ponentMapping = {};

    // TOOD: store all available props for dom
    const currentContext = {};
    const getComponent = (tag) => {
      // html tag
      if (tag.toLowerCase() === tag) {
        return tag;
      }

      // TODO: add default tag for testing
      return ponentMapping[tag] || tag;
    };

    const getComponentProps = (attrs) => {
      return Object.keys(attrs).reduce((p, c) => {
        const value = attrs[c];
        const match = /^{(.+)}$/.exec(value);
        if (match) {
          // TODO: here need to consider number, string, object, function types
          p[c] = currentContext[match[1]] || match[1];
        } else {
          p[c] = value;
        }

        return p;
      }, {});
    };

    const walk = (dom, parent) => {
      // if is text node
      if (dom.type === 'text') {
        parent.push(dom.data);
        return;
      }

      const Component = getComponent(dom.name);
      const current = [];
      for (let child of dom.children) {
        walk(child, current);
      }
      const node = React.createElement(Component, {
        children: current,
        ...getComponentProps(dom.attribs),
      });

      if (parent) {
        parent.push(node);
      } else {
        return node;
      }
    };

    const result = walk(nodes[0], null);
    console.log(result);
    // TODO: render react element to current 
  }
});
const parser = new Parser(handler, {
  lowerCaseTags: false,
  lowerCaseAttributeNames: false,
  recognizeSelfClosing: true,
});
parser.write(rawHtml);
parser.end();

本文标签: javascriptCompile a JSX string to a component on the flyStack Overflow