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.
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;
}
}
So, in my next app, i'm using react-google-one-tap-login library that has useGoogleOneTapLogin hooks and must be call inside of react component. But when i call it like:
func() {
useGoogleOneTapLogin({})
return (some jsx)
}
I got an error ReferenceError: window is not defined.
Then, i'll try to fix it using if (process.browser) then run useGoogleOneTapLogin, but i got an error: useGoogleOneTapLogin is called conditionally, hooks must be called in the exact same order in every component render. BUT, this is only occur when i'll try to deploy the app.
Then, once again i'll try to fix by call it inside useEffect for the first time comp load, by also got an error: Error: Invalid hook call. Hooks can only be called inside of the body of a function component.
https://github.com/MSalmanTariq/react-google-one-tap-login/issues/13#issuecomment-1056016843
import dynamic from 'next/dynamic'
const MyComponent = () => {
const GoogleOneTapLogin = dynamic(() => import('react-google-one-tap-login'))
return (
<GoogleOneTapLogin
onError={(error) => console.log(error)}
onSuccess={(response) => console.log(response)}
googleAccountConfigs={{ client_id: 'example.apps.googleusercontent.com' }}
/>
)
}
You can't call hooks inside conditions per react documentation. See: https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level
To solve your issue if your hook depends on window object or you want to client side rendering, you can delay rendering your component only on client side.
// Create a component called ClientOnly
// components/ClientOnly.js
import { useEffect, useState } from "react";
export default function ClientOnly({ children, ...rest}) {
const [hasMounted, setHasMounted] = useState(false);
useEffect(() => {
setHasMounted(true);
}, []);
if (!hasMounted) {
return null;
}
return <div {...rest}>{children}</div>;
}
then, wrap your component with this component
<ClientOnly>
<YourGoogleOneTapLoginComponent />
</ClientOnly>
To read more about this pattern, See: https://www.joshwcomeau.com/react/the-perils-of-rehydration
I want to integrate qr-scanner to my project. This library is capable of decoding QR-Codes by utilizing the camera. It just needs the reference to a html-video-element and then renders the webcam-stream and optionally some indicators of a QR-code being found to this element.
The easiest component would be something like this:
import { useRef } from "react";
import QrScanner from "qr-scanner";
export const QrComponent = () => {
const videoElement = useRef(null);
const scanner = new QrScanner(videoElement.current, (result) => {
console.log(result)
})
return (
<video ref={videoElement} />
)
}
however, qr-scanner checks, if the passed target-element is already part of the DOM:
if (!document.body.contains(video)) {
document.body.appendChild(video);
shouldHideVideo = true;
}
the video-element will never be added to the DOM, when the QrScanner-object is created. This leads to shouldHideVideo being set to true, which disables the video altogether later in the library-code.
So I think I need some kind of way to react to the video-element being added to the DOM. I thougt about using a MutationObserver (and tried it out by stealing the hook from this page), however I only wanted to print out all mutations using the hook like this:
import { useRef, useCallback } from "react";
import QrScanner from "qr-scanner";
import { useMutationObservable } from "./useMutationObservable";
export const QrComponent = () => {
const videoElement = useRef(null);
const scanner = new QrScanner(videoElement.current, (result) => {
console.log(result)
})
const onMutation = useCallback((mutations) => console.log(mutations), [])
useMutationObservable(document, onMutation)
return (
<video ref={videoElement} />
)
}
however, I never got a single line printed, so to me it seems, as if there are no mutations there.
Did I maybe misunderstand something? How can I react to the video-element being added to the document?
Ok, so I think I didn't provide enough information, but I found it out on my own:
The MutationObserver doesn't work, because I told it to watch document, yet I'm actually inside of a shadowdom with this particular component! So I suspect that mutations to the shadowdom won't be detected.
Also, because I'm inside of a shadowdom, document.contains(videoElem.current) will never be true. So in this particular case I have no better choice, than to copy the code of qr-scanner into my project-tree and adapt as needed.
On another note: useLayoutEffect is a react-hook, that is scheduled to run after DOM-mutations. So that's what I would use if I had to start over with this idea.
We have a rather big react application that is occasionally resulting in the following error:
https://reactjs.org/docs/error-decoder.html/?invariant=307
Hooks can only be called inside the body of a function component.
(https://reactjs.org/link/invalid-hook-call)
Sadly that is only happening in the production environment and we can't manage to reproduce the error in our development environment. And even in production it happens rarely, our QA department was also unable to reproduce it...
There is no obvious call of a hook outside of a function component and the component is not called outside of render return of the outer components.
Here is a extremely simplified example of the structure I'm talking about (and also a little bit of the structure of the function inside which the error happens):
import React, {useState, useEffect} from 'react';
class Outer extends React.Component {
render () {
return (
<div>
<Fu outerProps={this.props} />
</div>
);
}
}
const Fu = ({outerProps}) => {
const [open, setOpen] = useState(false);
const [boundCloseFn] = useState(() => setOpen.bind(null, false));
//the 2nd useSetState is only used to ensure a reference to the same function is obtained for every render
//since we only need the initial state the 'bind' happens inside a function, using the 'useState' lazy loading
useEffect(
() => {
document.addEventListener('click', boundCloseFn, true);
document.addEventListener('touchend', boundCloseFn, true);
return () => {
document.removeEventListener('click', boundCloseFn, true);
document.removeEventListener('touchend', boundCloseFn, true);
};
},
[boundCloseFn]
);
const switchOpen = () => {
setOpen(!open);
};
return (
<div onClick={switchOpen}>
{open && <div>Stuff</div>}
<div>Other stuff</div>
</div>
);
};
We realise the creation of the boundCloseFn can be done with different hooks, but we don't think that is causing the issue we have.
The react error seems to happen with the extremely simple first useState, the very first line of the Fu function.
While the entire app has a lot of complicated logic and is loading some components lazily, this particular part is loaded directly, similarly to how it is in the example.
The reason we mix classes and hooks is that the class was written years ago while that component was added recently and we are trying to use hooks from now on.
Our current react version is 16.8.4.
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.