utterance / utterances

:crystal_ball: A lightweight comments widget built on GitHub issues
https://utteranc.es
MIT License
8.68k stars 566 forks source link

example for react use #161

Closed jmau111 closed 5 years ago

jmau111 commented 5 years ago

hi @jdanyow,

Your work is awesome ! Just want to contribute this example made with JSX (react) cause it took me some time especially with the custom attributes for script tag.

import React, {Component} from "react";

export default class Comments extends Component {

  componentDidMount () {
      let script = document.createElement("script");
      let anchor = document.getElementById("inject-comments-for-uterances");
      script.setAttribute("src", "https://utteranc.es/client.js");
      script.setAttribute("crossorigin","anonymous");
      script.setAttribute("async", true);
      script.setAttribute("repo", "[ENTER REPO HERE]");
      script.setAttribute("issue-term", "pathname");
      script.setAttribute( "theme", "github-light");
      anchor.appendChild(script);
  }

  render() {
    return (
        <div id="inject-comments-for-uterances"></div>
    );
  }
}

Might be useful. And of course I'm gonna use your tool ! (I use gatsby js)

jdanyow commented 5 years ago

Nice, thanks!

Also checkout https://github.com/b6pzeusbc54tvhw5jgpyw8pwz2x6gs/react-utterances (I'm not a react expert, not sure if it's a good example)

jmau111 commented 5 years ago

Also checkout https://github.com/b6pzeusbc54tvhw5jgpyw8pwz2x6gs/react-utterances (I'm not a react expert, not sure if it's a good example)

neither do I, but I guess the important thing here is componentDidMount in the lifescycle and to use setAttribute(). Thanks for the link, seems better with the whole list of parameters.

osdiab commented 4 years ago

For a more concise approach to it, this is how I just did it on my repo:

export const UtterancesComments: React.FC = () => (
  <section
    ref={elem => {
      if (!elem) {
        return;
      }
      const scriptElem = document.createElement("script");
      scriptElem.src = "https://utteranc.es/client.js";
      scriptElem.async = true;
      scriptElem.crossOrigin = "anonymous";
      scriptElem.setAttribute("repo", "osdiab/osdiab.github.io");
      scriptElem.setAttribute("issue-term", "pathname");
      scriptElem.setAttribute("label", "blog-comment");
      scriptElem.setAttribute("theme", "github-light");
      elem.appendChild(scriptElem);
    }}
  />
);

This should be safe - you get the DOM elem, this gets invoked once on mount, and once with null on unmount.

rscharfer commented 3 years ago

I love this, but I am wondering why it works now. It seems on subsequent renders, React would see that it is supposed to render an empty div and then delete the utterances iframe that was rendered in there by the script...

vincentntang commented 3 years ago

I use GatsbyJs (react)

I've run into some troubles installing utteranc.es. What I did wrong was I had included https://github.com/vincentntang/ in the reponame, but this should be omitted when specifying the repo attribute

Here's the Comments component I use for my personal blog site:

import React, {Component} from "react";
export default class Comments extends Component {

  constructor(props){ 
    super(props);
    this.commentBox = React.createRef();
  }
  componentDidMount () {
      let scriptEl = document.createElement("script");
      scriptEl.setAttribute("src", "https://utteranc.es/client.js");
      scriptEl.setAttribute("crossorigin","anonymous");
      scriptEl.setAttribute("async", true);
      scriptEl.setAttribute("repo", "vincentntang/vincentntang.com-comments");
      scriptEl.setAttribute("issue-term", "pathname");
      scriptEl.setAttribute( "theme", "github-light");
      this.commentBox.current.appendChild(scriptEl);
  }

  render() {
    return (
        <div ref={this.commentBox} className="comment-box"></div>
    );
  }
}

I have my comments at

and my blog at

live site is at :

vincentntang commented 3 years ago

I wrote a guide here too

https://www.vincentntang.com/installing-gatsbyjs-blog-comments/

luizwhite commented 3 years ago

For a more concise approach to it, this is how I just did it on my repo:

export const UtterancesComments: React.FC = () => (
  <section
    ref={elem => {
      if (!elem) {
        return;
      }
      const scriptElem = document.createElement("script");
      scriptElem.src = "https://utteranc.es/client.js";
      scriptElem.async = true;
      scriptElem.crossOrigin = "anonymous";
      scriptElem.setAttribute("repo", "osdiab/osdiab.github.io");
      scriptElem.setAttribute("issue-term", "pathname");
      scriptElem.setAttribute("label", "blog-comment");
      scriptElem.setAttribute("theme", "github-light");
      elem.appendChild(scriptElem);
    }}
  />
);

This should be safe - you get the DOM elem, this gets invoked once on mount, and once with null on unmount.

Nice code, I used this but I had to check if the element already has child nodes, because this code creating a utterances child div every time i navigated to that page Didn't know though if I was doing something wrong, but i guess not.

export const UtterancesComments: React.FC = () => (
  <section
    ref={(elem) => {
      if (!elem || elem.childNodes.length) {
        return;
      }
    ...
    }}
  />
);
PsyGik commented 3 years ago

Using a hook:

import { useEffect, useState } from 'react';

export const useUtterances = (commentNodeId) => {
    useEffect(() => {
                 const scriptParentNode = document.getElementById(commentNodeId);
        if (!scriptParentNode) return;
        // docs - https://utteranc.es/
        const script = document.createElement('script');
        script.src = 'https://utteranc.es/client.js';
        script.async = true;
        script.setAttribute('repo', ${YOUR_REPO_NAME});
        script.setAttribute('issue-term', 'pathname');
        script.setAttribute('label', 'comment :speech_balloon:');
        script.setAttribute('theme', 'photon-dark');
        script.setAttribute('crossorigin', 'anonymous');

        scriptParentNode.appendChild(script);

        return () => {
            // cleanup - remove the older script with previous theme
            scriptParentNode.removeChild(scriptParentNode.firstChild);
        };
    }, [commentNodeId]);
};

Usage:

import React from 'react';
import { useUtterances } from '../../hooks/useUtterances';

const commentNodeId = 'comments';

const Comments = () => {
    useUtterances(commentNodeId);
    return <div id={commentNodeId} />;
};

export default Comments;

I've written an article which also shows how to lazy load, so that the comments show up when user reaches to the end of the page, (or wherever your comments are on the page), using Intersection Observer API.

deadcoder0904 commented 2 years ago

Typescript using @PsyGik's solution

import React from 'react'

// username/repo format
const REPO_NAME = '<username>/<repo>'

export const useUtterances = (commentNodeId: string) => {
    React.useEffect(() => {
        const scriptParentNode = document.getElementById(commentNodeId)
        if (!scriptParentNode) return

        // docs - https://utteranc.es/
        const script = document.createElement('script')
        script.src = 'https://utteranc.es/client.js'
        script.async = true
        script.setAttribute('repo', REPO_NAME)
        script.setAttribute('issue-term', 'pathname')
        script.setAttribute('label', 'comment :speech_balloon:')
        script.setAttribute('theme', 'photon-dark')
        script.setAttribute('crossorigin', 'anonymous')

        scriptParentNode.appendChild(script)

        return () => {
            // cleanup - remove the older script with previous theme
            scriptParentNode.removeChild(scriptParentNode.firstChild as Node)
        }
    }, [commentNodeId])
}

How do I react to theme change? @PsyGik you mention unmounting & remounting the component, how to do that? Isn't there a better way?

PsyGik commented 2 years ago

@deadcoder0904 you can provide another argument to your useEffect which has theme.

import React from 'react'

// username/repo format
const REPO_NAME = '<username>/<repo>'

