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

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.

Related

How to useEffect in component that is rendered in another component

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!

Why does the Sign In With Google button disappear after I render it the second time?

I am using the Sign In With Google button from Google Identity. I have put the HTML from this button documentation page into a React component. Looks like this:
export default function GoogleLoginButton() {
return (
<>
<div
id="g_id_onload"
data-client_id="XXXXXX"
data-auto_prompt="false"
></div>
<div
className="g_id_signin"
data-type="standard"
data-size="large"
data-theme="outline"
data-text="sign_in_with"
data-shape="rectangular"
data-logo_alignment="left"
></div>
</>
);
}
On loading the page the first time the Google sign-in button appears correctly and I can log in. The sign-in button is then replaced by a log-out button. The problem is that when I click the log-out button which should render the Google sign-in button again, it doesn't reappear! Why is that?
I can add that refreshing the page after logging out brings back the Google button.
As Stian says, the google script injects the google button once the app renders. The problem is that if you re-render the component where your button is located it will disappear because the google script was already executed, and won't be executed in future re-renders (it won't inject the google button again).
A different solution is to call window.google.accounts.id.renderButton inside an useEffect, that useEffect should be placed inside the component that is re-rendered. This will re-inject the google button each time the useEffect is called (comoponent is re-rendered).
The first argument the renderButton method receives should be a ref to a div, the google script will inject the sign-in button inside that div.
NOTE: Remember to first initialize the google script calling google.accounts.id.initialize
Here's the example:
const divRef = useRef(null);
useEffect(() => {
if (divRef.current) {
window.google.accounts.id.initialize({
client_id: <YOUR_CLIENT_ID_GOES_HERE>,
callback: (res, error) => {
// This is the function that will be executed once the authentication with google is finished
},
});
window.google.accounts.id.renderButton(divRef.current, {
theme: 'filled_blue',
size: 'medium',
type: 'standard',
text: 'continue_with',
});
}
}, [divRef.current]);
return (
{/* Other stuff in your component... */}
{/* Google sign in button -> */} <div ref={divRef} />
);
The reason it doesn't work is because the accompanying client library doesn't run again on later renders.
On page load the client library runs and injects an iframe where the HTML is. On later renders one can see in the DOM that this doesn't happen; the HTML is present but no iframe.
One solution is to never remove the HTML from DOM. Instead, apply the style display: none to the sign-in button when in need of hiding it.
For TypeScript, I have modified Alex's answer a bit.
First don't forget to add the <script src="https://accounts.google.com/gsi/client" async defer></script> to the the index.html page found in the react public folder.
Create a google_sso.ts file and use this code:
import { useRef, useEffect } from 'react';
declare const google: any;
const GoogleSSO = () => {
const g_sso = useRef(null);
useEffect(() => {
if (g_sso.current) {
google.accounts.id.initialize({
client_id: "xxxxxxxx-koik0niqorls18sc92nburjfgbe2p056.apps.googleusercontent.com",
callback: (res: any, error: any) => {
// This is the function that will be executed once the authentication with google is finished
},
});
google.accounts.id.renderButton(g_sso.current, {
theme: 'outline',
size: 'large',
type: 'standard',
text: 'signin_with',
shape: 'rectangular',
logo_alignment: 'left',
width: '220',
});
}
}, [g_sso.current]);
return (<div ref={g_sso} />);
}
export default GoogleSSO
Then wherever you need the button, use this:
import GoogleSSO from "../common/google_sso";
<GoogleSSO />

How to get browser information for react components in nextjs?

I am writing a react component in nextjs that needs some information about the browser environment before it can be rendered. I know I can get this information from the user agent string after page load with a useEffect like so...
const MyComponent = () => {
const [browser, setBrowser] = useState(null);
useEffect(() => {
const ua = utils.getBrowserFromUA()
setBrowser(ua)
});
if (!browser) {
return <div>Loading...</div>
}
return (
<div>
<SpecialComponent browser={browser} />
</div>
);
};
Is it possible to get these values before the react component renders, before the pageLoad event perhaps? I really only need to calculate these values once and potentially share the values with other components. I can't find anything in the docs apart from running scripts with the beforeInteractive flag but I'm not sure how I would get the data to my component.

React Ant Design Modal Method update on state change

I'm currently migrating to antd, and have a modal appear on a certain route (ie /userid/info). I'm able to achieve this if I use the antd Modal react component, but I'd like to be able to use the modal methods provided such as Modal.confirm,Modal.info and Modal.error as they offer nicer ui straight out of the box.
I'm running to multiple issues such as having the modal rendered multiple times (both initially and after pressing delete in the delete user case), and unable to make it change due to state (ie display loading bar until data arrives). This is what i've tried but it constantly renders new modals, ive tried something else but that never changed out of displaying <Loader /> even though isFetching was false. I'm not sure what else to try.
const UserInfoFC: React.FC<Props> = (props) => {
const user = props.user.id;
const [isFetching, setIsFetching] = React.useState<boolean>(true);
const [userInfo, setUserInfo] = React.useState<string>('');
const modal = Modal.info({
content: <Loader />,
title: 'User Info',
});
const displayModal = () => {
const renderInfo = (
<React.Fragment>
<p>display user.info</p>
</React.Fragment>
);
const fetchInfo = async () => {
try {
user = // some api calls
setUserInfo(user.info);
modal.update({ content: renderInfo })
} catch (error) {
// todo
}
setIsFetching(false);
};
fetchInfo();
};
displayModal();
return(<div />);
};
reference: https://ant.design/components/modal/#components-modal-demo-confirm
edit: here is a replication of one of the issues I face: https://codesandbox.io/embed/antd-reproduction-template-1jsy8
As mentioned in my comment, you can use a useEffect hook with an empty dependency array to run a function once when the component mounts. You can initiate an async call, wait for it to resolve and store the data in your state, and launch a modal with a second hook once the data arrives.
I made a sandbox here
Instead of going to /:id/info and routing to a component which would have returned an empty div but displayed a modal, I created a displayInfo component that displays a button and that controls the modal. I got rid of attempting to use routes for this.
What I have now is similar to the docs

