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?
scriptParentNode.removeChildin your effect, so it will mount and unmount when the state changes in the tree above.idusingscript.setAttributeand only append the script if you don't find thatidin the DOM. viz the script was added previously.useUtterances.ts, then inUtterances.tsxhow can I reference the sameidto know it came in the view like yourIntersectionObserverblog 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 usingsiteMetaData.ts& it stopped working so hoping to find a solution for that.