How to use components that use `window` reference in React with server-side rendering? - node.js

An example of such component is Pikaday (react-pikaday) that contains in its code this line
hasEventListeners = !!window.addEventListener
but window is not defined on server so when I run the code it throws an error ReferenceError: window is not defined.
I tried to solve this issue by conditionally import Pikady component, something along these lines:
let Pikaday;
if (typeof window !== 'undefined' && window.document && window.document.createElement) {
import('./Pikaday').then(P => {
Pikaday = P.default;
});
}
In such a case it doesn't throw an error because the component is not loaded but I presumed that the initial HTML sent from server would not contain this component and then it would somehow "switch" to JS code on client side and the component would load. But this doesn't happen. I'm new to server-side rendering in general so I'm getting a bit lost.
How can I solve this? I have the same problem with Leaflet library that also contains window in its code.

Do a simple check for window.
if (typeof window !== 'undefined') {
// write your logic here
}

I eventually used this fork of Pikaday that introduces several small changes to check for window availability:
https://github.com/everdimension/Pikaday
I don't think there was any other way than edit the plugin itself.

Related

Crypto.randomUUID() not working with Next.js v13

The Environment
Next.js v13 (stable, no ./app folder) is running with Node v18 in Ubuntu WSL.
As per de docs , the Crypto API has been around since Node v14~ish.
This has been indeed tested in Node, in my environment:
Node 18 importing and running crypto.randomUUID()
I also printed the whole object and it looks as the docs says it should.
The Problem
Imagine this simple component:
import crypto from 'crypto';
export default function Crypto() {
console.log(crypto);
return (
<p>
{crypto.randomUUID()}
</p>
);
}
Next.js says it "compiled client and server successfully in 397 ms". But after the UUID renders in the browser for a couple of milliseconds, Next.js throws a couple of errors revolving around randomUUID not being a function.
Next Runtime Error with crypto.randomUUID()
I see that Webpack is somewhat mingling in there; haven't tried Turbopack. It's beyond the scope of this issue.
After commenting out the method invocation within the paragraph, the console.log(crypto) runs and prints twice, as usual, in the devtools as follows:
crypto method printed
Notice how one comes from "react devetools backend" and the other one from webkpack. That leads me to believe the error gets thrown server-side as the console.loglog is invoked before the UUID method.
Server-side, despite the errors thrown in the browser, the object gets printed by the Next CLI and it contains the method: Next CLI prints crypto object and randomUUID is listed
Client-side, within the printed object, the method randomUUID() is nowhere to be found:
Inside printed crypto object in devtools
This confirms the error message. My code is not getting access to the method. Also, a couple of methods are missing, when compared to the Node docs.
And yet if one console.log(crypto) directly from the devTools, it has indeed the method within its prototype:
randomUUID directly from devtools
Furthermore, because of the structure, I'm inclined to believe the crypto object being printed is somehow coming from Node, as the structure of the Chrome V8 crypto object is completely different. But why in the hell are those methods missing?
I tried console.loging the object server-side, client-side, and in-between. Somehow the method gets lost in-between. Webpack might be the culprit. Worst of all, albeit being for the blink of an eye, I can see the string rendered before the errors get thrown; and dismissing the error cards throws a blank body. The string disappears.
EDIT
The reason one imports/requires crypto is so it can run in Node. Next is a SSR framework; in a nutshell it is intended to run first on the server, get rendered and delivered as HTML as much as it can to the client. If not imported, Node throws an error when Next tries to invoke Crypto server-side.
Now then, I tell that piece of code to only run if the Window object is available (i.e. I'm in the Browser) and it runs with the native chromium V8 Crypto object.
// import crypto from 'crypto';
export default function Crypto() {
if (typeof window !== 'undefined') {
console.log('CLIENT: ', crypto.randomUUID());
return (
<p>
{crypto.randomUUID()}
</p>
);
}
return (
<h1>SERVER SIDE</h1>
);
}
The only downside is that is somehow still runs twice bc of Next magic, once server side and one client-side, which means it's not bc of React 18. It tells me accurately that which is to be expected as an UUID function always returns a different result.
Browsers restrict access to some crypto APIs when not running in a secure context (as defined here).
Set it to state in a useEffect hook when page initially loads so it persist and then render it from state.
const Crypto = () => {
const [randomUUID, setRandomUUID] = useState();
useEffect((
if (typeof window !== 'undefined' && !randomUUID) {
setRandomUUID(crypto.randomUUID());
}
),[]);
if(!randomUUID) <>No UUID</>;
return <>{randomUUID}</>
}
export default Crypto;

Can I prevent node_modules bundled with webpack from using window.postMessage?

For security reasons, I need to disallow 3rd party modules which are included in my bundle by webpack from using window.postMessage to communicate with other processes in my Electron app.
Is that possible?
A standard trick could help here: simply move the method to another variable on window, like this maybe:
window._postMessage = window.postMessage;
window.postMessage = () => {};
Run this first on your render script or with a <script> tag on your html and plugins that would use that can't send events anymore (in fact, they don't crash but never get a response).
Edit
If you want to make sure that only authorized user can use it, something like this could work:
function createSecurePostMessage() {
const _postMessage = window.postMessage;
return {
get() {return function(...args) {
if (isAuthorized(...args) {
_postMessage(...args);
}
}
}
}
window.postMessage = createSecurePostMessage().get();
Now the original postMessage is inside the function and inaccessible and you can implement a method, for example, that checks if a certain message can be sent. If someone calls create again, then the secured postMethod will just be 'secured' again.

#material material-component-web Invalid tab component given as activeTab

firstly let my say that the mdc documentation is difficult for non-pros like me.
I'm using Elixir Phoenix and Brunch.
I import and everything is fine.
import {MDCTab, MDCTabFoundation} from '#material/tabs'; import
{MDCTabBar, MDCTabBarFoundation} from '#material/tabs'; import
{MDCTabBarScroller, MDCTabBarScrollerFoundation} from
'#material/tabs';
I manually instantiate the tab bar in a separate function that I export
export var Tabbable = {
run: function(MDCTabBar, el){
var myDynamicTabBar = window.myDynamicTabBar = new MDCTabBar(document.querySelector('#' + el));
Which is following the documentation like this
const tabBar = new MDCTabBar(document.querySelector('#my-mdc-tab-bar'));
but is slightly different to the documentation's use of the tab bar in their code snippet
var dynamicTabBar = window.dynamicTabBar = new mdc.tabs.MDCTabBar(document.querySelector('#dynamic-tab-bar'));
But, whenever I try to use mdc I get a 'not defined' error. Therefore, I'm not using it :-)
Now, when the user clicks the tab bar I capture that like this:
myDynamicTabBar.listen('MDCTabBar:change', function ({detail: tabs}) {
var nthChildIndex = tabs.activeTabIndex;
updatePanel(nthChildIndex);
});
The subtle difference is that my myDynamicTabBar is MDCTabBar but the documentation's dynamicTabBar is mdc.tabs.MDCTabBar
My tab control works, but it throws an error only visible in the console:
Uncaught Error: Invalid tab component given as activeTab: Tab not
found within this component's tab list
which is likely because I'm not using mdc.tabs? The documentation notes the change event happens on the MDCTabBar.
Therefore, how do I get rid of this annoying error in the console?
And why can I not access the global mdc? I have tried this in my Brunch file
globals: { mdc: "#material"}
But no good.
I'm right behind you on this! I'm frustrated with the docs too :(
You answered your own question in this Elixir thread which is very informative.
I found the real solution in this thread https://github.com/hyperapp/hyperapp/issues/546
MDCTabBar automatically initiates its children. So initiating tabs will result in that error.
The fix is to just initiate MDCTabBar

Error `window not defined` in Node.js

I know window doesn't exist in Node.js, but I'm using React and the same code on both client and server. Any method I use to check if window exists nets me:
Uncaught ReferenceError: window is not defined
How do I get around the fact that I can't do window && window.scroll(0, 0)?
Sawtaytoes has got it. I would run whatever code you have in componentDidMount() and surround it with:
if (typeof(window) !== 'undefined') {
// code here
}
If the window object is still not being created by the time React renders the component, you can always run your code a fraction of a second after the component renders (and the window object has definitely been created by then) so the user can't tell the difference.
if (typeof(window) !== 'undefined') {
var timer = setTimeout(function() {
// code here
}, 200);
}
I would advise against putting state in the setTimeout.
This will settle that issue for you:
typeof(window) === 'undefined'
Even if a variable isn't defined, you can use typeof() to check for it.
This kind of code shouldn't even be running on the server, it should be inside some componentDidMount (see doc) hook, which is only invoke client side. This is because it doesn't make sense to scroll the window server side.
However, if you have to reference to window in a part of your code that really runs both client and server, use global instead (which represents the global scope - e.g. window on the client).
This is a little older but for ES6 style react component classes you can use this class decorator I created as a drop in solution for defining components that should only render on the client side. I like it better than dropping window checks in everywhere.
import { clientOnly } from 'client-component';
#clientOnly
class ComponentThatAccessesWindowThatIsNotSafeForServerRendering extends Component {
render() {
const currentLocation = window.location;
return (
<div>{currentLocation}</div>
)
};
}
https://github.com/peterlazzarino/client-component
<Router onUpdate={() => window.scrollTo(0, 0)} history= {browserHistory}>
if you need to open new page on top in React JS app, use this code in router.js
Move the window and related code to the mounted() lifecycle hook. This is because mounted() hook is called on the client side only and window is available there.

Re-inject content scripts after update

I have a chrome extension which injects an iframe into every open tab. I have a chrome.runtime.onInstalled listener in my background.js which manually injects the required scripts as follows (Details of the API here : http://developer.chrome.com/extensions/runtime.html#event-onInstalled ) :
background.js
var injectIframeInAllTabs = function(){
console.log("reinject content scripts into all tabs");
var manifest = chrome.app.getDetails();
chrome.windows.getAll({},function(windows){
for( var win in windows ){
chrome.tabs.getAllInWindow(win.id, function reloadTabs(tabs) {
for (var i in tabs) {
var scripts = manifest.content_scripts[0].js;
console.log("content scripts ", scripts);
var k = 0, s = scripts.length;
for( ; k < s; k++ ) {
chrome.tabs.executeScript(tabs[i].id, {
file: scripts[k]
});
}
}
});
}
});
};
This works fine when I first install the extension. I want to do the same when my extension is updated. If I run the same script on update as well, I do not see a new iframe injected. Not only that, if I try to send a message to my content script AFTER the update, none of the messages go through to the content script. I have seen other people also running into the same issue on SO (Chrome: message content-script on runtime.onInstalled). What is the correct way of removing old content scripts and injecting new ones after chrome extension update?
When the extension is updated Chrome automatically cuts off all the "old" content scripts from talking to the background page and they also throw an exception if the old content script does try to communicate with the runtime. This was the missing piece for me. All I did was, in chrome.runtime.onInstalled in bg.js, I call the same method as posted in the question. That injects another iframe that talks to the correct runtime. At some point in time, the old content scripts tries to talk to the runtime which fails. I catch that exception and just wipeout the old content script. Also note that, each iframe gets injected into its own "isolated world" (Isolated world is explained here: http://www.youtube.com/watch?v=laLudeUmXHM) hence newly injected iframe cannot clear out the old lingering iframe.
Hope this helps someone in future!
There is no way to "remove" old content scripts (Apart from reloading the page in question using window.location.reload, which would be bad)
If you want to be more flexible about what code you execute in your content script, use the "code" parameter in the executeScript function, that lets you pass in a raw string with javascript code. If your content script is just one big function (i.e. content_script_function) which lives in background.js
in background.js:
function content_script_function(relevant_background_script_info) {
// this function will be serialized as a string using .toString()
// and will be called in the context of the content script page
// do your content script stuff here...
}
function execute_script_in_content_page(info) {
chrome.tabs.executeScript(tabid,
{code: "(" + content_script_function.toString() + ")(" +
JSON.stringify(info) + ");"});
}
chrome.tabs.onUpdated.addListener(
execute_script_in_content_page.bind( { reason: 'onUpdated',
otherinfo: chrome.app.getDetails() });
chrome.runtime.onInstalled.addListener(
execute_script_in_content_page.bind( { reason: 'onInstalled',
otherinfo: chrome.app.getDetails() });
)
Where relevant_background_script_info contains information about the background page, i.e. which version it is, whether there was an upgrade event, and why the function is being called. The content script page still maintains all its relevant state. This way you have full control over how to handle an "upgrade" event.

Resources