useEffect rendering form from script twice on load? - reactjs

I am using a hubspot form in my next.js site - I want to render the form on page load as you can see in the code.
The undesired effect is that the form renders twice, I want to build in a system so it only renders on page load and thats it. How do I achieve this?
I tried to follow this to solve it but did not work https://www.youtube.com/watch?v=KzH6YxW0zW4.
Code below
useEffect(() => {
let isCancelled = false;
const script = document.createElement('script');
script.src = "//js-eu1.hsforms.net/forms/v2.js";
document.body.appendChild(script)
if (!isCancelled) {
script.addEventListener('load', () => {
if (window.hbspt){
hubspotForm();
}
})
}
return () => {
isCancelled = true
};
}, [])

Removing strict mode is something that you should not prefer. I believe if you do this:
useEffect(() => {
const script = document.createElement('script');
script.src = "//js-eu1.hsforms.net/forms/v2.js";
document.body.appendChild(script)
const listener=() => {
if (window.hbspt){
hubspotForm();
}
}
script.addEventListener('load', listener)
return () => {
script.removeEventListener('load',listener)
};
}, [])
it should work fine. I prepared a detailed explanation of the topic (why useEffect is running twice and how to handle it.) here if you might want to have a look. Removing Strict mode is an easy out way but you should not choose it.

Remove strict mode from your index.js, that should do the trick.

check the version of react and react-dom in the package.json file
if you're using the lastest version of react v18. Because of the react strict mode and concurrrent rendering feature it executes the useEffect() hook to run twice even the if you set an empty dependency array.
you can disable react strictmode from next.config.js file
it should help to fix your problem

