why onload function is being called twice, though component is rendering once? - javascript

I am having a simple react component, the fire is printed once, but injectWindowInterval is called twice event though i am setting flag value, why is it so?
const Header = () => {
let flag = true;
console.log("fire");
function injectWindowInterval() {
if (flag && window.google) {
flag = false;
console.log(window.google);
}
}
const script = document.createElement("script");
script.src = "https://accounts.google.com/gsi/client";
script.onload = injectWindowInterval;
script.async = true;
document.querySelector("body")?.appendChild(script);
return (
<div className="header">
<h3 className="header__title">RE</h3>
</div>
);
};
Update: for some reason, the script is appending twice in body.

In some cases function are called on render when they are defined. One common example is with onClick methods for buttons
<button onClick={onClickFunction}>Click me</button> //fires on render
<button onClick={()=>onClickFunction()}>Click me</button> //no fire on render, works well
So maybe try
const Header = () => {
let flag = true;
console.log("fire");
const script = document.createElement("script");
script.src = "https://accounts.google.com/gsi/client";
script.onload = () => {
if (flag && window.google) {
flag = false;
console.log(window.google);
}
};
script.async = true;
document.querySelector("body")?.appendChild(script);
return (
<div className="header">
<h3 className="header__title">RE</h3>
</div>
);
};

If you are rendering the Component in <React.StrictMode>, the component will render twice for sure in development mode.
I tried rendering above component(Header) and the fire is logged in the console twice so do the script tag appended twice.
Here is the console Output
Note : Also note that React renders component twice only in development mode. Once you build the app the component will be rendered only once.

Related

Adding multiple <script>s to a specific page in Gatsby

