ReactJS - Adding (Mollie) script to React page - javascript

I'm trying to import a script called Mollie (used for payments), but I'm not sure how to do it in React.
Normally in Javascript you would do something like this:
<html>
<head>
<title>My Checkout</title>
</head>
<body>
<script src="https://js.mollie.com/v1/mollie.js"></script>
</body>
</html>
I've tried this (according to other Stackoverflow posts)
useEffect(() => {
const script = document.createElement("script");
script.src = "https://js.mollie.com/v1/mollie.js";
script.addEventListener("load", () => setScriptLoaded(true));
document.body.appendChild(script);
}, []);
const mollie = Mollie(); // Mollie is not defined
But then Mollie is undefined. Can anyone point in the right direction on how to import Mollie in React?
I'm following this guide (but it's for standard Javascript)

You can easily install this package from npmjs.com where you can find necessary documentations and examples to get started. Installation:
npm i #mollie/api-client

the point here is that the effect is being invoked after the react component mounted and rendered to the user.
The next line where you are trying to call Mollie in fact running earlier when component is being constructed but not rendered yet.
There are multiple options what you can do about it:
Import script in the index.html file as you do for standard non-React solution. There should be a "public" folder containing this file in case of create-react-app usage or other place in case of custom project setup. The HTML should exist in any form even in case it's being generated on the server side dynamically. A mollie instance can later be created in the component or globally.
Use multiple effects in the React component: one to load Mollie and another one to use it when loaded:
// MollieExample.jsx
const MollieExample = () => {
const [mollie, setMollie] = useState();
useEffect(() => {
const script = document.createElement('script');
script.src = 'https://js.mollie.com/v1/mollie.js';
script.addEventListener("load", () => {
setMollie(window.Mollie(/* Mollie arguments */);
});
document.body.appendChild(script);
}, []);
useEffect(() => {
if (mollie) {
console.log('Mollie exists here');
console.log(typeof mollie);
}
} , [mollie]);
return <p>typeof mollie: {typeof mollie}</p>;
};
Use dynamic script loading in case it is required with globally shared Mollie instance via custom hook:
// useMollie.js
let molliePromise;
const useMollie = (effect, deps) => {
const [mollie, setMollie] = useState();
const mollieCb = useCallback((mollie) => effect(mollie), deps);
useEffect(() => {
if (!molliePromise) {
molliePromise = new Promise((resolve) => {
const script = document.createElement("script");
script.src = "https://js.mollie.com/v1/mollie.js";
script.addEventListener("load", () => {
resolve(window.Mollie(/* Mollie arguments */);
});
document.body.appendChild(script);
});
}
molliePromise.then((mollie) => setMollie(mollie));
}, []);
useEffect(() => {
if (mollie) {
mollieCb(mollie);
}
}, [mollie, mollieCb]);
};
// MollieConsumer.jsx
const MollieConsumer = () => {
useMollie((mollie) => {
console.log('Mollie exists here');
console.log(typeof mollie);
}, [/* useMollie effect callback dependencies array */]);
return (
<p>Mollie consumer</p>
);
};
// App.jsx
function App() {
/* Both consumers use the same instance of Mollie */
return (
<div>
<MollieConsumer/>
<MollieConsumer/>
</div>
);
}
I assume you will end up with using some middle option. For instance, with importing script in the index.html (or any other sort of the HTML page you have containing the React application host element) and global hook.

Related

ReactJS: Data not binding from continuous response

While integrating the aws ivs streaming channel with quiz related metadata, at that time getting the console.log of the metadata records and while passing those records into another component it is not handling any how.
A playground that i have created into codesandobx
PlayerComponent
function PlayerComponent(options) {
useEffect(() => {
const script = document.createElement("script");
script.src = "https://player.live-video.net/1.0.0/amazon-ivs-player.min.js";
script.async = true;
document.body.appendChild(script);
script.onload = (IVSPlayer) => {
if (IVSPlayer.isPlayerSupported) {
const player = IVSPlayer.create();
player.attachHTMLVideoElement(document.getElementById("video-player"));
player.load(
"https://fcc3ddae59ed.us-west-2.playback.live-video.net/api/video/v1/us-west-2.893648527354.channel.xhP3ExfcX8ON.m3u8"
);
player.play();
player.addEventListener(
IVSPlayer.PlayerEventType.TEXT_METADATA_CUE,
(cue) => {
const metadataText = cue.text;
setMetaData(metadataText);
console.log("PlayerEvent - METADATA: ", metadataText);
}
);
}
};
return () => {
document.body.removeChild(script);
};
}, []);
return (
<div ref={divEl}>
<video id="video-player" ref={videoEl} autoPlay controls></video>
{metaData !== undefined ? <QuizComponent metadata={metaData} /> : ""}
</div>
);
}
On QuizComponent would like to render those metadata
export default function QuizComponent(props) {
const questionData = props;
return (
<React.Fragment>
<h2>{questionData.metadata.question}</h2>
</React.Fragment>
);
}
But any how not able to render the data into component.
Ref example of what I am going to implement.
https://codepen.io/amazon-ivs/pen/XWmjEKN?editors=0011
I found the problem. Basically you are referring IVSPlayer as if it was the argument of the arrow function you passed to script onload, while the argument instead is an event (the onload event).
Solution: const {IVSPlayer} = window;. Infact docs say
Once amazon-ivs-player.min.js is loaded, it adds an IVSPlayer variable to the global context.
Docs also explain how to setup with NPM which you may be interested in.
I updated my playground here.
I also suggest you to edit the version of the player as the last one is 1.2.0.

How can React useScript hook handles multiple calls

I'm using the React useScript hook (from useHooks website). It allows to easily load external scripts and cache them once loaded.
It works fine however I found an edge-case causing me some issues..
The problem is with the caching of scripts.
If I have a component loaded 2 times in a page using useScript as below:
const ScriptDemo = src => {
const [loaded, error] = useScript("https://hzl7l.codesandbox.io/test-external-script.js");
return (
<div>
<div>
Script loaded: <b>{loaded.toString()}</b>
</div>
<br />
{loaded && !error && (
<div>
Script function call response: <b>{TEST_SCRIPT.start()}</b>
</div>
)}
</div>
);
};
function App() {
return (
<div>
<ScriptDemo />
<ScriptDemo />
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
You can see and reproduce here: https://codesandbox.io/s/usescript-hzl7l
If my App only have one ScriptDemo it's fine, however having two or more would make it fails.
Indeed the flow will be:
ScriptDemo -> is script cached ? no -> add script to cache -> fetch it -> render
ScriptDemo2 -> is script cached ? yes -> render (but it's not finish loading
yet ..)
One way to fix it is to change the useScript hook to only cache the script after a successful onScriptLoad callback.
The issue with this approach is that the external script will be called twice.
See here: https://codesandbox.io/s/usescript-0yior
ScriptDemo -> is script cached ? no -> fetch it -> add script to cache -> render
ScriptDemo -> is script cached ? no -> fetch it -> add script to cache -> render
I thought about caching the script src AND a loading boolean but then it implies setting up a timeout handling and it gets very complex in my opinion.
So, what is the best way to update the hook in order to load the external script only once but ensuring it's correctly loaded?
In useScript module, we will need to keep track of status of loading the script.
So instead of cachedScripts being simple array of strings, we now need to keep an object representing the status of loading.
This modified implementation of useScript will address the issue:
import { useState, useEffect } from 'react';
let cachedScripts = {};
export function useScript(src) {
// Keeping track of script loaded and error state
const [state, setState] = useState({
loaded: false,
error: false
});
useEffect(
() => {
const onScriptLoad = () => {
cachedScripts[src].loaded = true;
setState({
loaded: true,
error: false
});
};
const onScriptError = () => {
// Remove it from cache, so that it can be re-attempted if someone tries to load it again
delete cachedScripts[src];
setState({
loaded: true,
error: true
});
};
let scriptLoader = cachedScripts[src];
if(scriptLoader) { // Loading was attempted earlier
if(scriptLoader.loaded) { // Script was successfully loaded
setState({
loaded: true,
error: false
});
} else { //Script is still loading
let script = scriptLoader.script;
script.addEventListener('load', onScriptLoad);
script.addEventListener('error', onScriptError);
return () => {
script.removeEventListener('load', onScriptLoad);
script.removeEventListener('error', onScriptError);
};
}
} else {
// Create script
let script = document.createElement('script');
script.src = src;
script.async = true;
// Script event listener callbacks for load and error
script.addEventListener('load', onScriptLoad);
script.addEventListener('error', onScriptError);
// Add script to document body
document.body.appendChild(script);
cachedScripts[src] = {loaded:false, script};
// Remove event listeners on cleanup
return () => {
script.removeEventListener('load', onScriptLoad);
script.removeEventListener('error', onScriptError);
};
}
},
[src] // Only re-run effect if script src changes
);
return [state.loaded, state.error];
}
Edit:
Went to GitHub page of useHooks to suggest this improvement and found some one has already posted similar fix:
https://gist.github.com/gragland/929e42759c0051ff596bc961fb13cd93#gistcomment-2975113

Jest: Testing API Methods from Intercom

I am having trouble understanding what the testing flow would be for testing functions which use functions loaded from a JavaScript library from Intercom.
My method looks like this:
export const generateButton = (handleOnClick) => {
case "moo":
return <button onClick={() => Intercom('show')}>Sign Up</button>
default:
return 'moo'
The error I get when running this is:
ReferenceError: Intercom is not defined
So I figured it out, I needed to add a new file and point jest set up on package.json to it like so (the file added is mockObject)
"setupFiles": [
"./config/jest/setupJest.js",
"./config/jest/mockObject.js"
],
then in the file itself has this in it
global.Intercom = () => {
console.log('Intercom called')
}
If I understand what you're trying to do then create a dummyFunction to replace Intercom in your tests. Something like this...
const Intercom = jest.fn();
describe('button click', () => {
it('Intercom is called correctly', () => {
// whatever component contains the button should be mounted
const wrapper = mount(<YourComponentHere />);
// you may need to add a class to target the specific button
wrapper.find('button').simulate('click');
expect(dummyFunction).toHaveBeenCalledWith('show');
expect(dummyFunction).toHaveBeenCalledTimes(1);
});
});

Error in running unit test for Vue webapp

I am writing a webapp with VueJs, I am trying to setup unit test for it, I got inspired from vue-mdl unit-tests. But the tests are not running properly for my code and I am getting vm.$el as undefined, so not able to move forward at all.
Here is the component, I am trying to test:
Confirmation.vue
<template>
<div>
Your order has been confirmed with the following details.
</div>
</template>
<script type="text/javascript">
export default {
data () {
return {
data_from_pg: null
}
}
}
</script>
and here is test for it, which fails
Confirmation.spec.js
import Confirmation from 'src/components/Confirmation'
import { vueTest } from '../../utils'
describe('Confirmation', () => {
let vm
let confirmation
before(() => {
vm = vueTest(Confirmation)
console.log('vm.$el ' + vm.$el) => this prints undefined
confirmation = vm.$el.querySelector('#confirmation') => so this line gives error
// confirmation = vm.$('#confirmation')
})
it('exists', () => {
confirmation.should.exist
confirmation.should.be.visible
})
})
utils.js
export function vueTest (Component) {
const Class = Vue.extend(Component)
Class.prototype.$ = function (selector) {
return this.$el.querySelector(selector)
}
Class.prototype.nextTick = function () {
return new Promise((resolve) => {
this.$nextTick(resolve)
})
}
const vm = new Class({
replace: false,
el: 'body'
})
return vm
}
My complete code is available here, with all the test config, which I have tried to change many times, but could not figure out how to make it work. Please let me know if you see some error somewhere.
The vueTest function in utils is trying to load the Vue instance into the body tag:
const vm = new Class({
replace: false,
el: 'body'
})
return vm
The unit tests do not load index.html as an entry point into the app, but rather the individual components that you want to test; Therefore, you do not have access to document or html elements and the component is never mounted. I'd suggest using vm.$mount():
If elementOrSelector argument is not provided, the template will be rendered as an off-document element.
You could change the above lines to something like the following
const vm = new Class();
vm.$mount();
return vm;
Your tests should now have access to the $el property.

Why does my isomorphic Cycle.js app cause an xstream exception when rendering on server?

I am trying to write a demo isomorphic Cycle.js/Hapi.js app, but it fails with an exception in xstream while rendering on the server. What is going wrong here? I've based my app on Cycle.js' isomorphic app example.
The traceback looks like this:
TypeError: Uncaught error: s[i]._add is not a function
at CombineProducer._start (/Users/arve/Projects/hapi-cycle/node_modules/xstream/core.js:190:22)
at Stream._add (/Users/arve/Projects/hapi-cycle/node_modules/xstream/core.js:976:19)
at MapOperator._start (/Users/arve/Projects/hapi-cycle/node_modules/xstream/core.js:717:18)
at Stream._add (/Users/arve/Projects/hapi-cycle/node_modules/xstream/core.js:976:19)
at LastOperator._start (/Users/arve/Projects/hapi-cycle/node_modules/xstream/core.js:596:18)
at Stream._add (/Users/arve/Projects/hapi-cycle/node_modules/xstream/core.js:976:19)
at Stream.addListener (/Users/arve/Projects/hapi-cycle/node_modules/xstream/core.js:1050:14)
at Object.streamSubscribe (/Users/arve/Projects/hapi-cycle/node_modules/#cycle/xstream-adapter/lib/index.js:39:16)
at /Users/arve/Projects/hapi-cycle/node_modules/#cycle/base/lib/index.js:49:30
at Array.map (native)
The render code looks basically as follows:
import Cycle from '#cycle/xstream-run'
import xs from 'xstream'
import {html, section, h1, p, head, title, body, div, script, makeHTMLDriver,} from '#cycle/dom'
import serialize from 'serialize-javascript'
import Logger from '#arve.knudsen/js-logger'
let logger = Logger.get('server.rendering')
let wrapVTreeWithHtmlBoilerplate = ([vtree, context,]) => {
return (
html([
head([
title('Cycle Isomorphism Example'),
]),
body([
div('.app-container', [vtree,]),
script(`window.appContext = ${serialize(context)};`),
// script(clientBundle),
]),
])
);
}
let main = (sources) => {
let vtree = (
section('.home', [
h1('The homepage'),
p('Welcome to our spectacular web page with nothing special here.'),
])
)
return {
DOM: vtree,
}
}
let renderIndex = (request, reply) => {
let context = xs.of({})
Cycle.run((sources) => {
let vtree = main(sources).DOM
let wrappedVTree = xs.combine(vtree, context)
.map(wrapVTreeWithHtmlBoilerplate)
.last()
return {
DOM: wrappedVTree,
};
}, {
DOM: makeHTMLDriver((html) => {
let wrappedHtml = `<!doctype html>${html}`
}),
context: () => {return context},
PreventDefault: () => {},
})
}
You can find the full source code here.
I'm running on OS X using Node v6.6.0, babel-node 6.14.0, Hapi 15.0.3, #cycle/dom 12.2.5 and #cycle/xstream-run 3.1.0. Let me know if you need more info.
The reason for the error was that the rendered VTree was not a stream. I changed the code to the following and it works:
let vtree = sources.context.map(({}) => {
return (
section('.home', [
h1('The homepage'),
p('Welcome to our spectacular web page with nothing special here.'),
])
)
})
return {
DOM: vtree,
}
The sources.context.map call (which I borrowed from the original isomorphic example) ensures that vtree is a stream.

Categories

Resources