preactjs / preact

⚛️ Fast 3kB React alternative with the same modern API. Components & Virtual DOM.
https://preactjs.com
MIT License
36.53k stars 1.94k forks source link

PrismJS code is duplicated on 1st re-render #3236

Open jakedee opened 3 years ago

jakedee commented 3 years ago

Code highlighted by PrismJS is duplicated after the first re-render

Steps to reproduce the behaviour:

  1. Go to Codesandbox - Preact
  2. Click on 'Trigger re-render' button

Expected behaviour Codesandbox - React

developit commented 2 years ago

Hmm - this is sortof a fluke in both libraries TBH. React wipes out Prism's injected HTML because they use .textContent to assign the text of Elements with a single Text child. We use a different technique that is faster because it doesn't wipe content.

I believe the fix here is to set dangerouslySetInnerHTML={{ }} (empty) on the <code> element. Or, wrap it in a Component and add shouldComponentUpdate(){return false} to prevent diffing (since it's just thrown away when highlightAll() is called anyway):

globalThis.Prism = globalThis.Prism || {};
globalThis.Prism.manual = true;
import Prism from 'prism';
import { useEffect, useRef } from 'preact';

const SKIP_DIFFING = typeof document !== 'undefined' ? {} : undefined;

function Prism({ code, language }) {
  const ref = useRef();
  useEffect(() => {
    ref.current.textContent = code;
    Prism.highlightElement(ref.current);
  }, [code, language]);
  return (
    <pre className={'language-'+language'}>
      <code ref={ref} className={'language-'+language'} dangerouslySetInnerHTML={SKIP_DIFFING}>
        {code}
      </code>
    </pre>
  );
}

// usage:
<Prism code={code} language="js" />

There's also a much nicer way to use Prism with (p)react, if you're interested - they provide a string-to-string API that avoids the DOM stuff entirely. Let me know if you want an example.

jakedee commented 2 years ago

Hi Jason, the string-to-string API sounds interesting, an example would be great!

In the meantime, I'll try implementing the changes you've suggested. Thanks!

jpuntd commented 2 years ago

Use the low level highlight function from the Prism API to convert your code to highlighted code:

const highlightedCode = Prism.highlight(
    code,
    Prism.languages.javascript,
    "javascript"
  );

Then display the highlighted code inside your component. The variable highlightedCode now contains the necessary html tags to do the highlighting, and for security reasons Preact automatically escapes html tags inside variables. So you'll need to explicitly opt out of this security mechanism. Use dangerouslySetInnerHTML to have the content of the variable highlightedCode not escaped. Do so only if the content comes from a trusted source!

return (
    <>
      <pre className="language-js">
        <code
          className="language-js"
          dangerouslySetInnerHTML={{ __html: highlightedCode }}
        ></code>
      </pre>
      <button onClick={handleClick}>Trigger re-render</button>
      <div>Re-renders: {rerenders}</div>
    </>
  );

Since the code is already hightlighted, you can now get rid of the useEffect call. Adapted version of your original code, using Prism's string-to-string API can be found here: https://codesandbox.io/s/preact-prism-forked-h8lm1?file=/pages/index.js