Dinamically loading images with require in React has different behaviors - javascript

I have a scenario in which I want to dynamically tell a child component which image to show, based on an required image passed down by the parent, as such:
Parent:
function Social() {
return (
<SocialBar>
{
SocialMedias.map((media) =>{
return <SocialApp
key={uuidv4()}
link={media.link}
social={require(media.icon)}
/>
})
}
</SocialBar>
);
}
Child
function SocialApp(props) {
return (
<Social href={props.link} target="_blank">
<CustomSizedImage src={props.social.default} />
</Social>
);
}
Which seems pretty straight forward, right? However, this causes the error below:
I've seen some posts say that this is caused because the path is not absolute. However, I tested with absolute paths and it still doesn't work. The funny thing is, when I changed the parent code to the below for testing, it worked.
New parent code:
function Social() {
const img = require("../assets/icons/social-media/instagram.svg");
return (
<SocialBar>
{
SocialMedias.map((media) =>{
return <SocialApp
key={uuidv4()}
link={media.link}
social={img}
/>
})
}
</SocialBar>
);
}
export default Social;
The result:
OBS: I tested with EVERY image that this code will possibly load. Is not an image/image path related issue.
What is expected of all this is to dynamically load icons for social media with the correct URLs, etc. What I do not understand is: why does it work when I require the image outside the return?. I know I could make this work by iterating media object outside of return and use the resulting array with the map index, but that doesn't seem clean. Why does it have this behavior? Is there a way to make it work inside the map?

Your last code snippet works because the path is known at compile time.
I assume you use a bundler like Webpack or similar. When they compile your project, they check all imports (including require calls), use that to calculate the dependency tree, and finally bundle all required files together. For images, that includes "compiling it into a module" which just returns the path to the image.
Usually that also involves replacing paths with module identifiers. E.g. if you ever looked at the built Webpack code, you'll notice that it doesn't use e.g. require('./module') but __webpack_require.c(5) or something similar. It gave the module './module' the unique ID 5 in the bundled code.
With your earlier code snippets, when using a dynamic require with a non-constant string, Webpack just doesn't know that you'll request that image. Nor would it know the new unique ID to use for that.
The easiest solution is to import your images somewhere and use those references dynamically:
// images.ts
export IconA from './media/icon-a.svg';
export IconB from './media/icon-b.svg';
// somewhere else
import { IconA, IconB } from './images';
const iconPath = someCondition ? IconA : IconB;
Of course, you don't have to use the import/require mechanic. You can simply have your icon path be an actual path, relative to your public/ folder.

Related

String confusion in Javascript with regard to dynamically loading a path

I've encountered something very peculiar in Javascript, which I can't explain. Simply put, I'm building a dynamic Button component in React in which one can optionally pass in the path of an icon file.
This approach works perfectly:
const image = require('../styling/assets/images/search-icon.svg');
<button className='button' onClick={props.handleButtonClick}>
<img className='button-icon' src={image} />
<p>{props.label}</p>
</button>
But it fails if I change the first line to these two:
const path = '../styling/assets/images/search-icon.svg';
const image = require(path);
In this second case I get this error: Error: Cannot find module '../styling/assets/images/search-icon.svg'
Fundamentally the two approaches seem equivalent to me but apparently not.
Why does the 2nd approach fail?
According to the internet: "require is resolved during packaging not during runtime".
https://github.com/facebook/react-native/issues/6391
According with MDN docs:
The import statement cannot be used in embedded scripts unless such
script has a type="module". Bindings imported are called live bindings
because they are updated by the module that exported the binding.
More information here https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import

Functions included from the prototype in a javascript class are not seen in the editor's autocomplete

