How to useEffect in component that is rendered in another component - reactjs

This is what I am trying to do:
import { SVG } from '#svgdotjs/svg.js'
const SVGpaper = (props) => {
useEffect(() => {
let canvas = SVG().addTo('#canvas').size(6006, 600);
canvas.rect(100, 100);
});
return (
<div id="canvas">
</div>
)
}
const Profile = () => {
return (
(typeof someVariable !== "undefined") &&
<CCol>
<CRow>
<div>
{SVGpaper()}
</div>
</CRow>
</CCol>
);
};
export default Profile;
Profile is the "big" webpage, it will fetch some data from a server to "someVariable". To make sure it is not rendered prematurely it (Profile) has conditional rendering. When it is eventually rendered I want to create an SVG. I have therefore created a component that creates the SVG and put it into the render of Profile. Because SVG().addTo must be executed after the div id="canvas" actually exist, it is put in a useEffect to prevent it from executing before the div exist. This works great in my head, but not in reality because it breaks the rules of hooks. I get the error:
"Warning: React has detected a change in the order of Hooks called by Profile. This will lead to bugs and errors if not fixed. For more information, read the Rules of Hooks: https://reactjs.org/link/rules-of-hooks"
And I should know this, but I am having issues making this code work, so I am trying a little bit of every idea I get, but without any success. How can I change this code and make it render in this order:
conditional rendering for "someVariable"
render the div with id="canvas"
Create the SVG and assign it to the div with id="canvas"
?
Thank you for your help!

Related

React component switch animation with a loader - Glitchy

I have a requirement where i have couple of components out of which only one could be displayed at a particular point of time. The components are of varying heights.
When a user clicks on the button in the currently displayed component i need to
Show a loader to the user
Render the component in background.
Wait till the rendered component calls onLoad callback.
Once onLoad callback is recived, hide the loader.
Something like below (Code sandbox here)
const [loading, setLoading] = useState(false);
const [activeElement, setActiveElement] = useState("Component1");
const onLoad = () => {
setLoading(false);
};
const onClick = () => {
setLoading(true);
setActiveElement(
activeElement === "Component1" ? "Component2" : "Component1"
);
};
return (
<div className="container">
<ViewWrapperHOC loading={loading}>
{activeElement === "Component1" ? (
<Component1 onLoad={onLoad} onClick={onClick} />
) : (
<Component2 onLoad={onLoad} onClick={onClick} />
)}
</ViewWrapperHOC>
</div>
);
I was planning to write a wrapper component (ViewWrapperHOC in above example) to show the transition between the components and show the loader. Now the problem is when i try to animate, since i have to render both the progressbar and children in viewwrapper, the animation seems to be very glitchy.
Code sandbox here
Could someone suggest a way to fix this. I am open to any alternate animations or any alternate approach to achieve this in any form of pleasant ux. Thanks in advance.
In that second code sandbox, your issue is that as soon as you click, you are changing which element is active, therefore it gets rendered.
You could put a small timeout in the onClick function:
const onClick = () => {
setLoading(true);
setTimeout(() => {
setActiveElement(
activeElement === "Component1" ? "Component2" : "Component1"
);
}, 100);
};
Then in view wrapper you'll need to get rid of transition: "200ms all ease-in-out, 50ms transform ease-in-out".
You need to have a known height here to be able to make a smooth change in height transition.
See codesandbox fork
Version 2:
Here is version 2, using refs. Main changes are moving the box shadow out of the components to the container in view wrapper. Adding refs to the components, passing the active ref to the wrapper and then setting the height with height transition.
To be honest, I'd probably restructure the code a lot if I had the time.

React/Gatsby: Wrong div loads for a brief moment when conditional rendering

