React Component env variables from project for imported component - reactjs

I have a Create React Project that has some local env variables for config.
I have a component that uses the env variables to configure itself. It imports the variables using
process.env.MY_VAR;
I wanted to move the component out into a npm package but retain the ability to use the process.env.MY_VAR. When I build and publish the package it will build it with the current MY_VAR and not the MY_VAR that is in my project I want to import it into. Is there a way I can do what I am after.
I have considered refactoring the component to take the env vars as props, but that's not something I really want to do.
Is there a way I can do what I am after.

How about treating your component library as a npm package that already is built fully/independent of build variables and ready to be consumed by npm clients?
Unless you want to use the library as some sort of internal package that naturally has a lot of coupling to the surrounding build environment, it is probably easier to embrace the classic npm approach. And it makes sense especially, if you want to publish the package. In your importing app project, you could still use process.env.MY_VAR. For the library part , I would suggest to hand in a configuration object.
If process.env.MY_VAR is important for the whole library containing multiple components, start with a package initialization step:
import { init } from "my-comp-library"
const config = {
myVar: process.env.MY_VAR
}
const lib = init(config)
const MyComp = lib.myComp()
const App = () => <MyComp />
If only relevant to a single component inside library, you could pass it as props directly:
const App = () => <MyComp myVar={process.env.MY_VAR} />
In app you could e.g. create a wrapper component MyAppComp that already includes its configuration in order to avoid redundant initialization logic for MyComp. Additional benefit here is that you would just have one place in app to change code, if library public API changes.
MyAppComp.js in app project:
import { MyComp } from "my-comp-library"
export const MyAppComp = props => <MyComp myVar={process.env.MY_VAR} {...props} />
Independent build management of your app and component project removes coupling, which makes them easier to maintain.

If you created project via create-react-app then you can use env-cmd npm package. If you have webpack then you can use dotenv package as well. In alternative way you may use below code to define the variable.
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development')
}),

Related

NextJS: Force dependency with outputStandalone option

