Conditionally import assets in create-react-app - javascript

Is it possible to conditionally import assets when creating a React app using create-react-app? I'm aware of the require syntax - example:
import React from "react";
const path = process.env.REACT_APP_TYPE === "app_1" ? "app_1" : "app_2";
const imagePath = require(`./assets/${path}/main.png`);
export default function Test() {
return (
<img src={imagePath} alt="" />
);
}
This however bundles all my assets no matter what.
It will load the proper image, but it will still bundle all the files together in the final build.
When I look in the dev tools for the final build, I can see all the assets there even though I only wanted to load the assets for app_1.
Am I forced to touch the webpack config, if so, what should I change? or is there another way?

In the days when React didn't exist we didn't put assets into our JS files. We let the CSS to decide, what assets to load for what selectors. Then you could simply switch a corresponding class on or off for a corresponding element (or even the whole page) and viola it changes color, background, or even a form. Pure magic!
Ah. What times these were!
All above is true and I do not understand why would anyone do or recommend doing it differently. However if you still want to do it (for any reason) - you can! Latest create-react-app comes with out-of-the-box support for lazy loading of arbitrary components via dynamic importing and code splitting. All you need to do is use parenthesized version of the import() statement instead of the regular one. import() takes in a request string as usual and returns a Promise. That's it. Source code of the dynamicaly requested component won't be bundled in, but instead stored in separate chunks to be loaded on demand.
Before:
import OtherComponent from './OtherComponent';
function MyComponent() {
return (
<div>
<OtherComponent />
</div>
);
}
After:
const OtherComponent = React.lazy(() => import('./OtherComponent'));
function MyComponent() {
return (
<div>
<OtherComponent />
</div>
);
}
Notice how function MyComponent part is identical.
For those wondering if it is tied to CRA or React, it's not. It's a generic concept that can be used in vanilla JavaScript.