export const useUtterances = (commentNodeId: string, theme: string) => {
    React.useEffect(() => {
        const scriptParentNode = document.getElementById(commentNodeId)
        if (!scriptParentNode) return

        // docs - https://utteranc.es/
        const script = document.createElement('script')
        script.src = 'https://utteranc.es/client.js'
        script.async = true
        script.setAttribute('repo', REPO_NAME)
        script.setAttribute('issue-term', 'pathname')
        script.setAttribute('label', 'comment :speech_balloon:')
        script.setAttribute('theme', theme)
        script.setAttribute('crossorigin', 'anonymous')

        scriptParentNode.appendChild(script)

        return () => {
            // cleanup - remove the older script with previous theme
            scriptParentNode.removeChild(scriptParentNode.firstChild as Node)
        }
    }, [commentNodeId, theme])
}

Unfortunately because utterance uses a different css file for each theme, the only clean way to update is to unmount/remount, which the above code will do.

image

However, since us developers don't give up easy ;) , here's one using postMessage to the utterance iFrame.

document.getElementsByClassName("utterances-frame")[0].contentWindow.postMessage({ type: "set-theme", theme:  theme },  "*")

Do note that there is a bit of an ugly color interchange thingy happening when the css is unloaded and new file is loaded

deadcoder0904 commented 2 years ago

@PsyGik Awesome. I tried both & I like the iframe version better. It works snappy. I think the only issue it has is it requests Utterances Theme CSS file on every toggle.

However, I'm getting a TS error on React.useEffect dependencies:

React Hook React.useEffect has a missing dependency: 'utterancesTheme'. Either include it or remove the dependency array.eslintreact-hooks/exhaustive-deps

My code is:

DarkMode.tsx

const toggleTheme = () => {
        const newTheme = resolvedTheme === 'light' ? 'dark' : 'light'
        setTheme(newTheme)

        // for utterances
        const frame = document.getElementsByClassName('utterances-frame')[0] as HTMLIFrameElement
        if (frame.contentWindow) {
            const utterancesTheme = resolvedTheme === 'light' ? 'photon-dark' : 'github-light'
            frame.contentWindow.postMessage({ type: 'set-theme', theme: utterancesTheme }, '*')
        }
    }

useUtterances.ts

import React from 'react'
import { useTheme } from 'next-themes'

// username/repo format
const REPO_NAME = '<username>/<repo>'

export const useUtterances = (commentNodeId: string) => {
    const { theme } = useTheme()
    const utterancesTheme = theme === 'light' ? 'github-light' : 'photon-dark'

    React.useEffect(() => {
        const scriptParentNode = document.getElementById(commentNodeId)
        if (!scriptParentNode) return

        // docs - https://utteranc.es/
        const script = document.createElement('script')
        script.src = 'https://utteranc.es/client.js'
        script.async = true
        script.setAttribute('repo', REPO_NAME)
        script.setAttribute('issue-term', 'pathname')
        script.setAttribute('label', 'comment :speech_balloon:')
        script.setAttribute('theme', utterancesTheme)
        script.setAttribute('crossorigin', 'anonymous')

        scriptParentNode.appendChild(script)

        return () => {
            // cleanup - remove the older script with previous theme
            scriptParentNode.removeChild(scriptParentNode.firstChild as Node)
        }
    }, [commentNodeId])
}

I tried emptying the array as it says in the error but it re-renders the component. Same thing with adding utterancesTheme to the dependency.

Any ideas? This is a warning of ESlint. It works though so thank you :)

teilorbarcelos commented 2 years ago

Definitive solution here, without removeChild and appendChild, only with a replaceChildren, see the component in next:

export function Comments() {

  return (
    <section
      style={{ width: '100%' }}
      ref={
        element => {
          if (!element) {
            return
          }

          const scriptElement = document.createElement('script')
          scriptElement.setAttribute('src', 'https://utteranc.es/client.js')
          scriptElement.setAttribute('repo', 'youruser/repo-name')
          scriptElement.setAttribute('issue-term', 'pathname')
          scriptElement.setAttribute('theme', 'photon-dark')
          scriptElement.setAttribute('crossorigin', 'anonymous')
          scriptElement.setAttribute('async', 'true')
          element.replaceChildren(scriptElement)
        }
      }
    />
  )
}

enjoy :)