With the new outputStandalone experimental feature (https://nextjs.org/docs/advanced-features/output-file-tracing#automatically-copying-traced-files-experimental) we can, after the build, have a standalone folder that contains the necessary dependencies, we simply copy it into our docker and don't need to rebuild inside the docker. It automatically detects the necessary dependencies in the source code UNLESS that dependency is only used in our package.json.
We use cross-env to start our Next app and of course this library is never imported in our source code, yet it needs to be present in the node_modules of the standalone folder.
So how can I force #vercel/nft to include a specific dependency ?
My case was that our dockerized Next.js project used Knex.js, where migrations were invoked with a npm script. Following this, some dependencies only required for knex.migrations() were never included in the standalone output.
Credits to sbstn, you can use unstable_includeFiles to create a list of dependencies, which should be included in the standalone output. This solution scans for .js and .json in a node_modules/ directory.
It should be highlighted that this setting is not available inside next.config.js, but as a page config prop, meaning it has to be exported inside a export const config = {} from a page different from _app.jsx and /api routes.
// pages/index.jsx
export default function Home(){
...
}
const requiredStandaloneDependencies = [
// some required deps that have not been included in standalone
"commander",
"debug",
"escalade",
"get-package-type",
"getopts",
"interpret",
"lodash",
"pg-connection-string",
"rechoir",
"resolve-from",
"tarn",
"tildify",
];
export const config = {
unstable_includeFiles: requiredStandaloneDependencies.map(
(dep) => `node_modules/${dep}/**/*.+(js|json)`
),
};
As a final thought for my case, where migrations are invoked with a custom npm script, npm run migrate:latest, I should also be able to circumvent the issue using programmatically invoked migrations. I'm just unsure as to how I add the method to the Next.js project tree to be run during runtime initialization.

Is it possible to use single-spa (import map + SystemJS) and include TypeScript declarations?

I've created a few apps that follow the same pattern as the react-microfrontend projects listed on the Single-spa examples page. However, these examples are not using TypeScript.
I am receiving a TS error when App2 tries to import a function that exists in App1, because App2 is unable to find the typings for the App1 object. And this makes sense to me, since the importmap.json that is referenced in the index.html file of the root-config project only has a URL listed for the .js file of each microfrontend. I really don't want to add #ts-ignore statements everywhere, and I also don't want to do cross-app communication without being type safe.
An example of what I'm referring to:
This file in the Planets app references the Button component from the Styleguide app. I am unable to build this unless I place a #ts-ignore on the line before the import statement, because my Planets app cannot find the type declarations for Styleguide.
I am very new to import maps and SystemJS in general, so if anyone could point me in the right direction, I'd appreciate it. I've actually gone through the trouble of building my Styleguide as a regular npm library and just including it as a dependency in the Planets package.json file... which kind of defeats the whole purpose of using Single-spa in the first place.
importmap.json
{
"imports": {
"#react-mf/planets": "//localhost:9000/react-mf-planets.js",
"#react-mf/styleguide": "//localhost:9001/react-mf-styleguide.js"
}
}
page.component.ts
// #ts-ignore
import { Button } from "#react-mf/styleguide";
...
render() {
return (
// Unable to view the available props
<Button label="Hello World" />
);
}
Unfortunately no, they cannot be shared via the import map because that operates in the browser, and browsers do not understand TypeScript syntax.
Your alternatives are:
Publish your module's types to an npm registry and npm install it into each dependent microfrontend
One caveat here: you will need to update this with every new publish of the shared module, but that can be automated with various tools such as renovate
Mock the utility module's types
More info can be found at this Github issue: https://github.com/single-spa/single-spa/issues/609#issuecomment-665132965

requestAnimationFrame is not defined it Next.js with React Native Web (Animated module)

I'm working on Next.js and React-Native-Web. I managed to run them together following the official Next.js example but when I'm trying to use the Animated package from the react-native it fails with Error that the requestAnimationFrame isn't defined. Basically this functionality does the node_modules package but I set the alias in webpack to translate all react-native requires to the react-native-web so even the node_modules package should use the react-native-web.
Any suggestions on how to solve it?
ReferenceError: requestAnimationFrame is not defined
at start (...node_modules\react-native-web\
dist\cjs\vendor\react-native\Animated\animations\TimingAnimation.js:104:11)
enter code here
Thanks for any help!
The problem is in the missed RequestAnimationFrame functionality at the server. This error happens when Next.js tries to render the component during SSR.
Unfortunately, there is no polyfill, etc. for such purpose so I just decided to use the Next.js dynamic imports for a Component that has animation functionality.
Next.js Official documentation
My own case оust to show how code looks:
import dynamic from 'next/dynamic';
const AutocompleteDropdown = dynamic(
() => import(
'myAwesomeLib/components/dropdown/autocomplete/AutocompleteDropdown'
),
{
ssr: false,
}
);
Now you can use the AutocompleteDropdown as the standard JSX component
I'm coding an App with React Native Web and NextJS 12, and in 2021 I encounter this problem and I fixed it, but now I know my fix was only for Next Dev, because it returned for Next Production Build.
Solution details:
No Dynamic import (which is useful too, but can be annoying when having lot of components using it)
Using RAF polyfill and Webpack ProvidePlugin.
Main thing to have in mind is that next.config.js with webpack 5 is going to check the codes first before even reach next entry points _documents.js and _app.js. It means that, you can put polyfill in those entry point files, it will still raise error of RAF undefined. You have to make requestAnimationFrame ready for config check.
DEV approach that will work on Next DEV only. Install RAF package https://www.npmjs.com/package/raf and In next.config.js add codes:
const raf = require('raf');
raf.polyfill();
This will add requestAnimationFrame and cancelAnimationFrame function to global and window object if they don't have it. In our case, it would add it in global for NodeJS.
But this solution won't work when executing npm run dev. I don't know why, if anyone knows why Next or Webpack 5 act differently from DEV to PRODUCTION, let me know.
Complete Solution:
Use ProvidePlugin config of webpack 5 https://webpack.js.org/plugins/provide-plugin/ . Create a file to use as modules, let's say: raf.js in root project or anywhere you want:
const raf = require('raf');
const polys = {};
raf.polyfill(polys);
module.exports = polys.requestAnimationFrame;
And in next.config.js use it inside webpack: () = {} like:
webpack: (config, options) => {
// console.log('fallback', config.resolve.fallback);
if (options.isServer) {
// provide plugin
config.plugins.push(
new options.webpack.ProvidePlugin({
requestAnimationFrame: path.resolve(__dirname, './raf.js'),
}),
);
}
And now, it's up to you to adapt to your existing config logic. By doing this, in Production Build, NextJS is injecting the requestAnimationFrame function in Server Side everywhere a module is using it.

Build once and deploy build file in multiple environments with minimal changes React and Webpack

In webpack.config file declared a variable to read in application.
let BASEURL = "http://127.0.0.1:8090";
with this approach I am not able to update BASEURL after npm run build. Every time I want to generate a new build for each environment if BASEURL changes.
Is there any way to build once and deploy build file in multiple environments with minimal changes?
Tl;dr: use AJAX and have the config either in react context or in a global variable.
Detailed answer:
It is indeed as you state, after the application is built with npm run build, the environment variables become hardwired and cannot be changed.
The official statement of create-react-app is it does not support the build once deploy many principle. From https://create-react-app.dev/docs/adding-custom-environment-variables/ :
The environment variables are embedded during the build time. Since Create React App produces a static HTML/CSS/JS bundle, it can’t possibly read them at runtime.
However, there are ways of achieving the principle, just a bit more complicated. The idea is, you need to get the value of a variable at runtime from an external source, e.g. AJAX. In more details, possible solutions may be (but are not limited to) following:
1. server-side placeholder replacement
This is a solution proposed by create-react-app in https://create-react-app.dev/docs/title-and-meta-tags/#injecting-data-from-the-server-into-the-page, introducing a custom placeholder and replacing it with the data on the server, before it is rendered to clients.
<!doctype html>
<html lang="en">
<head>
<script>
window.SERVER_DATA = __SERVER_DATA__;
</script>
While this works, it introduces a major overhead, because it leaves the whole backend implementation up to you. Depending on your tech stack, this may be very easy or also very complicated to implement.
2. a dynamic <script> that assigns variable values
A solution proposed in https://www.cotyhamilton.com/build-once-deploy-anywhere-for-react-applications/ utilizes the dynamic nature of javascript. In the dynamically downloaded config.js file, a value is assigned to the variable. In the rest of the React code, the variable is read and used. You can change the config.js file any time, without the need of recompiling the react app.
// public/config.js
const apiUrl = 'localhost:1337';
const env = 'development';
<!-- public/index.html -->
<script src="%PUBLIC_URL%/config.js"></script>
<script>
window.config = { apiUrl, env };
</script>
The main downside is that this does not support TypeScript, and your IDE or linter may complain that apiUrl and env are not defined. Especially in bigger projects, this approach may be hard to maintain.
3. dynamic config with AJAX, with TypeScript support
Based on the 2nd solution, this article https://profinit.eu/en/blog/build-once-deploy-many-in-react-dynamic-configuration-properties/ describes in a great detail how to best achieve the build once deploy many principle with create-react-app and what are pros and cons.
It proposes downloading the dynamic config as a JSON with AJAX. The main caveat is to make sure that the dynamic config is downloaded BEFORE some code tires to use it. In the context of React lifecycle, there are two ways of how to achieve this.
3.1 global variable
Download the dynamic config JSON from globalConfigUrl, store it in a global variable, and only then render the React app. Example in TypeScript:
// index.tsx:
import axios from "axios";
import React, {ReactElement} from "react";
import App from "./App";
import {globalConfig, globalConfigUrl} from "./configuration/config";
axios.get(globalConfigUrl)
.then((response) => {
globalConfig.config = response.data; // THIS IS THE IMPORTANT LINE
return <App />;
})
.catch(e => {
return <p style={{color: "red", textAlign: "center"}}>Error while fetching global config</p>;
})
.then((reactElement: ReactElement) => {
ReactDOM.render(
reactElement,
document.getElementById("root")
);
});
Full working example: https://codesandbox.io/s/build-once-deploy-many-global-config-object-dvpzr
3.2. React context
Wrap your <App> component with a react context provider containing the configuration (with undefined or some default value). Fetch the configuration first time App is rendered and then save its value to the context. React will take care of the rest and will propagate the value change!
Full working example: https://codesandbox.io/s/build-once-deploy-many-react-context-7lk7g
The basic idea is this. Check the article above / working example for all details:
// App.tsx
import {useConfig} from "./configuration/useConfig";
// ... in the method:
const { setConfig } = useConfig(); // the `useConfig` is a custom hook, wrapping a React context. See the full working example for all details
useEffect(() => {
axios
.get(dynamicConfigUrl)
.then((response) => {
setConfig(response.data);
})
}, [setConfig]);

Webpack: How can I combine two completely separate bundles using dynamic bundling

I have spent a lot of time looking into this, but to no avail. I am aware of how code splitting and dynamic bundling works in Webpack using the import promise API.
Howevr, my use case is that I have two completely separate bundles, generated separately using different webpack builds. To give you perspective, I am building React components and there is a requirement to dynamically load a react component into the page that has been compiled in a different process. Is this possible in react? I do have control over both webpack builds, so I can exclude dependencies, etc.
Update: I just looked at Vue.js, and how it allows developers to register Vue.js components and then reference them later in the code. I could potentially load my Vue.js component scripts before my page script. I'm trying to see if I can do something similar in React.
Did I understand you correctly: you have essentially got
a library of custom React components (built by Webpack build #1)
a React app that needs to use some (all) of these components (built by Webpack build #2, totally separate from #1)
?
If yes, then read on.
The "Is this possible in react?" question should instead be "Is this possible in Webpack?", and the answer is "Yes". The following is tested with Webpack 2, but should also work with v.1.
Let's call your projects Lib (your React component library) and App (the library consumer).
In the Lib project:
Create an entry point file, say index.js, that exports all the custom React components like this:
import {Button} from './button';
import {DatePicker} from './DatePicker';
import {TextBox} from './textBox';
export const MyComponentLib = {
Button,
DatePicker,
TextBox
};
Update webpack.config.js to make the project's bundle a UMD library (could also be 'var'), and set the entry point to the above index.js file. Doing so will make your library available via a global variable named MyComponentLib (the name comes from the export above) in the consuming app later on:
...
output: {
path: './dist',
filename: 'mylib.bundle.js',
libraryTarget: 'umd'
},
...
entry: './index.js',
...
On to the App project:
In the index.html file you will have two <script> tags: one for mylib.bundle.js (the output of the Lib project), and another for the bundle of the App project itself. You might have more bundles (app, vendor etc.), I'm just simplifying things here.
Update webpack.config.js to mark the component library as external dependency. Here, MyComponentLib is, again, the name of the global variable the library is available at, and myComponents is the name to use in import statements:
...
externals: {
myComponents: 'MyComponentLib'
},
...
Now, in App you can import a component like this:
import {DatePicker} from 'myComponents';
This will dynamically load DatePicker from the component library at run time via the global variable.
Bonus: if you use eslint, you don't want it to complain about missing modules that you know are external; add this to your .eslintrc:
...
"settings": {
"import/core-modules": ["myComponents"]
},
...

Resources