You will need to use webpack (or other bundler.) The code is not being run when it's bundled, so the compiler has no way of knowing which branch of logic to follow (app_1 or app_2). Therefore you have to get into the bundler's logic in order to achieve your goal.
However, this isn't as scary as it seems since webpack has built in capability to do this (no 3rd parties required...)
I would look into using webpack.providePlugin
(https://webpack.js.org/plugins/provide-plugin)
or its sibling DefinePlugin
(https://webpack.js.org/plugins/define-plugin)
(I'm afraid these examples are off the top of my head, so it's very unlikely they'll work on first pass.)
Examples:
Both will require a provider module...
// in path/provider.js
module.exports = {
live: '/path/to/live/image',
dev: '/path/to/dev/image'
}
Provide Plugin Example
// in webpack
new webpack.ProvidePlugin({
imagePath: [
'path/provider', // the file defined above
process.env.ENVIRONMENT // either 'dev' or 'live'
]
}),
// in code
export default function Test() {
return (
<img src={imagePath} alt="" />
);
}
Define Plugin example:
// in webpack
new webpack.DefinePlugin({
'process.env.ENVIRONMENT': JSON.stringify(process.env.ENVIRONMENT)
});
// in code
var providers = require('path/provider'); // same path provider as above
export default function Test() {
return (
<img src={providers[process.env.ENVIRONMENT]} alt="" />
);
}
In both cases the bundler is forced to collapse your variable to an actual literal value at compile time - before bundling has taken place. Since you have now collapsed the logical path down to a single option, it is now free to only bundle the relevant assets.

You can't do this with default CRA settings.
Because if your dynamic require or dynamic import path is not static, webpack won't be able to determine which assets to include in the final build folder, therefore, it will grab everything from your ./src folder, and put them all to your build folder.

There is a way to do it with default CRA settings
You can add to .env something like
REACT_APP_SKIN=1
Put your skin assets in public/css1, public/css2 etc. And include them in public/index.html using code like
<link href="/css%REACT_APP_SKIN%/theme.css" rel="stylesheet">

Related

Can't resolve 'fs' in \node_modules\electron

I am using the electron-react boilerplate and want to use an electron dialog in App.tsx:
const { dialog } = require('#electron/remote') //also tried with import
const Hello = () => {
const readFromFile = async () => {
dialog.showOpenDialog({})
}
return (
<>
<button onClick={() => readFromFile()} >Test</button>
</>
)
}
in main.ts I placed the following line at the top
require('#electron/remote/main').initialize()
In the end I always get this error:
Module not found: Error: Can't resolve 'fs' in 'C:\Users\myUsername\source\repos\electronTest\node_modules\electron'
I also tried nodeIntegration: true and contextIsolation: false
Just spent a while on this and <rant about JS community>... eh. Maybe this will help others.
tl;dr
You can only use electron's imports in electron-main side.
longer story
Electron is split into electron-main and electron-renderer. As others suggested, you need to update webpack's config with the target pointing at the correct electron.
In case you're using one of the electron builder boilerplates, you might see directory .erb with a few webpack configs. Especially, there might be one with target = "electron-main" and another with target = ["web", "electron-renderer"]. So it feels like mission accomplished; however, according to webpack's doc on target, if you pass a list, then a common config is set. Since web doesn't include fs, the common part also won't include fs.
For the reason above, some of electron imports, e.g. clipboard, can be only used from the "electron-main" side of your application.
The work-around, e.g. using clipboard on the application side, is to use IPC to communicate between main and renderer sides.
Check your webpack.config.js. Looks like you target is not electron-main or electron-renderer.

How to export static images on Nextjs?

when I want to export my nextjs app, it says that I cannot export my images on static websites.
Error: Image Optimization using Next.js' default loader is not compatible with next export.
Possible solutions:
- Use next start to run a server, which includes the Image Optimization API.
- Use any provider which supports Image Optimization (like Vercel).
- Configure a third-party loader in next.config.js.
- Use the loader prop for next/image.
How can I make it so that it does ?
Is there a way for me to simply tell it to render images statically ? I dont want to go throught other onlines images loaders..
I created a npm module so that we can use the Next.js <Image/> component with optimized images while using the static export functionality.
https://www.npmjs.com/package/next-image-export-optimizer
The library wraps the <Image /> component of Next.js and automatically creates optimized images using sharp.js at export time.
It uses a custom loader to create a srcset for the <img /> that the <Image /> component of Next.js creates. Then at build/export time, the images inside the public/images folder (as an example) are optimized with sharp.js and copied into the build folder.
You need to set up a custom image loader in Next.js
In your next.config.js file, add this property to the export:
images: {
loader: "custom"
}
And make a script called loader.js that exports this:
function imageLoader({ src }) {
return `/images/${src}`; // REPLACE WITH YOUR IMAGE DIRECTORY
}
module.exports = imageLoader;
For each Image component, set the loader prop manually:
const imageLoader = require("PATH TO loader.js");
<Image loader={imageLoader} />
I'm going to add onto skara9's answer because it didn't quite work for me. I found a thread on github discussing it and the answer there worked for me. It just wraps around the NextJS image component and works pretty flawlessly for me.
// components/Image.js
import NextImage from "next/image";
// opt-out of image optimization, no-op
const customLoader = ({ src }) => {
return src
}
export default function Image(props) {
return (
<NextImage
{...props}
loader={customLoader}
/>
);
}
Make sure you change your imports and update your next.config.js
import Image from '../components/Image.js'

How to add custom scripts bundle in NextJS

I have some legacy custom javascripts that I need to bundle and put them in _document.js as a link. The filename should include a hash.
What would be the best way to accomplish this?
I tried webpack configs regarding entry/output but they break NextJs build.
The problem is that we use things like window, document, etc that do crash in server side.
Ideally what is needed is to inject this into a tag, as compiled / babelified javascript code.
What I tried is
Webpack HTML Plugin plus other plugins like InlineChunk or
InlineSource plugins. They didn't work because they generate code in
an index.html that is not used by NextJS.
Using Raw Loader to get the file content. Doesn't work because it is
not babelified.
Adding a custom entry to the Webpack config, like scripts:
'path/to/my-entry.js'. Didn't work because it adds a hash name to the
file and I have no way of knowing it.
Adding a custom entry into the NextJs polyfills. I thought it made
sense, but the polyfill tag has a nomodule which prevents its code to
run on new browsers.
Another options is to add the javascript code as a string, and then using __dangerouslySetInnerHtml but the problem is that I lose linter and babel abilities there.
I tried adding it as a page, but crashes for local development and even on build
webpack.config.js
module.exports = (nextConfig = {}) =>
Object.assign({}, nextConfig, {
webpack(config, options) {
const nextJsEntries = config.entry;
config.entry = async () => {
const entries = await nextJsEntries();
entries['pages/rscripts'] = 'test/test.js';
return entries;
};
...
Then in _document.js
<script src={`${publicRuntimeConfig.ASSET_PREFIX}/_next/${this.props.buildManifest.pages['/rscripts'][2]}`} />
You can just import js file like import 'path/to/js_file' in your _app.js/app.tsx file
import "../styles/globals.css"
import "../js/test"
function MyApp({ Component, pageProps }) {
return <Component {...pageProps} />
}
export default MyApp
This one works fine for me
I wanted to add another answer here as I came across this and I believe some things have changed in Next JS. Next now has this script component that you can use to load external scripts or dangerously set a script.
The Next.js Script component, next/script, is an extension of the HTML
element. It enables developers to set the loading priority of
third-party scripts anywhere in their application, outside next/head,
saving developer time while improving loading performance.
The cool thing is you can put them into whatever pages you want, maybe you have a script you want on a homepage, but not other pages, and Next will extract them and place them on the page based on the strategy you select. There are a few gotchas, can't load in the head, beforeInteractive is a little finicky, so I would read the link above and the actual API reference before making any choices.
import { useEffect } from 'react';
import Script from 'next/script';
function thirdPartyScript() {
useEffect(() => {
// just for fun. This actually fires
// before the onLoad callback
}, []);
return (
<Script
id="test-script"
strategy="afterInteractive"
src="/public/pages/scripts/test.js"
onLoad={() => {
console.log('Onload fires as you would expect');
}}
/>
);
}

How to import a module from the static using dynamic import of es6?

I'm trying to add dynamic import into my code to have a better performance on the client-side. So I have a webpack config where is bundling js files. On SFCC the bundled files are in the static folder where the path to that files is something like this: /en/v1569517927607/js/app.js)
I have a function where I'm using dynamic import of es6 to call a module when the user clicks on a button. The problem is that when we call for that module, the browser doesn't find it because the path is wrong.
/en/lazyLoad.js net::ERR_ABORTED 404 (Not Found)
This is normal because the file is on /en/v1569517927607/js/lazyLoad.js.
There is a way to get it from the right path? Here is my code.
window.onload = () => {
const lazyAlertBtn = document.querySelector("#lazyLoad");
lazyAlertBtn.addEventListener("click", () => {
import(/* webpackChunkName: "lazyLoad" */ '../modules/lazyLoad').then(module => {
module.lazyLoad();
});
});
};
I had the same problem and solved it using the Merchant Tools > SEO > Dynamic Mapping module in Business Manager.
There you can use a rule like the following to redirect the request to the static folder:
**/*.bundle.js i s,,,,,/js/{0}.bundle.js
All my chunk files are named with the <module>.bundle pattern.
Here you can find more info :
https://documentation.b2c.commercecloud.salesforce.com/DOC1/topic/com.demandware.dochelp/content/b2c_commerce/topics/search_engine_optimization/b2c_dynamic_mappings.html
Hope this helps.
I believe you'll likely need to do some path.resolve() magic in either your import statement or your webpack.config.js file as is shown in the accepted answer to this question: Set correct path to lazy-load component using Webpack - ES6
We did it in a different way. That required two steps
From within the template file add a script tag that creates a global variable for the static path. Something like
// inside .isml template
<script>
// help webpack know about the path of js scripts -> used for lazy loading
window.__staticPath__ = "${URLUtils.httpsStatic('/')}";
</script>
Then you need to instruct webpack to know where to find chunks by changing __webpack_public_path__ at runtime
// somewhere in your main .js file
// eslint-disable-next-line
__webpack_public_path__ = window.__staticPath__ + 'js/';
Optional step:
You might also want to remove code version from your __staticPath__ using replace (at least we had to do that)
__webpack_public_path__ = window.__staticPath__.replace('{YOUR_CODE_VERSION_GOES_HERE}', '') + 'js/';

Get asset path in Laravel from within React component?

I'm wondering what the best way is to get the asset path for things like images from inside a React component. I'm using Laravel, and the built-in asset() function that you can use in Blade files is exactly what I need, but I need it on the front-end in JS. Seems like Webpack territory, but I'm not sure what setting I need to modify.
The problem arises when my path is more than just the hostname, for example "localhost:3000/myproject/public" instead of just "localhost:3000".
Doing something like....
import myImage from '../../images/my-image.png'
...
<img src={myImage} />
results in a src path of "/images/my-image.png", which is not found, because the image actually resides at "/myproject/public/images/my-image.png"
In the past I've used the not-so-elegant solution of doing:
// react.blade.php
<body data-root-url={{ asset('') }}>
...
</body>
// helpers.js
export const rootUrl = document.querySelector('body').getAttribute('data-root-url')
While this hasn't gotten me into any trouble yet, I'm trying to find a better way, and I also started using Typescript recently and it (understandably) gets cranky about this because it technically could be null.
I've also tried the following in webpack.mix.js:
mix.webpackConfig({
output: {
publicPath: 'http://localhost:3000/myproject/public/',
},
})
but that doesn't seem to have any effect on image paths. Oddly enough though, this did work when running into a similar issue with code-splitting and looking for the JS chunks.
Any thoughts? Thanks!
I fix it by adding a new controller in the routes/web.php file.
Route::get('assets/{path}', function ($path) {
return response()->file(public_path("assets/$path"));
});
Then I added the images inside the folder public/assets/img/test.jpg and called it in my React component normally.
<Card sectioned>
<EmptyState image="/assets/img/test.jpg">
</EmptyState>
</Card>

Categories

Resources