0

I want to use Utterances for my blog. It should only load when someone scrolled to the bottom of the post so I use react-intersection-observer for that. I have made the following hook.

useUtterances.ts

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

import { siteMetadata } from '@/_data/index'

export const useUtterances = (commentNodeId: string) => {
    const config = siteMetadata.comment.utterancesConfig
    // username/repo format
    const REPO_NAME = config.repo as string

    const { theme, resolvedTheme } = useTheme()
    const utterancesTheme =
        theme === 'light' || resolvedTheme === 'light' ? config.theme : config.darkTheme

    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)
        }
    }, [REPO_NAME, commentNodeId, utterancesTheme])
}

Utterances.tsx

import React from 'react'
import { useInView } from 'react-intersection-observer'

import { useUtterances } from '@/hooks/useUtterances'

export const Utterances = () => {
    const COMMENTS_NODE_ID = 'comments'
    const { ref, inView } = useInView({ threshold: 0, triggerOnce: true })

    useUtterances(inView ? COMMENTS_NODE_ID : '')

    return (
        <div ref={ref} className="min-h-[400px]">
            {inView ? <div id={COMMENTS_NODE_ID} /> : null}
        </div>
    )
}

I use next-themes to toggle DarkMode. I also send a request to utterances iframe so it doesn't load script twice but it still loads it twice by unmounting & mounting the component.

DarkMode.tsx

import React from 'react'
import { useTheme } from 'next-themes'
import { MoonIcon, SunIcon } from '@heroicons/react/solid'

import { useHasMounted } from '@/hooks/index'
import { siteMetadata } from '@/_data/index'

export const DarkMode = () => {
    const { resolvedTheme, setTheme } = useTheme()
    const hasMounted = useHasMounted()

    const label = resolvedTheme === 'dark' ? 'Activate light mode' : 'Activate dark mode'

    if (!hasMounted) return null

    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'
                    ? siteMetadata.comment.utterancesConfig.darkTheme
                    : siteMetadata.comment.utterancesConfig.theme
            frame.contentWindow.postMessage({ type: 'set-theme', theme: utterancesTheme }, '*')
        }
    }

    return (
        <>
            <button
                className="focus:outline-none"
                type="button"
                title={label}
                aria-label={label}
                onClick={toggleTheme}
            >
                {resolvedTheme === 'light' ? (
                    <MoonIcon className="w-8 h-8" />
                ) : (
                    <SunIcon className="w-8 h-8" />
                )}
            </button>
        </>
    )
}

How do I make sure it only requests script once? It now calls it everytime I toggle. It mounts and unmounts the component as I see nothing for a while when the script is loading.

GitHub repo -> https://github.com/deadcoder0904/next-utterances-script-loads-twice/tree/master

Stackblitz demo -> https://stackblitz.com/edit/github-6frqvs?file=pages%2Findex.tsx

Open in New Window to see the Dark Mode as Stackblitz currently doesn't support Tailwind Dark Mode. Check the Network Tab to see it sends request everytime even though you can also see the Comments component mounting & unmounting.

How do I only load script once?

5
  • You are doing a cleanup scriptParentNode.removeChild in your effect, so it will mount and unmount when the state changes in the tree above. Commented Jul 28, 2021 at 6:28
  • @PsyGik if I remove it, then it keeps adding more comment boxes below. what should i do in this case to make it work? Commented Jul 28, 2021 at 6:38
  • Off the top of my mind, you should probably add an id using script.setAttribute and only append the script if you don't find that id in the DOM. viz the script was added previously. Commented Jul 28, 2021 at 6:47
  • @PsyGik if I set that in useUtterances.ts, then in Utterances.tsx how can I reference the same id to know it came in the view like your IntersectionObserver blog post. It does makes sense to do that though but not sure how? The old way worked but I had to put the config outside in 1 place using siteMetaData.ts & it stopped working so hoping to find a solution for that. Commented Jul 28, 2021 at 6:53
  • @PsyGik got the answer :) Commented Jul 28, 2021 at 12:36

1 Answer 1

1

Try something like this:

React.useEffect(() => {
    const scriptParentNode = document.getElementById(commentNodeId)
    const utterancesFrame = document.getElementsByClassName('utterances-frame')[0]
    if (!scriptParentNode || utterancesFrame) 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)
}, [REPO_NAME, commentNodeId, utterancesTheme])

So what basically is happen here. It is looks like when we append the utterances script after loading it replaces himself with:

<div class="utterances" style="height: 267px;">
    <iframe class="utterances-frame" title="Comments" scrolling="no" src="https://utteranc.es/..." loading="lazy"></iframe>
</div>

To avoid unnecessary append we check if there are some <iframe class="utterances-frame" already in the page and only if not append the script.

Sign up to request clarification or add additional context in comments.

3 Comments

Nope, it does the same thing.
Ok i dig more into it and now i see the problem. Updated answer and made a fork: stackblitz.com/edit/… Is it correct now?
damn it works now, what did you do? can you explain in the answer so I can upvote & accept it :)

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.