You can do something like this:
useEffect(() => {
// check whether the script is present already
if (document.getElementById('your-script-id')) return;
let isCancelled = false;
const script = document.createElement('script');
// add an id here
script.id = "your-script-id";
// the rest of your code
}
Also, check that you're not re-mounting your component, the fact that your script is triggered twice suggests that somewhere outside your component is re-mounted on load, which might cause performance issues.

Related

Saving information to localStorage, but localStorage is not saving it on refresh (React.js)

I'm creating this project, but I am having trouble learning why my code is not being saved to localStorage upon refresh. When I add stuff to the JSON file it saves properly, but when I reload all of the data from the JSON file disappears.
I have tried many different formats for loading in the information, such as adding other formats in the format string but nothing has seemed to work.
const [notes, setNotes] = useState([
]);
useEffect(() => {
const savedNotes = localStorage.getItem('react-notes-app-data');
if (savedNotes != null) { setNotes(JSON.parse(savedNotes)); }
}, []);
useEffect(() => {
localStorage.setItem(
'react-notes-app-data',
JSON.stringify(notes)
);
setDisplayNotes(notes);
}, [notes]);
Try initializing your useState like this
let initialNotes;
try {
initialNotes = JSON.parse(localStorage.getItem('react-notes-app-data'));
catch(err) {
initialNotes = [];
}
const [notes, setNotes] = useState(initialNotes);
And get rid of your entire first useEffect call. Basically there’s a logical error where you’re always setting localStorage to an empty array on load
Notes must be a new object to trigger the useEffect as it doesn't do deep checking.
I have created an example and tested it. It works fine. However, #ceckenrode has suggested better way.
https://codesandbox.io/s/silly-water-jiwi6x?file=/src/App.js

Doing a router push in useEffect causes the effect to run in an infinite loop

I am using NextJs, and I have a component that uses useDebounceHook.
The component looks like this.
import { useRouter } from 'next/router';
function SearchComponent() {
const router = useRouter();
const [searchResults, setSearchResults] = useState([]);
const [searchTerm, setSearchTerm] = useState<string>('');
const debouncedSearchTerm: string = useDebounce<string>(searchTerm, 250);
const handleSearchChange = (event) => {
setSearchTerm(event.target.value);
};
useEffect(() => {
const newPath = `/search?q=${debouncedSearchTerm}`;
router.push(newPath, undefined, { shallow: true });
}, [debouncedSearchTerm]);
// eslint complains above that router is missing and if we add, the function runs infintely.
useEffect(() => {
fetchQueryResults()
.then((data) => {
setSearchResults(data)l
})
}, [router.query.q]);
return (
<InputField
placeholder="Search"
value={searchTerm}
onChange={handleSearchChange}
/>
{renderSearchResults()}
)
}
// useDebounceHook reference: https://usehooks.com/useDebounce/
The component listens to the search change event and immediately updates the value as it needs to be visible on the screen textbox. However, it debounces the value for fetching search results.
And we want to do the fetch from the URL route as we want it to be bookmarkable. Hence we push the query param to the URL once the debounce value changes rather than directly fetching.
Here the problem is Eslint complains that we are missing router from the dependency array. If we add it, it goes into an infinite loop.
How to solve this issue? Is it ok if we skip adding the router to the dependency array?
One option is to only do the router push if it changes q, i.e.
useEffect(() => {
const newPath = `/search?q=${debouncedSearchTerm}`;
if (debouncedSearchTerm !== router.query.q) {
router.push(newPath, undefined, { shallow: true });
}
}, [router, debouncedSearchTerm]);
However I think it's also fine to omit router as a dependency here (and suppress the lint), since you don't need to re-navigate if the router changes (generally when dependencies change you want to rerun the effect, but here router doesn't seem necessary).
One thing to consider is if the page changes from some other reason (ex. they click a link on the page which takes them to "search?q=blah"), what should happen? Should the effect run taking them back to search?q=searchTerm? This is essentially the case being handled here.
I did this temporary fix until the larger issue is resolved from nextjs:
useEffect(() => {
const newPath = `/search?q=${debouncedSearchTerm}`;
router.push(newPath, undefined, { shallow: true });
// This is a hack to prevent the redirect infinite loop problem
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [undefined]);

Refresh page after and call a function automatically in react

I want to create a function that refreshes the page and then call element and click:
const handlePDF = () => {
window.location.reload(false);
pdfRef.current.click()
};
I had thought about setTimeout but it doesn't work like that either.
I imagine that I have to save that order in memory once the page is refreshed but I don't know how to do it.
I appreciate your help guys.
You can use sessionStorage on window load event to solve that.
1. Listen window on load
window.onload = () => {
let reloading = sessionStorage.getItem("reloading");
if (reloading) {
sessionStorage.removeItem("reloading");
pdfRef.current.click();
}
}
2. Save a session on handlePDF call
const handlePDF = () => {
sessionStorage.setItem("reloading", "true");
window.location.reload(false);
};
I had a similar requirement to your's, before.
The solution that I settled with is to use sessionStorage to flag states, so that some flag is available to you, upon page refresh.
Then, you'd look for that flag in a useEffect callback.
But first, let's create a constant, so that we don't aimlessly repeat ourselves.
const handlePdfFlag = 'handle_pdf';
Then, create the flag in localStorage, then reload the page.
const handlePDF = () => {
sessionStorage.setItem(handlePdfFlag, 'true');
window.location.reload(false);
};
Then, in some useEffect, you'd pick up the flag, ensuring that you delete the flag afterwards.
useEffect(() => {
if (sessionStorage.getItem(handlePdfFlag) === 'true' && pdfRef.current) {
sessionStorage.removeItem(handlePdfFlag);
pdfRef.current.click();
}
});
If you want a function that does some code based on something changing then maybe the useEffect hook will work for you.

Using addDecorator in Storybook's preview.ts throws Rendered more hooks than during the previous render

Reading through the resource loading documentation from Chromatic, the Solution B: Check fonts have loaded in a decorator section.
Mainly would like to load our fonts before rendering the stories. The solution suggest to use addDecorator where with a simple FC we can preload the fonts and once they are loaded it can render the stories with story().
See the suggested decorator for preview.ts:
import isChromatic from "chromatic/isChromatic";
if (isChromatic() && document.fonts) {
addDecorator((story) => {
const [isLoadingFonts, setIsLoadingFonts] = useState(true);
useEffect(() => {
Promise.all([document.fonts.load("1em Roboto")]).then(() =>
setIsLoadingFonts(false)
);
}, []);
return isLoadingFonts ? null : story();
});
}
For some reason this throws the usual error when violating the Rules of Hooks:
Rendered more hooks than during the previous render
What I've tried so far:
Mainly I tried to remove the useEffect which renders the stories:
if (isChromatic() && document.fonts) {
addDecorator((story) => {
const [isLoadingFonts, setIsLoadingFonts] = useState(true);
return story();
});
}
Also the error disappeared but the fonts are causing inconsistent changes our screenshot tests as before.
Question:
I don't really see any issues which would violate the Rules of Hooks in the added FC for the addDecorator.
Is there anything what can make this error disappear? I'm open to any suggestions. Maybe I missed something here, thank you!
Mainly what solved the issue on our end is removing from main.ts one of the addons called #storybook/addon-knobs.
Also renamed from preview.ts to preview.tsx and used the decorator a bit differently as the following:
export const decorators = [
Story => {
const [isLoadingFonts, setIsLoadingFonts] = useState(true)
useEffect(() => {
const call = async () => {
await document.fonts.load('1em Roboto')
setIsLoadingFonts(false)
}
call()
}, [])
return isLoadingFonts ? <>Fonts are loading...</> : <Story />
},
]
We dropped using the addDecorator and used as the exported const decorators as above.

Embed Pinterest widget in React

I wrote the following component to embed a Pinterest widget in my React app:
import React, { Component } from 'react'
class PinterestWidget extends Component {
componentWillMount() {
const script = document.createElement('script')
script.async = true
script.type = 'text/javascript'
script['data-pin-build'] = 'doBuild'
script.src = '//assets.pinterest.com/js/pinit.js'
document.body.appendChild(script)
}
render() {
const { url } = this.props
return (
<a data-pin-do="embedPin" data-pin-build="doBuild" href={url}>
{url}
</a>
)
}
}
export default PinterestWidget
It works fine when the component mount the first time.
But when I navigate to another route and go back to this page, it doesn't render the widget anymore.
I tried to place the script in componentDidUpdate, but no success.
PS: I searched for a component but the only one I found is react-pinterest which is not maintained for 2 years — and doesn't work in my app.
Can anybody help me?
I found myself in the exact same position today. After wrestling with forks of that same react-pinterest npm package I found a solution in this issue https://github.com/pinterest/widgets/issues/13.
It looks like you already got close by adding the data-pin-build="doBuild" data attribute but you aren't calling doBuild() when the user returns to the component after hitting a different route. Adding the data attribute instructs pinit.js to add doBuild() to the global scope but doesn't automatically call the function when you remount the component.
Try replacing your componentWillMount function with something like this:
componentDidMount() {
if (!window.doBuild) {
this.preloadWidgetScript();
} else {
window.doBuild();
}
}
preloadWidgetScript = () => {
const script = document.createElement('script');
script.async = true;
script.dataset.pinBuild = 'doBuild';
script.src = '//assets.pinterest.com/js/pinit.js';
document.body.appendChild(script);
}
One other thing to note is the change from componentWillMount to componentDidMount, this is necessary to ensure the embed link is rendered before running doBuild().
In my case, using the answer of #n33kos it works with useEffect and so far every time (without checking whether it has appended the script or not). Here the <a> link to pinterest exist in a markdown file that is injected as html
useEffect(() => {
const script = document.createElement('script')
script.async = true
script.type = 'text/javascript'
script.dataset.pinBuild = 'doBuild'
script.src = '//assets.pinterest.com/js/pinit.js'
document.body.appendChild(script)
if (window.doBuild) window.doBuild()
}, []) // only run once

Resources