I'm trying to conditionally render a div in Gatsby in an effort to build a responsive nav menu. Unfortunately, I'm getting a quick flash of the menu div just before the full navigation menu loads. Any tips or tricks to resolve this would be appreciated!
import React, { useEffect, useState } from "react"
import * as navlinksStyles from "./navlinks.module.scss"
const NavLinks = () => {
const [windowDimension, setWindowDimension] = useState(null)
useEffect(() => {
setWindowDimension(window.innerWidth)
}, [])
useEffect(() => {
function handleResize() {
setWindowDimension(window.innerWidth)
}
window.addEventListener("resize", handleResize)
return () => window.removeEventListener("resize", handleResize)
}, [])
const isMobile = windowDimension <= 740
return (
<div className={navlinksStyles.wrapper}>
{isMobile ? (
<div>
<h1 className={navlinksStyles.menu}>menu</h1>
</div>
) : (
<div className={navlinksStyles.navlinksWrapper}>
<ul>
<li>About</li>
<li>Services</li>
<li>Frames</li>
<li>Lenses</li>
<li>Locations</li>
<li>Mailbox</li>
</ul>
</div>
)}
</div>
)
}
export default NavLinks
Gatsby does server-side rendering, which involves rendering your React components in a Node environment and saving out the markup produced as a static file. When someone visits one of your pages in production (or in development if you're using SSR in dev), React renders your components and associates them with the already-visible DOM nodes in a process referred to as “rehydration”.
This is all important to know because useEffect (or class-based API methods like componentDidMount) don't run during SSR. They only run once the code is rehydrated client-side. Further, if the DOM nodes produced server-side don't match up with what React renders client-side on the initial render (before any useEffect hooks run), you wind up with a hydration mismatch error that prompts React to throw away the DOM nodes that exist and replace them with what it has produced client-side.
Armed with this info, you can start to debug what might be happening to cause a flash of unexpected content and how to address it:
Server-side, windowDimension is null, and null <= 740 is true, so isMobile is set to true
The output produced server-side then shows the div>h1 element that you're expecting mobile visitors to see
Client side, React rehydrates and fires useEffect hooks, the first of which calls a state setter passing a number greater than or equal to (presumably) 740, prompting a re-render
The component re-renders with the updated value and isMobile is set to false, updating the output to the full nav menu
Once approach to solving this is to wait to render markup or render a placeholder until you’re rendering in a browser:
// Note: do NOT do this!
const NavLinks = () => {
if (typeof window === "undefined") return null
return (
<div>Your content</div>
)
}
The problem with this, as alluded to above, is that you wind up producing different markup server-side than you do in a browser, causing React to throw away DOM nodes and replace them. Instead, you can ensure the initial render matches the server-side output by leveraging useEffect like so:
const NavLinks = () => {
const [ready, setReady] = useState(null)
useEffect(() => { setReady(true) }, [])
// note: the return value of an `&&` expression is the value of the first
// falsey condition, or the last condition if all are truthy, so if `ready`
// has not been updated, this evaluates to `return null`, and otherwise, to
// return <div>Your content</div>.
return ready && <div>Your content</div>
}
There is another approach that works in many scenarios that will avoid the extra render, DOM update/layout/paint cycle: use CSS to hide one of the DOM branches as relevant:
/** #jsx jsx */
import { jsx } from "#emotion/core"
const mobile = "#media (max-width: 740px)"
const NavLinks = () => (
<div>
<div css={{ display: "none", [mobile]: { display: "block" } }}>
Mobile menu
</div>
<div css={{ [mobile]: { display: "none" } }}>
Desktop menu
</div>
</div>
)
I prefer this approach when possible as it cuts down on layout thrashing on-load, but if the DOM trees are large, the extra markup can slow things down as well. As with anything, do some testing for your own use cases and select what works best for you and your team.
Your page is rendered with windowDimension as null, so when someone with a wider windowDimension visit your page, they'll see a brief flash of mobile layout before React kicks in & render the correct one.
You can get around this by using #media query instead.

How can I create a separate dynamic window in my React app?

My React app incorporates a video chat function (via Twilio). The user goes to a dashboard and then clicks a button to start the call. This prompts the VideoCall component to be instantiated and shown. On instantiation, it connects to a backend Twilio service to get an access token, and then connects to Twilio to create the call, set up events handlers etc.
Currently I'm showing the video windows in a div within the dashboard, but I would like them to appear in a pop-out window instead. I've tried using react-new-window and I've tried React Portals, but I didn't know enough about what I was doing to make it work.
Currently I have the following Dashboard component:
function Dashboard(props) {
const { displayInfo} = props
const [ callInitiated, setCallInitiated ] = useState(false)
const initCall = () => {
setCallInitiated(true)
}
return (
<div id="dashboard">
{callInitiated ? <VideoCall displayInfo={displayInfo} /> : null }
<div> ...rest of dashboard, including button which calls 'initCall' on being clicked... </div>
</div>
)
}
export default Dashboard
My VideoCall component is:
const Video = require('twilio-video');
// Get access token from backend service
async function getAccessToken(identity) {
const url = `${process.env.REACT_APP_TWILIO_TOKEN_SERVICE}?identity=${identity}`;
try {
const response = await axios.get(`${url}`, AXIOS_HEADERS);
return response.data.accessToken;
} catch {
return null;
}
}
// VideoCall component
function VideoCall(props) {
const { displayInfo} = props
// Connect to Twilio server function to get access token
getAccessToken('Tester')
.then((token) => {
Video.connect(token, {
name: 'Test room'
})
.then(room => {
participantConnected(room.localParticipant);
room.participants.forEach(participantConnected);
room.on('participantConnected', participantConnected);
room.on('participantDisconnected', participantDisconnected);
room.once('disconnected', error => room.participants.forEach(participantDisconnected))
})
});
function participantConnected(participant) {
const div = document.createElement('div');
div.id = participant.sid;
participant.on('trackSubscribed', track => trackSubscribed(div, track));
participant.on('trackUnsubscribed', trackUnsubscribed);
participant.tracks.forEach(publication => {
trackSubscribed(div, publication.track);
});
if(participant.identity === 'Tester') {
document.getElementById('myVideoWindow').appendChild(div)
}
}
function participantDisconnected(participant) {
document.getElementById(participant.sid).remove();
}
function trackSubscribed(div, track) {
div.appendChild(track.attach());
}
function trackUnsubscribed(track) {
track.detach().forEach(element => element.remove());
}
return (
<div id="callWrapper" className="callOuterWrapper">
<div className="titleBar">
<h1 className="pageHeader">Calling {displayInfo.receiverName}</h1>
</div>
<div className="videoRoom">
<div id="myVideoWindow" className="callWindow"></div>
<div className="callInfo"> ...this will contain info on call status... </div>
<div id="receiverWindow" className="callWindow"></div>
</div>
</div>
)
}
export default VideoCall
So far, this works. The user clicks the button and the video windows appear at the top of the dashbaord, as expected.
Now I want to pull the VideoCall component out into a separate window (so that the user can still see the dashboard while on the call.
I tried the package react-new-window, which just involved wrapping the VideoCall in a NewWindow. I tried wrapping it within the Dashboard component:
<div id="dashboard">
{callInitiated ? <NewWindow><VideoCall displayInfo={displayInfo} /></NewWindow> : null }
<div> ...rest of dashboard, including button which calls 'initCall' on being clicked... </div>
</div>
and when that didn't work I tried wrapping within the VideoCall component:
<NewWindow>
<div id="callWrapper" className="callOuterWrapper">...</div>
</NewWindow>
In both cases this displayed the new window with the empty callWrapper div; however, once it reached document.getElementById('myVideoWindow').appendChild(div) it was unable to find the div. The DOM being referenced appears to be the one from the Dashboard window rather than the new windows (also, any console.log commands get logged to the console of the original window, not the new one).
I then tried taking apart the NewWindow code and creating my own bespoke version, but I don't know enough about how it works to make it do what I needed.
So, is there a way to access the DOM of the new window from the component within it? Or is there a different approach I should be taking?
Twilio developer evangelist here.
Directly accessing the DOM with native DOM methods, like document.getElementById, is a little frowned upon within React. React itself should be in charge of adding and removing things from the DOM.
I wrote a post on how to build a video chat with React that covers how to add your participants to the page without accessing the DOM directly.
I'd recommend a look through that and perhaps updating your app so that you don't have to use document.getElementById and then hopefully the <NewWindow> component should work as advertised.

Ways to avoid the Warning: React has detected a change in the order of Hooks

I have a component that dispatches some actions when a state somewhere else in the app changes.
But this is somehow giving me the dreaded change in the order of Hooks error.
In reading the rules of hooks it says basically wrapping hooks in conditionals is verboten... Which I am not currently doing.
However I am rendering lists of items and the values of those lists changes, as the server loads data. Using selectors and redux-sagas makes all of this pretty impossible to actually debug, so how else might I track down the root of the problem?
I have one component:
const PhraseList = props => {
const trainingPhrases = useSelector(selectFilteredPhrases());
return (
<div className={styles.Wrapper}>
<ul className={opStyles.listReset}>
{
trainingPhrases.map((phrase, idx) => {
return PhraseItem({ phrase, idx });
})
}
</ul>
</div>
);
which calls the PhraseItem component, which throws an Error.
const PhraseItem = ({ phrase, idx }) => {
// this line chokes
const [checked, setChecked] = useState(false);
return (
<li key={"tr-" + idx}>
<div className={styles.container}>
<div className={styles.phraseText}>{phrase.text}</div>
</div>
</li>
);
};
I'm debugging changes in the selector (selector factory) and that is what seems to trigger the app to crash.
export const selectFilteredPhrases = () =>
createSelector(trainingPhrases, phraseFilter, (trainingPhrases, phraseFilter) => {
console.log('trainingPhrases', trainingPhrases)
return trainingPhrases
});
but if data didn't change ever, it's hard to buidl an app :O
I saw an answer about using JSX which I don't think is relevant. And not much else on this issue.
redux, sagas, hooks, selectors... the react DSL is more and more layers and it's hard to debug deep in the framework.
UPDATE. I think this might be because I'm calling two sagas one after the other and this causes some kind of race condition in the UI?
const pickIntent = () => {
dispatch(pickIntentAction(intentId));
dispatch(getIntentPhrasesAction(intentId));
I think this answer is related, as it is already mentioned in this link, if you call the jsx component as a function react thinks that, hooks which are declared in this jsx function is part of the app component, so if your array trainingPhrases has fewer element on the first render than on the second one it will cause this warning (there will be less rendered jsx components, which means there will be less hooks). solution is simple call jsx component like this <PhraseItem phrase={phrase} idx={idx} />

How do you reload an iframe with ReactJS?

My ReactJS component contains an iframe. In response to an event in the outer page, I need to reload the iframe. If the user has navigated to another page in the iframe, I need to reset it to the URL that it had when I first loaded the page. This URL is available in this.props.
I've tried using forceUpdate(). I can see that this causes the render method to run, but the iframe doesn't get reset - presumably because React can't tell that anything has changed.
At the moment I'm appending some random text to the iframe's querystring: this URL change forces React to re-render the iframe. However this feels a bit dirty: the page within the iframe is beyond my control so who knows what this extra querystring value might do?
resetIframe() {
console.log("==== resetIframe ====");
this.forceUpdate();
}
public render() {
console.log("==== render ====");
// How can I use just this.props.myUrl, without the Math.random()?
let iframeUrl = this.props.myUrl + '&random=' + Math.random().toString();
return <div>
<button onClick={() => { this.resetIframe(); }}>Reset</button>
<iframe src={iframeUrl}></iframe>
</div>
}
(I'm using TypeScript too, if that makes a difference.)
I'd create a state variable with the random, and just update it on resetIframe:
state = {
random: 0
}
resetIframe() {
this.setState({random: this.state.random + 1});
}
public render() {
return <div>
<button onClick={() => { this.resetIframe(); }}>Reset</button>
<iframe key={this.state.random} src={this.props.myUrl}></iframe>
</div>
}
Here is a fiddle working: https://codesandbox.io/s/pp3n7wnzvx
In case of Javascript frameworks and technologies, If you update the key, then it'll automatically reload the iFrame. So you just want to increase (or Random number) the value of key for your action.
state = {
iframe_key: 0,
iframe_url: 'https://www.google.com/maps' //Your URL here
}
iframeRefresh() {
this.setState({iframe_key: this.state.iframe_key + 1});
}
public render() {
return (
<div>
<button onClick={() => { this.iframeRefresh(); }}>Reload Iframe</button>
<iframe key={this.state.iframe_key} src={this.state.iframe_url}>
</iframe>
</div>
);
}
<iframe key={props.location.key} />
use This "Router location Key" each click will get random value, whenever to navigate to here this value will get change then iframe will get the refresh
You could create a new component which just loads you iframe. In your main component you load it and give it a update prop which you ignore in you iframe component. If you set the prop with a state variable, your component will reload. You can take a look at Diogo Sgrillo's answer for the setState method.
You could also check with shouldcomponentupdate() if the prop changed and only update if it changed.
Maybe a bit late here :) Just a side note: When you update state based on previous state you should pass a function to setState:
resetIframe() {
this.setState(prevState => {random: prevState.random + 1});
}
Updating the source causes a reload without causing a rerender.
import React, { useState, useRef } from 'react'
function renderIframe({src}){
const iframeRef = useRef()
return <>
<iframe ref={iframeRef} src={src} >
<span onClick={e => (iframeRef.current.src += '')}>Reload</span
</>
}

Resources