I have read several posts about issues that people are having with React Native and the require() function when trying to require a dynamic resource such as:
Dynamic (fails):
urlName = "sampleData.json";
data = require('../' + urlName);
vs. Static (succeeds):
data = require('../sampleData.json');
I have read on some threads that this is a bug in React Native and in others that this is a feature.
Is there a new way to require a dynamic resource within a function?
Related Posts (all fairly old in React time):
Importing Text from local json file in React native
React Native - Dynamically List/Require Files In Directory
React Native - Image Require Module using Dynamic Names
React Native: how to use require(path) with dynamic urls?
As i have heard of, react's require() only uses static url not variables, that means that you have to do require('/path/file'), take a look at this issue on github and this one for more alternative solutions, there are a couple of other ways to do it!
for e.g
const images = {
profile: {
profile: require('./profile/profile.png'),
comments: require('./profile/comments.png'),
},
image1: require('./image1.jpg'),
image2: require('./image2.jpg'),
};
export default images;
then
import Images from './img/index';
render() {
<Image source={Images.profile.comments} />
}
from this answer
Here is my solution.
Setup
File structure:
app
|--src
|--assets
|--images
|--logos
|--small_kl_logo.png
|--small_a1_logo.png
|--small_kc_logo.png
|--small_nv_logo.png
|--small_other_logo.png
|--index.js
|--SearchableList.js
In index.js, I have this:
const images = {
logos: {
kl: require('./logos/small_kl_logo.png'),
a1: require('./logos/small_a1_logo.png'),
kc: require('./logos/small_kc_logo.png'),
nv: require('./logos/small_nv_logo.png'),
other: require('./logos/small_other_logo.png'),
}
};
export default images;
In my SearchableList.js component, I then imported the Images component like this:
import Images from './assets/images';
I then created a new function imageSelect in my component:
imageSelect = network => {
if (network === null) {
return Images.logos.other;
}
const networkArray = {
'KL': Images.logos.kl,
'A1': Images.logos.a1,
'KC': Images.logos.kc,
'NV': Images.logos.nv,
'Other': Images.logos.other,
};
return networkArray[network];
};
Then in my components render function I call this new imageSelect function to dynamically assign the desired Image based on the value in the this.state.network:
render() {
<Image source={this.imageSelect(this.state.network)} />
}
The value passed into the imageSelect function could be any dynamic string. I just chose to have it set in the state first and then passed in.
I hope this answer helps. :)
For anyone reading this that cannot work with the existing answers, I have an alternative.
First I'll explain my scenario. We have a mono repo with a number of packages (large react-native app). I want to dynamically import a bunch of locale files for i18n without having to keep a central registry in some magic file. There could be a number of teams working in the same monorepo and the DX we want is for package developers to be able to just add their local files in a known folder {{packageName}}/locales/en.json and have our core i18n functionality pick up their strings.
After several less than ideal solutions, I finally landed on https://github.com/kentcdodds/babel-plugin-preval as an ideal solution for us. This is how I did it:
const packageEnFiles = preval`
const fs = require('fs');
const path = require('path');
const paths = [];
const pathToPackages = path.join(__dirname, '../../../../packages/');
fs.readdirSync(pathToPackages)
.filter(name => fs.lstatSync(path.join(pathToPackages, name)).isDirectory())
.forEach(dir => {
if (fs.readdirSync(path.join(pathToPackages, dir)).find(name => name === 'locales')) {
const rawContents = fs.readFileSync(path.join(pathToPackages, dir, 'locales/en.json'), 'utf8');
paths.push({
name: dir,
contents: JSON.parse(rawContents),
});
}
});
module.exports = paths;
`;
Then I can just iterate over this list and add the local files to i18next:
packageEnFiles.forEach(file => {
i18n.addResourceBundle('en', file.name, file.contents);
});
If you need to switch between multiple locally stored images, you can also use this way:
var titleImg;
var textColor;
switch (this.props.data.title) {
case 'Футбол':
titleImg = require('../res/soccer.png');
textColor = '#76a963';
break;
case 'Баскетбол':
titleImg = require('../res/basketball.png');
textColor = '#d47b19';
break;
case 'Хоккей':
titleImg = require('../res/hockey.png');
textColor = '#3381d0';
break;
case 'Теннис':
titleImg = require('../res/tennis.png');
textColor = '#d6b031';
break;
}
In this snippet I change variables titleImg and textColor depending of the prop. I have put this snippet directly in render() method.
I have found that a dynamic path for require() works when it starts with a static string. For example require("./" + path) works, whereas require(path) doesn't.
Simple to dynamic images (using require)
Example array(into state)
this.state={
newimage: require('../../../src/assets/group/kids_room.png'),
randomImages=[
{
image:require('../../../src/assets/group/kids_room.png')
},
{
image:require('../../../src/assets/group/kids_room2.png')
}
,
{
image:require('../../../src/assets/group/kids_room3.png')
}
]
}
Trigger image( like when press button(i select image random number betwenn 0-2))
let setImage=>(){
this.setState({newimage:this.state.randomImages[Math.floor(Math.random() * 3)];
})
}
view
<Image
style={{ width: 30, height: 30 ,zIndex: 500 }}
source={this.state.newimage}
/>
Hey lads I rounded another way to require It's ugly but works. Images dynamically. Instead of storing your URL in the state you store the entire JSX. For an example:
state = {
image: []
};
Instead of
let imageURL = `'../assets/myImage.png'`
this.state.image = imageURL
You use
let greatImage = (<Image source={require(../assets/myImage.png)}></Image>)
this.state.image = greatImage
To render in the JSX
{this.state.image}
You can style your image in the variable too. I had to use some if statements to render some images dynamically and after break my head for 2 hours this was the way that solved my problem. Like I said It's ugly and probably wrong.
Are you using a module bundler like webpack?
If so, you can try require.ensure()
See: https://webpack.js.org/guides/code-splitting/#dynamic-imports
Reading through the docs, I've found a working answer and I'm able to use dynamic images, in the docs they refer to it as Network Images here
https://facebook.github.io/react-native/docs/images#network-images
Not sure if this can be applied to other file types, but as they list require with non image types
You would need to use the uri: call
data = {uri: urlName}
For me I got images working dynamically with this
<Image source={{uri: image}} />
Try the solution mentioned in this thread for Android. This solves the issue but unfortunately, it's only for android.
But make sure to run react-native run-android after every update. Else, the added images won't appear in the app.
This seems to work :
const {
messages,
} = require(`../${dynamicPath}/messages.specific`);
Related
I am using Vue.js and Laravel for my project and recently added two animations using a plugin named Lottie. Each animation is a component, and they both use an individual JSON file to animate a group of PNG images (similar to a PNG sequence). These two JSON files are stored locally in the project folder under the path /public/data/.
Firstly the JSON files are not being read unless I put in the absolute path (/users/username/documents/projectname/public/data/filename.json), is there no way I can get this to work just by using /data/filename.json?
Secondly, when I add the code below in my component, my JS files are compiled to separate chunks as expected:
const animationData = () =>
import("/users/username/documents/projectname/public/data/filename.json");
I get the following error when the animation tries to run:
Invalid prop: type check failed for prop "data". Expected Object, got Function
found in
---> <VueLottie>
However when I import my json file using a normal import in my component like below it works fine and shows the animation:
import animationData from "/users/username/documents/projectname/public/data/filename.json";
My animation components are both set up like this:
<template>
<vue-lottie ref="lottie" loop autoplay :data="animationData" :height="400" :width="400"></vue-lottie>
</template>
<script>
import vueLottie from "vue-lottie-ssr";
import animationData from '/users/username/documents/projectname/public/data/filename.json'
export default {
name: 'animation',
components: {
vueLottie
},
data () {
return {
speed: 1,
animationData
}
},
computed: {
lottie () {
return this.$refs.lottie
}
}
}
</script>
I have also tried getting the JSON file via an axios call when the component mounts, but the same error occurs.
Update
I updated my code so that each component is lazy loaded instead of the JSON file. Like so:
components: {
WinAnimation: () => import("./WinAnimation.vue");
LoseAnimation: () => import("./LoseAnimation.vue");
}
However now I'm getting the following error:
Unknown custom element: <win-animation> - did you register the component correctly? For recursive components, make sure to provide the "name" option.
Update 2
I realised why I was getting an error message. The correct way was to add the following at the top of my script inside the parent vue file.
const winAnimation = () => import("./WinAnimation.vue");
const loseAnimation = () => import("./LoseAnimation.vue");
and then inside export default {...} I forgot to add the names, so:
components: { winAnimation, loseAnimation }
Now my code has been split and my app.js file size has reduced by almost a half! :)
1st - don't use vue-lottie library. If you take a look at the source code, the main and only thing which should be provided by this library is component src/lottie.vue (+ it's dependency lottie-web) but for some reason, NPM package also contains whole demo app including the demo JSON file (src/assets/pinjump.json)
If you take a look at lottie.vue component, its just very little and very simple wrapper for lottie-web which provides main functionality. By getting rid of vue-lottie you will get following benefits:
vue-lottie completely ignores one of the lottie-web options which is using path instead of animationData - documentation is not very clear here but I would guess that by providing path, the library will try download the animation data ad-hoc so you don't need to include it in your bundle. Worth trying imho...
Loading animation data on demand
why are you using dynamic import on JSON file instead of dynamically importing whole component ? By making separate chunk on component level, dynamic chunk will include not only your json data but also lottie-web which is also not small. And Vue will handle loading of the component without any additional code changes...
if you still want to load on demand only your JSON data, you must understand that Webpack dynamic import (import(".....")) is returning Promise and lottie-web (and in turn vue-lottie) is expecting object. So you must do something like this:
<script>
import lottie from 'lottie-web';
const animationData = () =>
import("/users/username/documents/projectname/public/data/filename.json");
export default {
mounted () {
animationData().then(function(data) {
this.anim = lottie.loadAnimation({
// other options
animationData: data
})
});
}
}
</script>
Update
You should be always very careful when considering adding 3rd party components into your project. One more thing I'v noticed is that lottie-web has destroy() method in it's API. This indicates that it is creating some resources (DOM elements probably) which needs to be cleaned up. This is something vue-lottie component is not handling at all and can lead to nasty memory leaks in your app. You can read about the problem here
When the animationData property is set it is a function, hence the line:
Expected Object, got Function
It needs an object, not a function.
The function being:
const animationData = () =>
import("/users/username/documents/projectname/public/data/filename.json");
When defining the animationData property you need to set an object as its value. Then when mounting fetch the data (or use Axios if you prefer that) to update the animationData property on the component.
N.B. I have never used Vue, so I hope that what I am saying is correct.
export default {
name: 'animation',
components: {
vueLottie
},
data () {
return {
speed: 1,
animationData: {}
}
},
computed: {
lottie () {
return this.$refs.lottie
}
},
mounted() {
fetch('/users/username/documents/projectname/public/data/filename.json')
.then(response => response.json())
.then(json => this.animationData = json;);
)
}
}
Hello I am trying to input a value into an SVG URI in React-Native as follows:
source={require('../tasks/images/' + {task.image} + '.png')}
I am currently iterating through an array of objects and outputting the svgs for each, so I want to do it this way but the syntax isnt accepted.
Any help or advice is welcome!
Because there is no way to make the string passed to require dynamically, you could have an image collection file that references every image you would need, for example:
Create a file ImageCollection.js with the following:
export default imageCollection={
"1": require("./image1.png"),
"2": require("./image2.png"),
"3": require("./image3.png"),
"4": require("./image4.png")
}
Then import it into your file where you need to require your images and do something like this:
import Images from './ImageCollection.js';
class YourComponent extends Component {
renderItem = ({item}) => (
<View>
<Text>Image: {item}</Text>
<Image source={Images[item]}/>
</View>
);
render () {
const data = ["1","2","3","4","5"]
return (
<FlatList data={data} renderItem={this.renderItem}/>
)
}
}
export default YourComponent;
But if you really need to pass dynamic image sources then you could use a package like react-native-image-progress which lets you pass a variable as an image source, I wouldn't recommend this approach unless if there is absolutely no other way for you to solve this problem.
Here, you cannot pass dynamic images path which is available locally as your assests.
Moreover if you have image URI which is coming from the server then you can just provide here and it will work like a charm. But for image Path which is present locally you have to provide actual path. More you can read here.
However, what you can do just provide nested if else for the path based on the conditions, but then please note, you have to provide the full path as well.
Example :-
source={this.state.data===1?require('../tasks/images/image1.png'):this.state.data===2?require('../tasks/images/image2.png'):require('../tasks/images/image3.png')}
Thanks....Hope it helps :)
I am working on a react app that is going to be launched for different countries. In each country some components will be the same but others are going to be different.
i.e
France will have
- Component A
- Component B
- Component C
Germany will have
- Component A
- Component D
So, both sites share similar components but some of them are unique.
We use a global env var to tell which site to load. (en, fr, etc)
Worth saying that the URL should be the same for all sites (the variable above should the one that tells the app which component to display)
What would be the best idea for handling this component differences?
Ideas that I`ve came up with until today:
Create one site per country. (Big problem since too much DRY. There are shared many components)
Conditional rendering (Feels hacky, since there are several countries, leading to endless if else)
High Order component that returns all the components inside a parent container component (Feels good, but I did not want to reinvent the wheel and I wanted to ask here first)
Is there a NPM package that will help me archive this?
Should I start from scratch?
Thank you very much.
not sure if I get the point (sorry if that is the case).
You don't need to create two sites. You just need to organize the site content in a passing structure (something like):
In the file with the site content:
const frenchData = [
{
idKey: 'aboutus',
textTitel: aboutUsTextTitelFR,
textField: aboutUsTextFieldFR
}
];
const englishData = [
{
idKey: 'aboutus',
textTitel: aboutUsTextTitelEN,
textField: aboutUsTextFieldEN
}
];
export const aboutusData = {
'fr': frenchData,
'en': englishData
}
You can get the preferred languages (from the user's browser) with the following lines.
static async getInitialProps({ req }) {
const userAgent = req ? req.headers['accept-language'] : navigator.userAgent;
return { userAgent };
}
After making some string formating on the userAgent result, you will get an array of preferred languages. After that you need to render the passing data (DE, EN, ES, whatever)
I have the following two components:
// component.js
// imports ...
function ListItem(item) {
const html = wire(item)
function render() {
return html`<li>${item.foo}</li>`
}
return render()
}
function List(items) {
const html = wire(items)
function render() {
return html`<ul>${items.map(ListItem)}</ul>`
}
return render()
}
I want to put them in a module which is shared between the client and the server. However, as far as I can tell, although the API pretty much identical, on the server I have to import the functions from the viperHTML module, on the client I have to use the hyperHTML module. Therefore I can not just import the functions at the top of my shared module, but have to pass to my components at the call site.
Doing so my isomorphic component would look like this:
// component.js
function ListItem(html, item) {
//const html = wire(item) // <- NOTE
function render() {
return html`<li>${item.foo}</li>`
}
return render()
}
function List(html, itemHtmls /* :( tried to be consistent */, items) {
//const html = wire(items) // <- NOTE
function render() {
return html`<ul>${items.map(function(item, idx) {
return ListItem(itemHtmls[idx], item)
})}</ul>`
}
return render()
}
Calling the components from the server:
// server.js
const {hyper, wire, bind, Component} = require('viperhtml')
const items = [{foo: 'bar'}, {foo: 'baz'}, {foo: 'xyz'}]
// wire for the list
const listWire = wire(items)
// wires for the children
const listItemWires = items.map(wire)
const renderedMarkup = List(listWire, listItemWires, items)
Calling from the browser would be the exact same, expect the way hyperhtml is imported:
// client.js
import {hyper, wire, bind, Component} from 'hyperhtml/esm'
However it feels unpleasant to write code like this, because I have a feeling that the result of the wire() calls should live inside the component instances. Is there a better way to write isomorphic hyperHTML/viperHTML components?
update there is now a workaround provided by the hypermorphic module.
The ideal case scenario is that you have as dependency only viperhtml, which in turns brings in hyperhtml automatically, as you can see by the index.js file.
At that point, the client bundler should, if capable, tree shake unused code for you but you have a very good point that's not immediately clear.
I am also not fully sure if bundlers can be that smart, assuming that a check like typeof document === "object" would always be true and target browsers only.
One way to try that, is to
import {hyper, wire, bind, Component} from 'viperhtml'
on the client side too, hoping it won't bring in viperHTML dependencies once bundled 'cause there's a lot you'd never need on the browser.
I have a feeling that the result of the wire() calls should live
inside the component instances.
You could simplify your components using viper.Component so that you'll have render() { return this.html... } and you forget about passing the wire around but I agree with you there's room for improvements.
At that point you only have to resolve which Component to import in one place and define portable components that work on b both client and server.
This is basically the reason light Component exists in the first place, it give you the freedom to focus on the component without thinking about what to wire, how and/or where (if client/server).
~~I was going to show you an example but the fact you relate content to the item (rightly) made me think current Component could also be improved so I've created a ticket as follow up for your case and I hope I'll have better examples (for components) sooner than later.~~
edit
I have updated the library to let you create components able to use/receive data/items as they're created, with a code pen example.
class ListItem extends Component {
constructor(item) {
super().item = item;
}
render() {
return this.html`<li>${this.item.foo}</li>`;
}
}
class List extends Component {
constructor(items) {
super().items = items;
}
render() {
return this.html`
<ul>${this.items.map(item => ListItem.for(item))}</ul>`;
}
}
When you use components you are ensuring yourself these are portable across client/server.
The only issue at this point would be to find out which is the best way to retrieve that Component class.
One possible solution is to centralize in a single entry point the export of such class.
However, the elephant in the room is that NodeJS is not compatible yet with ESM modules and browsers are not compatible with CommonJS so I don't have the best answer because I don't know if/how you are bundling your code.
Ideally, you would use CommonJS which works out of the box in NodeJS and is compatible with every browser bundler, and yet you need to differentiate, per build, the file that would export that Component or any other hyper/viperHTML related utilities.
I hope I've gave you enough hints to eventually work around current limitations.
Apologies if for now I don't have a better answer. The way I've done it previously used external renders but it's quite possibly not the most convenient way to go with more complex structures / components.
P.S. you could write those functions just like this
function ListItem(item) {
return wire(item)`<li>${item.foo}</li>`;
}
function List(items) {
return wire(items)`<ul>${items.map(ListItem)}</ul>`;
}
Suppose I have a list of url's like so :
[ '/images/1', '/images/2', ... ]
And I want to prefetch n of those so that transitioning between images is faster. What I am doing now in componentWillMount is the following:
componentWillMount() {
const { props } = this;
const { prefetchLimit = 1, document = dummyDocument, imgNodes } = props;
const { images } = document;
const toPrefecth = take(prefetchLimit, images);
const merged = zip(toPrefecth, imgNodes);
merged.forEach(([url, node]) => {
node.src = url;
});
}
with imgNodes being defined like so:
imgNodes: times(_ => new window.Image(), props.prefetchLimit),
and times, zip, and take coming from ramda.
Now when I use those urls inside of react like so:
<img src={url} />
it hits the browser cache according to the Etag and Expire tags regardless of where the url is used. I also plan on using this to prefetch the next n images whenever we hit n - 1 inside of the view, reusing imgNodes in the same manner.
My question are:
Is this even a valid idea give 100+ components that will use this idea but only 1 will be visible at a time?
Will I run into memory issues by doing this? I am assuming that imgNodes will be garbage collected when the component is unmounted.
We are using redux so I could save these images in the store but that seems like I am handling the caching instead of leveraging the browser's natural cache.
How bad of an idea is this?
You don't need to do it in all of your components. As soon as an image is downloaded it gets cached by the browser and will be accessible in all components, so you can do this only once somewhere in a high-level component.
I don't know what exactly UX you are trying to create by caching images, however, your code only initiates downloading images but doesn't know whether an image is being downloaded, has been downloaded successfully or even failed. So, for example, you want to show a button to change images or add a class to a component only when the images have been downloaded (to make it smooth), your current code may let you down.
You may want to resolve this with Promises.
// create an utility function somewhere
const checkImage = path =>
new Promise((resolve, reject) => {
const img = new Image()
img.onload = () => resolve(path)
img.onerror = () => reject()
img.src = path
})
...
// then in your component
class YourComponent extends Component {
this.state = { imagesLoaded: false }
componentDidMount = () =>
Promise.all(
R.take(limit, imgUrls).map(checkImage)
).then(() => this.setState(() => ({ imagesLoaded: true })),
() => console.error('could not load images'))
render = () =>
this.state.imagesLoaded
? <BeautifulComponent />
: <Skeleton />
}
Regarding memory consumption — I don't think anything bad will happen. Browsers normally limit the number of parallel xhr requests, so you won't be able to create a gigantic heap usage spike to crash anything, as unused images will garbage collected (yet, preserved in browser cache).
Redux store is a place to store the app state, not the app assets, but anyway you won't be able to store any actual images there.
This is simple and works fine:
//arraySrcs=["myImage1.png","myImage2.jpg", "myImage3.jpg", etc]
{arraySrcs.map((e) => (
<img src={e} style={{ display: "none" }} />
))}