react-router redirect to a different domain url

I am using react-router for client side routing. I have a button and when some one clicks the button, I want to redirect the user to a different url.
For e.g I want to redirect the user to "http://www.google.com". I used navigation mixin and used this.transitionTo("https://www.google.com"). But when I do this I get this error
Invariant Violation: Cannot find a route named "https://www.google.com".
I can use window.location but is that the right way to go?
As pointed out in the comments to this answer, default way of solving this would be to use anchor element (the a tag) with href attribute that points at the destination URL that you'd like to route the user to. A button that has appearance of a button but behavior or an anchor is pretty much a web anti-pattern. See more info in this answer: https://stackoverflow.com/a/1667512/1460905.
That said, there certainly is a potential scenario when a web app needs to perform some action and only then redirect the user. In this case, if primary action the user takes is submitting some data or really performing an action, and redirect is more of a side-effect, then the original question is valid.
In this case, why not use location property of window object? It even provides a nice functional method to go to external location. See the ref.
So, if you have a component, say
class Button extends Component {
render() {
return (
<button onClick={this.handleClick.bind(this)} />
);
}
}
then add handleClick that would make the component look like
class Button extends Component {
handleClick() {
// do something meaningful, Promises, if/else, whatever, and then
window.location.assign('http://github.com');
}
render() {
return (
<button onClick={this.handleClick.bind(this)} />
);
}
}
No need to import window since it's global. Should work perfectly in any modern browser.
Also, if you have a component that is declared as a function, you may possibly use the effect hook to change location when state changes, like
const Button = () => {
const [clicked, setClicked] = useState(false);
useEffect(() => {
if (clicked) {
// do something meaningful, Promises, if/else, whatever, and then
window.location.assign('http://github.com');
}
});
return (
<button onClick={() => setClicked(true)}></button>
);
};
You don't need react-router for external links, you can use regular link elements (i.e. <a href="..."/>) just fine.
You only need react-router when you have internal navigation (i.e. from component to component) for which the browser's URL bar should make it look like your app is actually switching "real" URLs.
Edit because people seem to think you can't use an <a href="..." if you need to "do work first", an example of doing exactly that:
render() {
return <a href={settings.externalLocation} onClick={evt => this.leave(evt)}/>
}
async leave(evt) {
if (this.state.finalized) return;
evt.preventDefault();
// Do whatever you need to do, but do it quickly, meaning that if you need to do
// various things, do them all in parallel instead of running them one by one:
await Promise.all([
utils.doAllTheMetrics(),
user.logOutUser(),
store.cleanUp(),
somelib.whatever(),
]);
// done, let's leave.
this.setState({ finalized: true }), () => evt.target.click());
}
And that's it: when you click the link (that you styled to look like a button because that's what CSS is for) React checks if it can safely navigate away as a state check.
If it can, it lets that happen.
If it can't:
it prevents the navigation of occurring via preventDefault(),
does whatever work it needs to do, and then
marks itself as "it is safe to leave now", then retriggers the link.
You can try and create a link element and click it from code. This work for me
const navigateUrl = (url) => {
let element = document.createElement('a');
if(url.startsWith('http://') || url.startsWith('https://')){
element.href = url;
} else{
element.href = 'http://' + url;
}
element.click();
}
As pointed by #Mike 'Pomax' Kamermans, you can just use to navigate to external link.
I usually do it this way, with is-internal-link
import React from 'react'
import { Link as ReactRouterLink} from 'react-router-dom'
import { isInternalLink } from 'is-internal-link'
const Link = ({ children, to, activeClassName, ...other }) => {
if (isInternalLink(to)) {
return (
<ReactRouterLink to={to} activeClassName={activeClassName} {...other}>
{children}
</ReactRouterLink>
)
}
return (
<a href={to} target="_blank" {...other}>
{children}
</a>
)
}
export default Link
Disclaimer: I am the author of this is-internal-link
I had the same issue and my research into the issue uncovered that I could simply use an "a href" tag. If using target="_blank" you should write your link this...
Your Link
I couldn't find a simple way to do that with React Router. As #Mike wrote you should use anchor (<a> tags) when sending the user to external site.
I created a custom <Link> component to dynamically decide whether to render a React-Router <Link> or regular <a> tag.
import * as React from "react";
import {Link, LinkProps} from "react-router-dom";
const ReloadableLink = (props: LinkProps & { forceReload?: boolean }) => {
const {forceReload, ...linkProps} = props;
if (forceReload)
return <a {...linkProps} href={String(props.to)}/>;
else
return <Link {...linkProps}>
{props.children}
</Link>
};
export default ReloadableLink;

Resources