I'm trying to add two scripts to a specific page in a gatsby.js app.
The page is /apply, and the second script depends on the first (the first script must be loaded before the second).
Of course this is straightforward on a traditional site as the scripts would be loaded synchronously in order. But in react-helmet, the scripts are loaded asynchronously so my second script errors (its trying to call a function in the first before the first is loaded).
I've taken a hook from https://usehooks.com/useScript/ and am trying to get things working.
If I inspect the page source after load, both scripts are present but I still get errors in the console (as if script2 is trying to run before script1).
myPage.js
const Apply = () => {
const scriptLoaded = useScript("https://myscripturl/script1.js");
if(scriptLoaded !== "ready"){
return <>Not loaded</>
}
return (
<>
<Helmet>
{scriptLoaded === "ready" &&
<script src="https://myscripturl/script2.js"></script>
}
</Helmet>
<!-- The rest of the page -->
</>
)
}
useScript.js
// taken from https://usehooks.com/useScript/
function useScript(src) {
// Keep track of script status ("idle", "loading", "ready", "error")
const [status, setStatus] = useState(src ? "loading" : "idle");
useEffect(
() => {
// Allow falsy src value if waiting on other data needed for
// constructing the script URL passed to this hook.
if (!src) {
setStatus("idle");
return;
}
// Fetch existing script element by src
// It may have been added by another intance of this hook
let script = document.querySelector(`script[src="${src}"]`);
if (!script) {
// Create script
script = document.createElement("script");
script.src = src;
script.async = true;
script.setAttribute("data-status", "loading");
// Add script to document body
document.body.appendChild(script);
// Store status in attribute on script
// This can be read by other instances of this hook
const setAttributeFromEvent = (event) => {
script.setAttribute(
"data-status",
event.type === "load" ? "ready" : "error"
);
};
script.addEventListener("load", setAttributeFromEvent);
script.addEventListener("error", setAttributeFromEvent);
} else {
// Grab existing script status from attribute and set to state.
setStatus(script.getAttribute("data-status"));
}
// Script event handler to update status in state
// Note: Even if the script already exists we still need to add
// event handlers to update the state for *this* hook instance.
const setStateFromEvent = (event) => {
setStatus(event.type === "load" ? "ready" : "error");
};
// Add event listeners
script.addEventListener("load", setStateFromEvent);
script.addEventListener("error", setStateFromEvent);
// Remove event listeners on cleanup
return () => {
if (script) {
script.removeEventListener("load", setStateFromEvent);
script.removeEventListener("error", setStateFromEvent);
}
};
},
[src] // Only re-run effect if script src changes
);
If I place both scripts in the index.html in /public, the code works without issue. But of course it runs on every route in the app which is no good. Is what I'm trying to do even possible?
Thanks for any help.
You can use Gatsby's Script API: https://www.gatsbyjs.com/docs/reference/built-in-components/gatsby-script/
It also has a section about loading script dependently: https://www.gatsbyjs.com/docs/reference/built-in-components/gatsby-script/#loading-scripts-dependently
So your code could be:
import React, { useState } from "react"
import { Script } from "gatsby"
function Apply() {
const [loaded, setLoaded] = useState(false)
return (
<>
<Script src="https://myscripturl/script1.js" onLoad={() => setLoaded(true)} />
{loaded && <Script src="https://myscripturl/script2.js" />}
</>
)
}
export default Apply

Injected event listeners in Vue JS project don't always fire on page change

I'm working with a Nuxt JS / Vue JS app with dynamic pages. My pages fetch data from a remote api which includes HTML and JS that needs to be executed.
In my context, I have a bunch of accordions, that when tapped should open the contents, so in the then block of my axios request I'm creating a script tag on the page with the contents:
const string = "some JS from api"
const injectTo = document.querySelector('[data-beam-injectes-interactivity]')
const script = document.createElement('script')
script.innerHTML = string
injectTo.appendChild(script)
The string of JS injected is:
function initDeviceGroupCollapses () {
const buttons = document.querySelectorAll('[data-toggle="collapse"]')
if (buttons) {
for (const [index, button] of buttons.entries()) {
if (button) {
button.addEventListener('click', (event) => {
event.stopPropagation()
const target = event.target.dataset.target
if (!target) {
return
}
const collapse = document.querySelector(target)
collapse.style.display = collapse.style.display == 'none' ? 'block' : 'none'
}, false)
}
}
}
}
The issue I'm having though is that if the JS is injected into a page where the HTML doesn't initially exist, the event listeners no longer fire on data-toggle="collapse".
How can I always make sure that the injected JS works?

Browser back action does not work in Firefox when PayPal Plus iframe is rendered in page of react app

I integrated the PayPal Plus iframe into my react app by appending the PayPal library js-script to the document in componentDidMount, updating the component state when the script is loaded and then calling the library method to initialize the PayPal Plus object. This renders the iframe into the placeholder div with the according id. I consulted the documentation (https://developer.paypal.com/docs/paypal-plus/germany/integrate/integrate-payment-wall/#) and did the aforementioned adaptations to use it in combination with my react app.
This works so far and looks like this in the minimalized example: page with rendered ppp iframe
In Firefox though, it isn't possible to navigate back by the browser's back button, once the iframe has been initialized.
Instead, the whole component is loaded inside of the iframe. Like this: page after clicking back button
This doesn't happen in Chrome or Internet Explorer.
Firefox version: 81.0 (64-Bit)
If I manually delete the iframe element from the DOM, the back button works normally again.
I already tried using custom event handlers for the "popstate" and "beforeunload" events, to try to work around this but to no avail. Those events seemingly don't occur in the parent window in Firefox.
Steps to reproduce:
Create react app (i used V16.13.1 in the example) (https://reactjs.org/docs/create-a-new-react-app.html#create-react-app).
Insert provided classes into the project and use PppComponent in App.js.
Acquire valid PayPal sandbox approvalUrl and set it on ppp config object.
Run yarn or npm start.
Open the app in Mozilla Firefox and click the back button of the browser.
This is basically the related code of the component:
import React from 'react';
import utils from './utils';
class PppComponent extends React.Component {
constructor() {
super();
this.state = {
scriptLoaded: false
}
}
componentDidMount() {
utils.appendScript(
'https://www.paypalobjects.com/webstatic/ppplus/ppplus.min.js',
() => this.setState({scriptLoaded: true})
);
}
componentWillUpdate(nextProps, nextState) {
if (nextState.scriptLoaded) {
this.initPaypalPlusIframe();
}
}
componentWillUnmount() {
utils.removeScript('https://www.paypalobjects.com/webstatic/ppplus/ppplus.min.js');
}
initPaypalPlusIframe() {
const pppConfig = {
approvalUrl: '{validApprovalUrl}',
placeholder: 'ppplus',
mode: 'sandbox',
language: 'de_DE',
country: 'DE',
useraction: 'commit',
};
window.PAYPAL.apps.PPP(pppConfig);
}
render() {
return (
<div>
<h1>Some Content Here</h1>
<div id="ppplus"/>
</div>
);
}
}
export default PppComponent;
And these are the util functions, used to append and remove the script:
class Utils {
appendScript(scriptToAppend, onLoad) {
const allSuspects = document.getElementsByTagName('script');
let doAppend = true;
if (allSuspects && allSuspects.length > 0) {
for (let i = allSuspects.length - 1; i >= 0; i--) {
if (allSuspects[i] && allSuspects[i].getAttribute('src') !== null
&& allSuspects[i].getAttribute('src').indexOf(`${scriptToAppend}`) !== -1) {
doAppend = false;
}
}
}
if (doAppend) {
const script = document.createElement('script');
script.src = scriptToAppend;
script.async = false;
script.onload = () => onLoad();
document.body.appendChild(script);
}
}
removeScript(scriptToRemove) {
const allSuspects = document.getElementsByTagName('script');
for (let i = allSuspects.length - 1; i >= 0; i--) {
if (allSuspects[i] && allSuspects[i].getAttribute('src') !== null
&& allSuspects[i].getAttribute('src').indexOf(`${scriptToRemove}`) !== -1) {
allSuspects[i].parentNode.removeChild(allSuspects[i]);
}
}
}
}
export default new Utils();
Has anybody an idea why this happens or maybe experienced something similar and knows how to fix/workaround this behavior?
I'd be glad about any help.
Thank You in advance.

onload wait script to load finish in react component?

I have this block in react to inject script, I put it within useEffect, but the problem is the script is big, the onload function trigger when the script is appended, it doesn't check whether the script is finished loaded, so what I did is I have to use setTimeout which is ugly, any clue?
useEffect(() => {
function loadScript() {
const script = window.document.createElement('script');
script.src = `//my_external_script.js`;
script.async = true;
script.defer = true;
script.onload = () => {
setTimeout(() => console.log('do something all script is
finished loaded') ,2000)
}
}
}, [])

How to open a third-party widget by clicking a button in Gatsby.js?

I'm creating website in Gatsby.js, and I want to open widget from Booksy, by onClick method. They gave me a script:
<script src="https://booksy.net/widget/code.js?id=9178&country=pl&lang=pl"></script>
How can I do that? Function document.write() doesn't work, its blocked by browser. I also tried conditional rendered iframe it works, but badly.
const StyledIframe = styled.iframe`
width: 100vw;
height: 100vh;
position: absolute;
z-index: 99999;
`;
const BooksyWidget = ({ isOpen }) => {
const src = "https://booksy.com/widget/index.html?id=9178&lang=pl&country=pl&mode=dialog&theme=default"
const Content = () => {
return isOpen ? <StyledIframe src={ src }/> : null
}
return (
<>
{Content()}
</>
)
};
I want to get result when clicking 'Make an Appointment' button like on this website:
http://gentlemanbarber.pl/
I guess my Iframe method can't provide this. So how can i do it?
Problem above is solved by this approach:
const LoadScript = () => {
useEffect(() => {
const script = document.createElement('script');
script.type = 'text/javascript';
script.src = 'https://booksy.net/widget/code.js?id=9178&country=pl&lang=pl';
script.async = true;
document.getElementById('script').appendChild(script);
}, []);
return <div id='script'></div>
}
Script renders a button in div. But when I go to 'http://localhost:8000/page-2/' and back to 'http://localhost:8000/', button disappears and console throws error:
"Uncaught TypeError: Cannot read property 'src' of undefined". The same thing happens when I use class-based component and componentDidMount() method instead of hook. There is link to repository with code:
https://github.com/kamilduleba/script
How can I use this script to render button that doesn't disappear?
const BooksyWidget = React.memo(() => {
const booksyWidgetRef = React.useRef();
React.useEffect(() => {
if (!booksyWidgetRef.current) return
const script = document.createElement('script');
script.type = 'text/javascript';
script.src = 'https://booksy.com/widget/code.js?id=123&country=pl&lang=pl';
script.async = true;
booksyWidgetRef.current.appendChild(script);
// to prevent the following error after unmount and mount again:
// Uncaught Error: [Booksy][widget][error] cannot locate current script
return () => {
if (!window.booksy) return;
delete window.booksy;
};
}, []);
return <div ref={booksyWidgetRef}></div>
})
Optional:
To prevent content layout shift div should have min-height: 94px

Categories