I have the following problem, I have created a library. which its methodos
they are in separate files writeLog.js in a folder called ./lib.
and I have an index.js file that explores that folder and extracts the function
and puts the same name as the file as the function name.
example:
writeLog.js
and then I can use
let jsPackTools require('./index');
let utils = new jsPackTools();
utils.writeLog('test');
The way I use to add the methods to the classes is through the prototype.
the folder is scanned with readdirSync () and then I take the file name to
place it inside a require ().
the code is the following:
fs.readdirSync(__dirname+'/lib').forEach(modules => {
let module = modules.split('.').shift();
jsPackTools.prototype[module] =require(\`${__dirname}/lib/${module}\`);
});
Everything works perfectly fine, the problem is that when I want to access my methods through the autocomplete of any code editor. Methods they seem to be not accessible. however everything works perfectly, I can do use of functions.
    
The problem is that I have to know each method that I am going to use and it cannot be visualized in the completed auto of any editor.
I have tried with:
const writeLog = require('./lib/writeLog');
class jsPackTools {
get writeLog() { return writeLog.bind(this) }
}
This allows indexing perfectly. however, I don't logo do it dynamically.
The idea is that the class does not know what its methods are until the ./lib folder is scanned and all methods are extracted. This way you can add functions inderteminately and when you use them you can see.
    
I do not want to use transpilators, I thought of using a d.ts but it is necessary to use typeScript but the intention is to create this library without dependencies or the least possible.

React | import images from URL-holding variables - why am I getting wrong paths?

I read a bit about answers but haven't found something that's what I'm looking for. Please refer me to a relevant question and delete my question if it's after all a duplicate.
I've got the following file structures:
components
icons
bitcoin_icon.png
ethereum_icon.png
...
CoinsList.js
CoinsTable.js
As seen in the code, I have in CoinsTable an array of objects called supportedCoins, whose elements are CryptoCoin objects, and those have the property iconURL which stores the path of images (which are icons).
As seen in the code, in Coinstable.js I have code that renders a table that displays all the elements of supportedCoins, each property separately, using their object properties (e.g supportedCoins['bitcoin'].symbol), but when trying to render the following for a CryptoCoin, for example, again with supportedCoins['bitcoin']:
<img src={supportedCoins['bitcoin'].iconURL, the rendered src is the actual path, but React doesn't render the picture.
That's what I want to do and what I want for to work.
What I found out that does work is doing the following to do in CoinsTable.js:
Import the icon from the right path, for example:
import biticonURL from './coins_icons/bitcoin_icon.png - same path as the one in supportedCoins['bitcoin'].iconURL`
Create an image as desired, only with src={biticonURL}:
`
To compare the different things I tried, here are the resulted srcs of supportedCoins['bitcoin']:
- With supportedCoins['bitcoin'].iconURL: ./coins_icons///bitcoin_icon.png
- With bitcoinIcon(import): /static/media/bitcoin_icon.b6168699.png
Why am I getting wrong paths, and why does store my pictures there? The images aren't stored on my server in those paths.
Why am I getting wrong paths with triple forward slashes?
You're getting the ./coins_icons///bitcoin_icon.png path because of how you concatenate strings in CoinsList.js. Here's the relevant code:
const coinsIconsDir = './coins_icons/';
supportedCoins['bitcoin'] = new CryptoCoin ('BTC', 'Bitcoin', coinsIconsDir + '/' + '/bitcoin_icon.png');
Notice that there is one forward slash at the end of coinsIconsDir, one as a single character, and one at the beginning of '/bitcoin_icon.png', which is why you're getting that triple slash. The simplest way to fix that is to just pick one consistent place for the forward slash, like the following for example:
const coinsIconsDir = './coins_icons';
supportedCoins['bitcoin'] = new CryptoCoin ('BTC', 'Bitcoin', `${coinsIconsDir}/bitcoin_icon.png`);
Now your path will be ./coins_icons/bitcoin_icon.png as desired.
Why are my pictures stored under the static/media/ path?
You're using a build and bundle system to run your app. That system is taking the resources you are importing and storing them under the static/media/ folder before running your web app. Based on the static/media/ path, I'm guessing that you're using create-react-app, which will take images and other static assets and place them in a static folder in the final build. That's described here in the documentation. The most relevant lines are
Webpack finds all relative module references in CSS (they start with ./) and replaces them with the final paths from the compiled bundle.
This ensures that when the project is built, Webpack will correctly move the images into the build folder, and provide us with correct paths.
So what does that mean for your case?
I'm going to give you a number of alternatives for importing images and you can pick the one you like best.
If you fix the forward slashes, you should be able to use:
<img src={supportedCoins['bitcoin'].iconURL} style={{maxHeight:'25px'}}/>
However, if ./coins_icons/bitcoin_icon.png is not publicly available on your server (which is probably the case if you're using create-react-app), this will not work.
In that case, you'll either need to put the image somewhere that's publicly available (in create-react-app that's the public folder), or continue to import them as you are doing. There are several different ways to import, including using the syntax you currently have:
import bitcoinIcon from './coins_icons/bitcoin_icon.png';
If you're using Webpack, you can also use require. Note that require needs at least some file path information, so you'll need to do the file path construction inside the require.
// Assuming supportedCoins['bitcoin'].iconName has been initialized to the icon's file name
// e.g., supportedCoins['bitcoin'].iconName === bitcoin_icon.png
<img src={require(`./coins_icons/${supportedCoins['bitcoin'].iconName}`)} style={{maxHeight:'25px'}}/>
However, requires outside of the top level
and dynamic requires can be considered bad practice. To avoid that you could change CoinsList.js to:
import bitcoinIcon from './coins_icons/bitcoin_icon.png';
import ethereumIcon from './coins_icons/ethereum_icon.png';
import dashIcon from './coins_icons/dash_icon.png';
import iotaIcon from './coins_icons/iota_icon.png';
var supportedCoins = new Array();
const coinsIconsDir = './coins_icons/';
supportedCoins['bitcoin'] = new CryptoCoin ('BTC', 'Bitcoin', bitcoinIcon);
supportedCoins['ethereum'] = new CryptoCoin('ETH', 'Ethereum', ethereumIcon);
supportedCoins['dash'] = new CryptoCoin('DASH', 'Dash', dashIcon);
supportedCoins['iota'] = new CryptoCoin('IOTA', 'IOTA', iotaIcon);
and then in CoinsTable.js do:
<img src={supportedCoins['bitcoin'].iconURL} style={{maxHeight:'25px'}}/>
I'd pick this last option because it keeps the coin definition contained inside one file and keeps all imports static.

JavaScript intercept module import

I have a SPA (in Aurelia / TypeScript but that should not matter) which uses SystemJS. Let's say it runs at http://spa:5000/app.
It sometimes loads JavaScript modules like waterservice/external.js on demand from an external URL like http://otherhost:5002/fetchmodule?moduleId=waterservice.external.js. I use SystemJS.import(url) to do this and it works fine.
But when this external module wants to import another module with a simple import { OtherClass } from './other-class'; this (comprehensiblely) does not work. When loaded by the SPA it looks at http://spa:5000/app/other-class.js. In this case I have to intercept the path/location to redirect it to http://otherhost:5002/fetchmodule?moduleId=other-class.js.
Note: The Typescript compilation for waterservice/external.ts works find because the typescript compiler can find ./other-class.ts easily. Obviously I cannot use an absolute URL for the import.
How can I intercept the module loading inside a module I am importing with SystemJS?
One approach I already tested is to add a mapping in the SystemJS configuration. If I import it like import { OtherClass } from 'other-class'; and add a mapping like "other-class": "http://otherhost:5002/fetchmodule?moduleId=other-class" it works. But if this approach is good, how can I add mapping dynamically at runtime?
Other approaches like a generic load url interception are welcome too.
Update
I tried to intercept SystemJS as suggest by artem like this
var systemLoader = SystemJS;
var defaultNormalize = systemLoader.normalize;
systemLoader.normalize = function(name, parentName) {
console.error("Intercepting", name, parentName);
return defaultNormalize(name, parentName);
}
This would normally not change anything but produce some console output to see what is going on. Unfortunately this seems to do change something as I get an error Uncaught (in promise) TypeError: this.has is not a function inside system.js.
Then I tried to add mappings with SystemJS.config({map: ...});. Surprisingly this function works incremental, so when I call it, it does not loose the already provided mappings. So I can do:
System.config({map: {
"other-class": `http://otherhost:5002/fetchModule?moduleId=other-class.js`
}});
This does not work with relative paths (those which start with . or ..) but if I put the shared ones in the root this works out.
I would still prefer to intercept the loading to be able to handle more scenarios but at the moment I have no idea which has function is missing in the above approach.
how can I add mapping dynamically at runtime?
AFAIK SystemJS can be configured at any time just by calling
SystemJS.config({ map: { additional-mappings-here ... }});
If it does not work for you, you can override loader.normalize and add your own mapping from module ids to URLs there. Something along these lines:
// assuming you have one global SystemJS instance
var loader = SystemJS;
var defaultNormalize = loader.normalize;
loader.normalize = function(name, parentName) {
if (parentName == 'your-external-module' && name == 'your-external-submodule') {
return Promise.resolve('your-submodule-url');
} else {
return defaultNormalize.call(loader, name, parentName);
}
}
I have no idea if this will work with typescript or not. Also, you will have to figure out what names exactly are passed to loader.normalize in your case.
Also, if you use systemjs builder to bundle your code, you will need to add that override to the loader used by builder (and that's whole another story).

Relative paths with fetch in Javascript

I was surprised by an experience with relative paths in JavaScript today. I’ve boiled down the situation to the following:
Suppose you have a directory structure like:
app/
|
+--app.html
+--js/
|
+--app.js
+--data.json
All my app.html does is run js/app.js
<!DOCTYPE html>
<title>app.html</title>
<body>
<script src=js/app.js></script>
</body>
app.js loads the JSON file and sticks it at the beginning of body:
// js/app.js
fetch('js/data.json') // <-- this path surprises me
.then(response => response.json())
.then(data => app.data = data)
The data is valid JSON, just a string:
"Hello World"
This is a pretty minimal usage of fetch, but I am surprised that the URL that I pass to fetch has to be relative to app.html instead of relative to app.js. I would expect this path to work, since data.json and app.js are in the same directory (js/):
fetch('data.json') // nope
Is there an explanation for why this is the case?
When you say fetch('data.json') you are effectively requesting http://yourdomain.com/data.json since it is relative to the page you're are making the request from. You should lead with forward slash, which will indicate that the path is relative to the domain root: fetch('/js/data.json'). Or fully qualify with your domain fetch('http://yourdomain.com/js/data.json').
An easy way to understand why it must be the case is to consider what should happen if we write a helper function in app/js/helper/logfetch.js:
// app/js/helper/logfetch.js
function logFetch(resource) {
console.log('Fetching', resource);
return fetch(resource);
}
Now, consider what happens if we use logFetch from app/js/app.js:
// app/js/app.js
fetch('data.json'); // if this is relative to js/, then ...
logFetch('data.json'); // should this be relative to js/ or js/helper/?
We might want these two calls to return the same thing - but if fetch is relative to the contained file, then logFetch would request js/helper/data.json instead of something consistent with fetch.
If fetch could sense where it is called from, then to implement helper libraries such as logFetch, the JavaScript would need a whole range of new caller-location-aware functionality.
In contrast, performing the fetch relative to the HTML file provides more consistency.
CSS works differently because it doesn't have the complexity of method calling: you can't create "helper CSS modules" that transform other CSS modules, so the idea of relative paths is a lot more conceptually cleaner.
This is not exactly a recommendation since it relies on a number of things that aren't guaranteed to work everywhere or to continue to work into the future, however it works for me in the places I need it to and it might help you.
const getRunningScript = () => {
return decodeURI(new Error().stack.match(/([^ \n\(#])*([a-z]*:\/\/\/?)*?[a-z0-9\/\\]*\.js/ig)[0])
}
fetch(getRunningScript() + "/../config.json")
.then(req => req.json())
.then(config => {
// code
})

Categories