I have a list of items, each having a body and a source. Currently it renders like this:
const ListItem = (props) => {
const {body, source} = props.context;
return (
<View style={styles.item}>>
<View style={{backgroundColor: 'lightblue'}}>
<Text style={styles.body}>{body}</Text>
</View>
<View style={{backgroundColor: 'lightyellow'}}>
<Text style={styles.source}>{source}</Text>
</View>
</View>
);
}
That's a lot of nesting and containers. Could it be done more optimally?
I am going to post my comment as an answer.
Previous comment:
I guess it depends on your design, AFAIK this is fine within React Native assuming you're using an optimized way of rendering your list (e.g. using a FlatList or similar)
As per your following comment, I don't think it's monstrous at all.
Here's an alternative. However, for readability I would much prefer the snippet you posted in your question.
const ListItem = props => {
const items = [
{ key: 'body', backgroundColor: 'lightblue' },
{ key: 'source', backgroundColor: 'lightyellow' }
];
return (
<View style={styles.item}>
{
items.map(({ key, backgroundColor }) =>
<View style={{ backgroundColor }}>
<Text style={styles[key]}>
{ props[key] }
</Text>
</View>
)
}
</View>
)
}
The Text component does take backgroundColor so you can let go of the two Views:
<View style={styles.item}>
<Text style={[styles.body, {backgroundColor: '...'}]}>{body}</Text>
<Text style={[styles.source, {backgroundColor: '...'}]}>{source}</Text>
</View>
Additionally, I don't know what the styles.item consists of, but if you want to go all the way, you might replace the other container View with React.Fragment.
Related
A react native 0.70 component displays items in FlatList. The array items is a state and is assigned value in hook useEffect. I would like to jest (0.29) it to see if a item's name is shown up on screen so a mock of state items is needed. Here is the render item code for FlatList:
return (
<TouchableOpacity testID={'listselltrade.detail'} onPress={() => {item.sell ? navigation.navigate("Deal Detail", {item:item}) : navigation.navigate("Trade Detail", item={item})} } >. <<== here is the testID
<View style={{flex:1, flexDirection:"row", alignItems:"center", width:wp("100%"), height:wp("17%")}}>
<View style={{width:wp("17%"), padding:wp("0.1%")}}>
{sourceUri && sourceAltUri ? <CachedImage source={{uri:sourceUri}} sourceAlt={{uri:sourceAltUri}} style={styles.itemImage} /> : null}
</View>
<View style={{width:wp("25%")}}>
<Text style={{fontSize:wp("4.2%"), flexWrap:"wrap"}}>{item.artwork.item_name}</Text>
</View>
<View style={{width:wp("8%")}}>
<Text style={{fontSize:wp("4.2%"), flexWrap:"wrap"}}>{helper.returnStatus(item.status)}</Text>
</View>
<View style={{width:wp("7%")}}>
<Text style={{fontSize:wp("4.2%"), flexWrap:"wrap"}}>{item.price+item.shipping_cost}</Text>
</View>
<View style={{width:wp("3%"),flexDirection:"row"}}>
{item.sell ? <Icon name='logo-usd' size={_size} /> : <Icon name='sync' size={_size} />}
</View>
<View style={{width:wp("8%")}}>
<Text style={{fontSize:wp("4.2%"), flexWrap:"wrap"}}>
{item.artwork.category.category_name}
</Text>
</View>
{item.sell && _me.id !== data.uploader_id && <View style={{width:wp("15%")}}>
<TouchableOpacity onPress={() => {navigation.push('ListBuyExg', {uploader_id:item.artwork.uploader_id, title:_sellerposter_name})} }>
<Text style={{fontSize:wp("4.2%"), flexWrap:"wrap"}}>
{_sellerposter_name}
</Text>
</TouchableOpacity>
</View> }
{!item.sell && _me.id !== data.uploader_id && <View style={{width:wp("15%")}}>
<TouchableOpacity onPress={() => {navigation.push('ListBuyExg', {uploader_id:item.artwork.uploader_id, title:_sellerposter_name})} } >
<Text style={{fontSize:wp("4.2%"), flexWrap:"wrap"}}>
{_sellerposter_name}
</Text>
</TouchableOpacity>
</View> }
<View style={{width:wp("20%")}}>
<Text style={{fontSize:wp("3%"), flexWrap:"wrap"}}>{item.active_date.substring(0,10)}</Text>
</View>
</View>
</TouchableOpacity>
);
Here is jest code:
test ('has screen detail', async () => {
const route = {params: {title:"my title", uploader_id:8}}; //8 is a test user. all is listed
const navigation = {navigate:jest.fn()};
const propsVal = {device_id:"a device id"};
const authVal = {result:"fdkfjdsl;fjdsafkl", myself:{id:1,name:"me"}};
const stateVal = {name:"me", alias:"akkus", aka:"aka"};
const items = [{artwork:{item_name:"work name"}}]; //<<==mock state items
const component = (
<NavigationContainer>
<propsContext.Provider value={propsVal}>
<authContext.Provider value={authVal}>
<stateContext.Provider value={stateVal}>
<ListSellTrade route={route} navigation={navigation}/>
</stateContext.Provider>
</authContext.Provider>
</propsContext.Provider>
</NavigationContainer>);
const wrapper = render(component);
expect(screen.getByText('my title')).toBeTruthy(); //<<==pass
expect(screen.getByText('work name')).toBeTruthy(); //<<==failed
expect(screen.getByTestId('listselltrade.detail')).toBeTruthy(); //<<==failed
})
But there is no item was rendered on screen and 2 expects were failed. What is the right way to mock state items so it can be fed into the jest for further test?
You should start by using screen.debug() to output the host element tree your component is rendering. This should help you understand what actually gets rendered in your RNTL tests.
You might also want to consult official RNTL + React Navigation example how to properly test screen components when using React Navigation.
we are currently running React-Native 0.33
We are trying to use the refs to go from 1 text input to another when they hit the next button. Classic username to password.
Anyone have any idea? Below is the code we are using. From other posts I've found on stack this is what they've done; however, it's telling us that this.refs is undefined.
UPDATE
So I've narrowed the problem down to
render() {
return (
<Navigator
renderScene={this.renderScene.bind(this)}
navigator={this.props.navigator}
navigationBar={
<Navigator.NavigationBar style={{backgroundColor: 'transparent'}}
routeMapper={NavigationBarRouteMapper} />
} />
);
}
If I just render the code below in the renderScene function inside of the original render it works, however with the navigator it won't work. Does anyone know why? Or how to have the navigator show as well as render the code in renderScene to appear in the original render?
class LoginIOS extends Component{
constructor(props) {
super(props);
this.state={
username: '',
password: '',
myKey: '',
};
}
render() {
return (
<Navigator
renderScene={this.renderScene.bind(this)}
navigator={this.props.navigator}
navigationBar={
<Navigator.NavigationBar style={{backgroundColor: 'transparent'}}
routeMapper={NavigationBarRouteMapper} />
} />
);
}
renderScene() {
return (
<View style={styles.credentialContainer}>
<View style={styles.inputContainer}>
<Icon style={styles.inputPassword} name="person" size={28} color="#FFCD00" />
<View style={{flexDirection: 'row', flex: 1, marginLeft: 2, marginRight: 2, borderBottomColor: '#e0e0e0', borderBottomWidth: 2}}>
<TextInput
ref = "FirstInput"
style={styles.input}
placeholder="Username"
autoCorrect={false}
autoCapitalize="none"
returnKeyType="next"
placeholderTextColor="#e0e0e0"
onChangeText={(text) => this.setState({username: text})}
value={this.state.username}
onSubmitEditing={(event) => {
this.refs.SecondInput.focus();
}}
>
</TextInput>
</View>
</View>
<View style={styles.inputContainer}>
<Icon style={styles.inputPassword} name="lock" size={28} color="#FFCD00" />
<View style={{flexDirection: 'row', flex: 1, marginLeft: 2, marginRight: 2, borderBottomColor: '#e0e0e0', borderBottomWidth: 2}}>
<TextInput
ref = "SecondInput"
style={styles.input}
placeholder="Password"
autoCorrect={false}
secureTextEntry={true}
placeholderTextColor="#e0e0e0"
onChangeText={(text) => this.setState({password: text})}
value={this.state.password}
returnKeyType="done"
onSubmitEditing={(event)=> {
this.login();
}}
focus={this.state.focusPassword}
>
</TextInput>
</View>
</View>
</View>
);
}
Try setting the reference using a function. Like this:
<TextInput ref={(ref) => { this.FirstInput = ref; }} />
Then you can access to the reference with this.FirstInput instead of this.refs.FirstInput
For a functional component using the useRef hook. You can use achieve this easily, with...
import React, { useRef } from 'react';
import { TextInput, } from 'react-native';
function MyTextInput(){
const textInputRef = useRef<TextInput>(null);;
return (
<TextInput ref={textInputRef} />
)
}
Try changing the Navigator's renderScene callback to the following (based on Navigator documentation) cause you will need the navigator object later.
renderScene={(route, navigator) => this.renderScene(route, navigator)}
Then, use 'navigator' instead of 'this' to get the refs.
navigator.refs.SecondInput.focus()
I have a ScrollView with a list of items. When I click on one item I navigate to a new scene (slide in from the right). However, during the transition period the two scenes overlap; they are fine after the animation/transition is done.
Here's an example:
and heres the related code:
render() {
return (
<Navigator
initialRoute={routes.frontpage()}
renderScene={(route, navigator) => {
if(route.index === 1) {
return (
<TouchableHighlight
onPress={() => navigator.pop()}>
<View style={{flex: 1}}>
<Text style={styles.header}> other </Text>
<Text> {route.post.title} </Text>
</View>
</TouchableHighlight>
)
}
return (
<View style={{flex: 1}}>
<Text style={styles.header}> Frontpage </Text>
<ScrollView>
{
this.props.posts.map(post => (
<TouchableHighlight
key={post.created}
onPress={() => {
if (route.index === 0) {
return navigator.push(routes.post(post))
}
navigator.pop();
}}>
<View>
<PostSummary
title={post.title}
upvotes={post.ups}
author={post.author}
subreddit={post.subreddit}
created={post.created_utc}
/>
</View>
</TouchableHighlight>
))
}
</ScrollView>
</View>
)
}}
/>
)
}
Any ideas on how to solve this?
Navigator uses animations during transitions between two scenes. In your case it uses fade during the transition. How about using a different animation. Navigator has Scene Transitions feature that you may try to change the animation.
I am building my first app with React Native, an app with a long list of images. I want to show a spinner instead of image while image is loading. It is sounds trivial but i didn't found a solution.
I think for a spinner i suppose to use ActivityIndicatorIOS , but how am i combining it with an Image component?
<Image source={...}>
<ActivityIndicatorIOS />
</Image>
Is this a right direction? Am i missing something?
I will share my solution
<View>
<Image source={{uri: this.state.avatar}} style={styles.maybeRenderImage}
resizeMode={"contain"} onLoadStart={() => this.setState({loading: true})}
onLoadEnd={() => {
this.setState({loading: false})
}}/>
{this.state.loading && <LoadingView/>}
</View>
LoadingView.js
export default class LoadingView extends Component {
render() {
return (
<View style={styles.container}>
<ActivityIndicator size="small" color="#FFD700"/>
</View>
);
}
}
const styles = StyleSheet.create({
container: {
position: "absolute",
left: 0,
right: 0,
top: 0,
bottom: 0,
opacity: 0.7,
backgroundColor: "black",
justifyContent: "center",
alignItems: "center",
}
});
Here is a complete solution to providing a custom image component with a loading activity indicator centered underneath the image:
import React, { Component } from 'react';
import { StyleSheet, View, Image, ActivityIndicator } from 'react-native';
export default class LoadableImage extends Component {
state = {
loading: true
}
render() {
const { url } = this.props
return (
<View style={styles.container}>
<Image
onLoadEnd={this._onLoadEnd}
source={{ uri: url }}
/>
<ActivityIndicator
style={styles.activityIndicator}
animating={this.state.loading}
/>
</View>
)
}
_onLoadEnd = () => {
this.setState({
loading: false
})
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
activityIndicator: {
position: 'absolute',
left: 0,
right: 0,
top: 0,
bottom: 0,
}
})
I will share my own solution based only on CSS manipulation, which in my opinion is easy to understand, and the code is pretty clean. The solution is a little similar to other answers, but doesn't require absolute position of any component, or creating any additional components.
The idea is to switch between showing an <Image> and <ActivityIndicator>, based on some state variable (isImageLoaded in the snippet below).
<View>
<Image source={...}
onLoad={ () => this.setState({ isImageLoaded: true }) }
style={[styles.image, { display: (this.state.isImageLoaded ? 'flex' : 'none') }]}
/>
<ActivityIndicator
style={{ display: (this.state.isImageLoaded ? 'none' : 'flex') }}
/>
</View>
Also you should set image size using flex property (otherwise image will be invisible):
const styles = StyleSheet.create({
image: {
flex: 1,
}
});
Note that you don't have to initiate the isImageLoaded variable to false in the constructor, because it will have undefined value and the if conditions will act as expected.
Just ran into the same issue. So basically you have the correct approach, but the indicator should of course only be rendered when the image is loading. To keep track of that you need state. To keep it simple we assume you have just on image in the component an keep the state for it in the same component. (The cool kids will argue you should use a higher order component for that and then pass the state in via a prop ;)
The idea then is, that your image starts out loading and onLoadEnd (or onLoad, but then the spinner gets stuck on error, which is fine or course) you re-set the state.
getInitialState: function(){ return { loading: true }}
render: function(){
<Image source={...} onLoadEnd={ ()=>{ this.setState({ loading: false }) }>
<ActivityIndicatorIOS animating={ this.state.loading }/>
</Image>
}
You could also start out with { loading: false } and set it true onLoadStart, but I'm not sure what the benefit would be of that.
Also for styling reasons, depending on your layout, you might need to put the indicator in a container view that is absolutely positioned. But you get the idea.
Yes, deafultSource and loadingIndicatorSource is not working properly. Also image component cannot contain children. Try this solutions => https://stackoverflow.com/a/62510268/11302100
You can simply just add a placeholder
import { Image } from 'react-native-elements';
<Image
style={styles(colors).thumbnail}
source={{ uri: event.image }}
PlaceholderContent={<ActivityIndicator color={colors.indicator} />}
/>
const [imageLoading, setIsImageLoading] = useState(true);
<View>
<View
style={[
{justifyContent: 'center', alignItems: 'center'},
imageLoading ? {display: 'flex'} : {display: 'none'},
]}>
<ActivityIndicator />
</View>
<Image
source={{uri: ""}}
onLoadEnd={() => {setIsImageLoading(false)}}
style={[
imageStyle,
imageLoading ? {display: 'none'} : {display: 'flex'},
]}
/>
</View>
I've just started with React Native and I'm sure I'm missing something small. However, for the life of me I can't figure out how to display this.data.mission.XX anywhere. I can console log the data and see it in Xcode, but that's it.
var React = require('react-native');
var Parse = require('parse/react-native');
var ParseReact = require('parse-react/react-native');
var {
Component,
StyleSheet,
Image,
Text,
View,
StatusBarIOS,
} = React;
var CountdownClock = require('./CountdownClock');
var CountdownScreen = React.createClass({
mixins: [ParseReact.Mixin],
getInitialState: function() {
return{
mission: null,
};
},
componentDidMount: function() {
StatusBarIOS.setStyle('light-content');
},
observe: function() {
return {
mission: (new Parse.Query('mission')).equalTo('featuredMission', true)
};
},
render: function() {
console.log(this.data.mission);
return (
<Image source={{uri: '#'}} style={styles.backgroundImage}>
<View style={styles.mainContainer}>
<View style={styles.clock}>
<CountdownClock />
</View>
<View style={styles.featured}>
<Text style={styles.header}> Name: {this.data.mission.missionName} </Text>
<Text style={styles.header}> Estimated Start: {this.data.mission.windowStart}</Text>
<Text style={styles.header}> Site: {this.data.mission.missionSite}</Text>
<Text style={styles.header}> Vehicle: {this.data.mission.missionVehicle}</Text>
<Text style={styles.description}>{this.data.mission.missionDescription}</Text>
</View>
</View>
</Image>
);
}
});
Taking into account that this.data is logging out, you probably need to do something like this:
Set the data property in the state:
getInitialState(){
data: {}
}
Attach this.data to the state:
componentWillUpdate() {
this.setState({
data: this.data
})
}
Use this.state.data vs this.data
<View style={styles.featured}>
<Text style={styles.header}> Name: {this.state.data.mission.missionName} </Text>
<Text style={styles.header}> Estimated Start: {this.state.data.mission.windowStart}</Text>
<Text style={styles.header}> Site: {this.state.data.mission.missionSite}</Text>
<Text style={styles.header}> Vehicle: {this.state.data.mission.missionVehicle}</Text>
<Text style={styles.description}>{this.state.data.mission.missionDescription}</Text>
</View>