I am having issues mounting an external script into a component of my React/Gatsby App. The script below is called into a component that is used in two places throughout app.
First being pages/index.js and loads fine with zero issue, yet when called to use within a gatsby created page (exports.createPages = ({ graphql, boundActionCreators }) => {) from a template the script will load, show content and then go.
Here is the code for the script being mounted into the component -
componentDidMount () {
const tripadvisor = document.createElement("script");
tripadvisorLeft.src = "https://www.jscache.com/wejs?wtype=selfserveprop&uniq=789&locationId=10467767&lang=en_NZ&rating=true&nreviews=0&writereviewlink=true&popIdx=true&iswide=true&border=false&display_version=2";
tripadvisorLeft.async = true;
document.body.appendChild(tripadvisor);
}
I am not getting any errors from the console.
NOTE: Incase of relation to the error? I also have this code using componentDidMount and componentWillUnmount in the /layout/index.js file that handles a body class for navigation elements.
componentDidMount () {
this.timeoutId = setTimeout(() => {
this.setState({loading: ''});
}, 100);
this.innerContainer.addEventListener("scroll", this.handleHeaderStuck), 100;
this.innerContainer.addEventListener("scroll", this.handleSubNavStuck), 200;
}
componentWillUnmount () {
if (this.timeoutId) {
clearTimeout(this.timeoutId);
}
this.innerContainer.removeEventListener("scroll", this.handleHeaderStuck);
this.innerContainer.removeEventListener("scroll", this.handleSubNavStuck);
}
UPDATE: All code
import React from 'react';
import Link from 'gatsby-link'
import styled from 'styled-components'
const Wrapper = styled.section`
display:block;
`
class ReviewsPage extends React.Component {
componentDidMount () {
const tripadvisorLeft = document.createElement("script");
tripadvisorLeft.src = "https://www.jscache.com/wejs?wtype=selfserveprop&uniq=789&locationId=10467767&lang=en_NZ&rating=true&nreviews=0&writereviewlink=true&popIdx=true&iswide=true&border=false&display_version=2";
tripadvisorLeft.async = true;
document.body.appendChild(tripadvisorLeft);
}
render() {
return (
<Wrapper id="tripAdvisor">
<div id="TA_selfserveprop789" className="TA_selfserveprop">
<ul id="3LacWzULQY9" className="TA_links 2JjshLk6wRNW">
<li id="odY7zRWG5" className="QzealNl"></li>
</ul>
</div>
</Wrapper>
)
}
}
export default ReviewsPage
So, all your componentDidMount() is doing is adding a <script> tag which references a third party script. I am assuming that third party script tries to add some information or thing to the DOM (something you can see visually).
However, the DOM only exists between component updates. React will completely redraw the DOM (the HTML inside your component) any time it detects a change to State or Props. I'm assuming in this case that Wrapper is what is resetting each time.
I'm not sure how to help with this, mainly because React's entire role in an application is really just managing the state of the DOM, and that script is trying to edit the DOM, but without telling React. React might be sensing an invalid change to the DOM then trying to correct it, but I really don't think React does that. At any rate, the issue is that React is trying to manage the DOM while another thing is trying to edit the DOM, and that's not gonna end well.
It would be better if you could have a script that asynchronously calls to the other service and receives data, then let React apply that data to the DOM, instead of letting the script edit the DOM itself. Granted, you probably don't have control over how that external script actually works, which is why I say I'm not sure how to help.
Related
When an HTML document defined a variable that is not available until a later time during the page load.
Issue: A React, Vue, or other block of code depends on an object that has not yet been declared and outside the direct scope of the component, like window['varname']. What are the proper way(s) to wait for that variable to be defined before rendering a component's real content.
My Attempt:
import React from 'react'
import ReactDOM from 'react-dom/client'
import AppWrapper from "./components/AppWrapper";
const App = () => {
let intervalId
intervalId = setInterval(() => {
console.log('Waking up... checking if window.app is defined')
if (window['app'] !== undefined) {
console.log('yes')
clearInterval(intervalId)
} else {
console.log('no')
}
}, 1000)
if(app.ins.length === 0) {
return 'Loading...'
}
return (
<AppWrapper app={window['app']}></AppWrapper>
)
}
export default App
What other ways could you, should you, do it?
I will make it clearer for you :) I will describe exactly my problem: So I am writing a custom Joomla Component for Joomla. The Joomla Component is written in PHP and uses the Joomla Framework. Inside this component, I have written a Reactjs component. The way that you inject JavaScript into Joomla is via Joomla Methods. These methods either load the JS on the head of the document or in the body. Now, my Reactjs component is loaded during this process. This is fine and it works as long as I do not need to rely on outside variables.
I am using Joomla to store data that is need by the Reactjs component. The way that Joomla makes data available to JS is by a Joomla library that will inject the JS object into a script tag. This is also okay. The issue is that when the head tag loads the Reactjs component before the injected JS object, needed by the Reactjs component, is available. In my example above I store the global JS object into the window object as window.app = Some Object. Because the window.app object is not available at the time the Reactjs component has been loaded, I add a setInterval and check every 500 ms.
Then the setInterval wakes up and checks to see if the window["app"] is available yet. It keeps doing that until it is available. Once it is, it quits the interval and loads the Reactjs component container, passing in the required object.
Now, two things here:
I have no way of synchronizing this process in Joomla. Joomla is stubborn like that.
This is my attempted to only load the Reactjs container component once the data is available.
Question: Knowing the situation, what are the best strategies to accomplish this, apart from my current strategy?
Thanks :)
I believe, one of the approaches could be any kind of callback or subscription.
For example, you can define a function, which changes a state in state-container like redux.
(Pseudocode)
async function loadAppData(store) {
const data = await fetch('/some-data');
const json = await data.json();
store.dispatch('data-loaded', json)
}
And in component
function App() {
const appData = useSelector(store => store.appData);
if (!appData) {
return 'Loading...'
}
return <Markup />
}
Other option can be subscription. Again you can add some function which emits some event:
async function loadAppData(store) {
const data = await fetch('/some-data');
const json = await data.json();
eventBus.emit('data-loaded', json)
// or
window.appData = json
}
In react you can
function App() {
const [appData, setAppData] = useState();
useEffect(() => {
setAppData(window.appData)
}, [window.appData])
if (!appData) {
return 'Loading...'
}
return <Markup />
}
Or in Vue.js you could
data() {
return {
appData: ''
}
}
mounted() {
this.$on('data-loaded', this.onDataLoaded)
}
methods: {
onDataLoaded($event) {
this.appData = $event;
}
}
I am working on a library which requires exporting a couple of functions for users to call upon. Those functions need access to component ref in order to add/remove classNames and auto scroll etc.
I was able to get it to work by moving my ref (created by React.createRef) outside of the component itself (NOT talking about defining it outside of the constructor but inside the component)
Here's how my code looks like (used a class component instead of functional as the hook useRef obviously can't be used outside)
import React, { PureComponent, createRef } from "react";
import { typingEffect } from "../redux/actions/dispatch";
import { containerRef } from "./Container";
let typingRef = createRef();
export async function displayTypingEffect() {
await typingEffect();
typingRef.current.className += " rcb-is-typing";
containerRef.current.scrollTop = containerRef.current.scrollHeight + 700;
}
export function hideTypingEffect() {
typingRef.current.className = "rcb-typing-container";
containerRef.current.scrollTop = containerRef.current.scrollHeight + 700;
}
export default class Typing extends PureComponent {
render() {
return (
<div ref={typingRef}>
rest of the component code which is unnecessary for this question
</div>
)
}
I am just wondering if there's a possibility of any unforeseen issues or bugs if I follow this pattern.
Thank you.
This makes the typingRef
a global variable (inside the module), and
be created outside of any React life cycles
typingRef will be the same object for every instance of the Typing component, i.e. if two components are created from the Typing class, both will write to the same typingRef. Your API will provide access some DOM element, but you can not be sure which one it currently is.
typingRef is created as soon as the file is imported, before React even starts, and will live for the life time of the Javascript code, not the life time of any React component.
I think (not 100% sure) any DOM elements referenced by typingRef will be kept (at least) until typingRef gets overwritten (or the Javascript execution is ended). So if a Typing component gets unmounted, the DOM element (and everything that's connected to it) is still kept in memory. So your API will provide access to "useless" DOM elements.
I am trying to create a Buy Me A Coffee React component for my Gatsby website and even though my Gatsby site runs in development mode and successfully builds, the component (Buy Me A Coffee widget) doesn't show up when loading the page.
My website uses MDX, so ideally I'd like to be able to import the component into my blog posts. I like the idea of importing it into my blog posts because it allows me to optionally include it, whereas if I use a standard solution like gatsby-ssr.js for including the third party Buy Me A Coffee script, I foresee it being much more difficult to regulate on what pages the component does and doesn't show.
At the moment, I use the library browser-monads so I don't have to do typeof !== "undefined" conditional check for building my site. Using the traditional conditional format they recommend here doesn't help. Also styles.scss is currently empty. I am importing this in case I need to go back and add styles to my component later.
Thanks for your help!
Below is my code:
import React from 'react';
import './styles.scss'
import { window, document, exists } from 'browser-monads';
class BuyMeACoffee extends React.Component {
constructor(props){
super(props)
let script = document.createElement("script");
script.setAttribute('data-name','BMC-Widget')
script.src = "https://cdnjs.buymeacoffee.com/1.0.0/widget.prod.min.js"
script.setAttribute('data-id', 'x');
script.setAttribute('data-description', 'Thank you for your support!');
script.setAttribute('data-message', "We're proudly reader-supported! If our content helps you, we would be honored and greatly appreciate it if you'd consider buying us a coffee!");
script.setAttribute('data-color',"#2962ff")
script.setAttribute('data-position','right')
script.setAttribute('data-x_margin','18')
script.setAttribute('data-y-margin','18')
script.async = true
//Call window on load to show the image
script.onload=function(){
var evt = document.createEvent('Event');
evt.initEvent('load', false, false);
window.dispatchEvent(evt);
}
this.script=script
}
componentDidMount () {
document.head.appendChild(this.script)
}
componentWillUnmount(){
document.head.removeChild(this.script);
document.body.removeChild(document.getElementById("bmc-wbtn"))
}
render(){
return(null)
}
}
export default LoadBuyMeACoffee;
I would suggest using gatsby-ssr.js approach instead of adding a load performance directly in the class component. Something like this should work for you:
const React = require("react");
exports.onRenderBody = ({ setPostBodyComponents }) => {
setPostBodyComponents([
<script
data-name="Mac-Mann-Widget"
src="https://cdn.buymeacoffee.com/widget/1.0.0/prod/widget.prod.min.js"
data-id="eshlox"
data-description="Support me on Buy me a coffee!"
data-message="Thank you for visiting. You can now buy me a coffee!"
data-color="#FF813F"
data-position="right"
data-x_margin="28"
data-y_margin="18"
></script>,
]);
};
I will leave the snippet above to see if it helps someone.
Thanks for the response. Unfortunately I really don't want to use
gatsby-ssr.js because I want the ability to be able to optionally
include the Buy Me A Coffee script in my blog posts. – Mac-Mann 4
hours ago
You can try something like this:
Create a function that will render asynchrounously on-demand the script:
const addExternalScript = (url, callback) => {
const script = document.createElement('script');
script.src = url;
script.async=true;
script.onload = callback;
// add as many parameters as you need.
document.body.appendChild(script);
};
You can also remove the callback function parameter if you don't need it despite being useful.
Call them in your componentDidMount() (that will make the trick since you need to wait for the DOM tree loading until access to a document global object):
componentDidMount(){
addExternalScript(`https://cdn.buymeacoffee.com/widget/1.0.0/prod/widget.prod.min.js`)
}
I'm working with Storybook for components creation in pure HTML, CSS and JS (jQuery actually).
I'm wondering if there's a callback function when a story is rendered on the screen (like useEffect for React) because I need to run then the relative jQuery code only when the component is really in the DOM.
Here's my situation:
// Button.stories.js
// my pure HTML component
const Button = () => `<button>My button </button>`;
export default {
title: 'Buttons',
};
export const ButtonStory = () => Button()
// would be great something like: ButtonStory.callback = function(){ do this}
There's something without workaround? (Like wrap the HTML component inside react component and use useEffect for trigger the code)
Thanks
I'll share a not optimal solution I've found but maybe could be useful for others:
MyStory.stories.js
import {ActionBar} from '../public/components/ActionBar/actionbar.module';
import controller from "!raw-loader!../public/components/ActionBar/controller.js";
import customRenderStory from "../utils/customRenderStory";
export default {
title: 'Basic/ActionBar',
};
//
export const ActionBarToExport= () =>
customRenderStory(
ActionBar(),
controller,
2000
);
customRenderStory.js
export default (component, javascript, timeout) => {
if (javascript !== undefined) setTimeout(() => eval(javascript), timeout)
return component;
}
In this way, I can execute the code inside controller.js each time the story is rendered. I need a timeout (which can be configured) because I can't be sure that the component will be mounted after the code execution.
In my case, StoryBook in plain HTML and jQuery, seems is working.
I have a site with lots of modals which can be opened from anywhere (for example LoginModal). The challenge I'm running into is if I open one programmatically with something like ReactDOM.render, how do I clean it up automatically when the parent component is unmounted without putting it (and all possible modals) in the template.
For example, something like this to open it:
openLoginModal() {
ReactDOM.render(<LoginModal />, document.body);
}
LoginModal can clean itself up when closed. However, if the DOM from the component which opened it is unmounted, how do I let LoginModal know to unmount as well.
One thought I've had is to use an Rx.Subject to notify it when to unmount, but this also sounds like a bit of a wrong approach and a possible anti-pattern.
For example:
// modules/User.js
openLoginModal(unmountSubj) {
const container = document.createElement('div');
ReactDOM.render(<LoginModal />, container);
unmountSubj.subscribe(() => {
ReactDOM.unmountComponentAtNode(container);
});
}
// components/RandomView.jsx
unmountSubject = new Rx.Subject();
componentWillUnmount() {
this.unmountSubject.next();
}
login() {
User.openLoginModal(this.unmountSubject);
}
I'd like to avoid having all the possible modal components in each JSX template they might be used in.
How would you approach this?
Here's the solution I've come up with so far: There's a modal manager module, which will render a modal into the DOM (via ReactDOM.render) and return a function which will unmount it.
Here's a simplified version:
// modalManager.js
export default modalManager = {
openModal(modalClass, props) {
// Create container for the modal to be rendered into
const renderContainer = document.createElement('div');
document.body.appendChild(renderContainer);
// Create & render component
const modalInst = React.createElement(modalClass, { ...props, renderContainer });
ReactDOM.render(modalInst, renderContainer);
// Return unmounting function
return () => this.unmountModal(renderContainer);
},
unmountModal(renderContainer) {
ReactDOM.unmountComponentAtNode(renderContainer);
renderContainer.parentNode.removeChild(renderContainer);
},
}
// TestThing.jsx
class TestThing extends React.Component {
unmountLogin = null;
componentWillUnmount() {
this.unmountLogin();
}
login() {
this.unmountLogin = modalManager.openModal(Login, {});
}
}
You'll also notice that renderContainer is passed to the modal component. This way the modal can call modalManager.unmountModal itself when closed.
Let me know what you think.
In my current React project, I've addressed this by having a multilayered component architecture.
<App>
// the standard starting point for React apps
<DataLayer>
// this is where I make API calls for data that is shared between all components
<DisplayLayer>
// this is where I put the methods to launch display elements that are shared
// by all components (e.g., modals, alerts, notifications, etc.)
<Template>
// this is the first layer that is actually outputting HTML content
<ModuleX>
<ModuleY>
<ModuleZ>
// these modules control the main display area of the screen, they encompass
// major UI functions (e.g., UsersModule, TeamsModule, etc.)
// when one of these modules needs to launch a shared UI element (like a
// modal), they call a method in the <DisplayLayer> template - this means
// that if I have a commonly-used modal (like, LoginModal), it doesn't need
// to be included in every one of the core modules where someone might need
// to initiate a login; these modules are mounted-and-unmounted as the user
// navigates through the app
So when the app loads, <App>, <DataLayer>, <DisplayLayer>, and <Template> all load up (and they will only load one time). As the user navigates around, the <ModuleX/Y/Z/etc> components are mounted-and-unmounted, but all of the "common stuff" stays in place that was mounted/loaded in the higher layers of the app.