I managed to fetch data and show to UI with this code:
export default class BoxGarage extends Component {
render() {
let garage = this.props.garage;
garage.name = garage.name.replace('strtoreplace', 'My Garage');
let cars = garage.cars.length ?
garage.cars.map((val, key) => {
return (
<Car key={key} car={val} />
)
}) : (
<View style={styles.boxEmpty}>
<Text style={styles.textEmpty}>(No Cars)</Text>
</View>
);
return (
<View style={styles.boxGarage}>
<Text>{ garage.name }</Text>
{ cars }
</View>
)
}
}
Then I tried to change with a function, but no cars shown. What is missing?
export default class BoxGarage extends Component {
render() {
let garage = this.props.garage;
garage.name = garage.name.replace('strtoreplace', 'My Garage');
cars = function(garage) {
if (garage.cars.length) {
garage.cars.map((val, key) => {
return (
<Car key={key} car={val} />
);
});
}
else {
return (
<View style={styles.boxEmpty}>
<Text style={styles.textEmpty}>(No Cars)</Text>
</View>
);
}
}
return (
<View style={styles.boxGarage}>
<Text>{ garage.name }</Text>
{ cars(this.props.garage) }
</View>
)
}
}
And I think I should refactor for best practice either using constructor or just move the function outside render, but I don't know what it is. Please advice.
The reason your second code doesn't work is that you're not returning anything from your function if garage.cars.length > 0.
if (garage.cars.length) {
// Added a return statement on the next line
return garage.cars.map((val, key) => {
return (
<Car key={key} car={val} />
);
});
}
That said, i think your first version of the code was much cleaner. If a piece of code got complicated enough that i was tempted to make an inline function to do calculations, i'd either pull that out to another class method, or to another component. In your case though, just doing a ternary or an if/else will be much better.
Related
Using the Open Library API I am trying to get the first lccn from the array which is 2002044748 which looks something like this.
"first_publish_year": 1937,
"lccn": [
"2002044748",
"67029221",
"2013497341",
"38005859",
"2012545250",
"73008769",
"88156046",
"67000312",
"37038859",
"2007028554",
"84009023",
"2012278060",
"77078707",
"2002075940",
"88009061",
"98102207",
"90156122",
"2001276594",
"2012039220"
],
The lccn - 2002044748 is equal to the book title The Hobbit.
If you will take a closer look on it, you'll be convince that it's an array but when I tried to get the type of it by using typeof lccn it returns an object. Even trying to get the first value like lccn[0] did not work since it's saying it's an object.
What I did is I tried to iterate on the object and see if value will be string since it looks like a string.
Object.entries(lccn).map(item => {
console.log(typeof item);
});
But this returned an object again. I am really confuse on this. How do i get the first item from the lccn array that's actually equates to the title.
Any idea how? I am a newbie when it comes to array.
UPDATE:
I have the following codes on my component:
class BookListingScreen extends Component {
_isMounted = false;
constructor(props) {
super(props);
this.state = {
books: [],
errorMessage: '',
isFetching: true,
};
}
async fetchRandomBooks() {
try {
let response = await fetch(
'https://openlibrary.org/search.json?author=tolkien',
);
let json = await response.json();
this.setState({books: json.docs, isFetching: false});
} catch (error) {
this.setState({errorMessage: error});
}
}
componentDidMount() {
this._isMounted = true;
this.fetchRandomBooks();
}
componentWillUnmount() {
this._isMounted = false;
}
render() {
let content = <BookList books={this.state.books} />;
if (this.state.isFetching) {
content = <ActivityIndicator size="large" />;
}
return <View style={styles.container}>{content}</View>;
}
}
Here's the booklist component:
export default class BookList extends Component {
_keyExtractor = item => item.title;
render() {
return (
<FlatList
style={{flex: 1}}
data={this.props.books}
keyExtractor={this._keyExtractor}
renderItem={({item} = this.props.books) => {
return <BookItem item={item} />;
}}
showsVerticalScrollIndicator={false}
showsHorizontalScrollIndicator={false}
/>
);
}
}
On my item list props:
export default class BookItem extends Component {
render() {
const {title, lccn} = this.props.item;
console.log(lccn);
return (
<View>
<View style={styles.cardContainerStyle}>
<View style={{paddingRight: 5}}>
{lccn.map(firstLccn => {
return (
<Image
source={{
uri: `https://covers.openlibrary.org/b/lccn/${firstLccn[0]}-M.jpg`,
}}
style={{width: 100, height: 100}}
/>
);
})}
<Text style={styles.cardTextStyle}>{title}</Text>
</View>
</View>
</View>
);
}
}
Basically, I am trying to get the equivalent lccn for each title so I can get the first book cover image as stated on this doc.
It would be data.docs[0].lccn[0]. The JSON you linked to has a docs array where the lccn is in the first element object.
const data = {
numFound: 296,
start: 0,
numFoundExact: true,
docs: [
{
lccn: [
"2002044748",
"67029221",
"2013497341",
"38005859",
"2012545250",
"73008769",
"88156046",
"67000312",
"37038859",
"2007028554",
"84009023",
"2012278060",
"77078707",
"2002075940",
"88009061",
"98102207",
"90156122",
"2001276594",
"2012039220"
]
}
],
num_found: 296,
q: "",
offset: null
};
console.log(data.docs[0].lccn[0]);
Update
TypeError: Cannot read properties of undefined (reading 'map')
at BookItem.render (App.js.js:23:19
The issue in your code is that there are some docs (books) elements that don't have a lccn property. This isn't a problem until in BookItem you attempt to map the lccn array, but you can't since it's undefined.
class BookItem extends React.Component {
render() {
const { title, lccn } = this.props.item;
console.log(lccn); // <-- some undefined values logged
return (
<View>
<View style={styles.cardContainerStyle}>
<View style={{ paddingRight: 5 }}>
{lccn.map((firstLccn) => { // <-- blows up here
return (
...
);
})}
...
</View>
</View>
</View>
);
}
}
See this running Expo Snack for your code and check the console log.
To resolve you can either provide a valid, mappable fallback value when destructuring:
const { title, lccn = [] } = this.props.item;
Or you can use the Optional Chaining operator to guard against the potentially null/undefined value.
{lccn?.map((firstLccn) => {
return (
...
);
})}
Code
class BookItem extends React.Component {
render() {
const { title, lccn } = this.props.item;
console.log(lccn);
return (
<View>
<View style={styles.cardContainerStyle}>
<View style={{ paddingRight: 5 }}>
{lccn?.map((firstLccn) => (
<Image
source={{
uri: `https://covers.openlibrary.org/b/lccn/${firstLccn[0]}-M.jpg`,
}}
style={{ width: 100, height: 100 }}
/>
))}
<Text style={styles.cardTextStyle}>{title}</Text>
</View>
</View>
</View>
);
}
}
If you want to access the first element of an array, you do this:
lccn[0]
The number in the braces is the index of the item. In arrays the first index is 0.
If you want to console.log all elements in an array after eachother, you can do this with the map function.
lccn.map((item)=>{
console.log(item)
})
I saw the lccn you show is an object with key lccn.
SO you can get value by: lccn.lccn[0]
If you want the first element then simply use arr_name[0]. [0] is the index of the very first item.
I would like to map a more dimensional array that looks like this:
const array = [
{
name: "Anna",
items: ["Bread", "Cake", "Wine"]
},
{
name: "John",
items: ["Cucumber", "Pizza", "Jam"]
}
]
I've tried this:
class Example extends Component {
render() {
return (
<View>
{
array.map((data) => {
return(
<Text>{data.name}</Text>
{
data.items.map((item) => {
return (
<Text>{item}</Text>
);
}
);
}
}
</View>
);
}
}
I have also tried to put this into a function that I'm rendering but neither is working for me
can you help ?
Maybe this one can help you. Also you should use key otherwise you got warn during rendering.
class Example extends Component {
_renderYYY(item) {
console.log(item);
return item.map((data) => {
return (
<View>
<Text>{data}</Text>
</View>
);
});
}
_renderXXX(array) {
return array.map((data) => {
return (
<View>
<Text key={data.name}>{data.name}</Text>
{
this._renderYYY(data.items)
}
</View>
);
});
}
render() {
return (
<View>
{
this._renderXXX(array)
}
</View>
);
}
}
A limitation of JSX is that a JSX object must always have one single root.
That is,
return (
<Text>One</Text>
<Text>Two</Text>
);
is not valid.
You should wrap the return value of the outer map (including the outer <Text> and the inner result of .map()) with a root element (probably <View>).
Additionally, you should always use the key={} prop, and give it a a globally unique value when rendering an array into JSX.
All in all, I'd have something like this:
class Example extends Component {
render() {
return (
<View>
{
array.map((data) => (
<View key={data.name}>
<Text>{data.name}</Text>
{
data.items.map((item) => (
<Text key={data.name + item}>{item}</Text>
))
}
</View>
))
}
</View>
);
}
}
I'm assuming that there cannot be duplicate names, and that there cannot be duplicate items inside of a single named object.
Here this might help.
class Example extends Component {
renderData = (array) => (
array.map((data, index) => ([
<Text key={index}>{data.name}</Text>,
this.renderItems(data.items)
])
))
renderItems = (items) => (
items.map((item, index) => (
<Text key={`${item}${index}`}>{item}</Text>
)
))
render() {
return (
<View>
{this.renderData(array)}
</View>
);
}
}
I'm trying to integrate opening app intro sliders to my app, but failing to connect the points to get from the intro to my main app body.
The library i'm using says use 'react-native-app-intro-slider' as such, where an _onDone() function is called to finish the intro and show the 'real' app:
export default class App extends React.Component {
_onDone = () => {
// User finished the introduction. Show "real" app
}
render() {
return (
<AppIntroSlider
slides={slides}
onDone={this._onDone}
/>
);
}
}
With the main body of my app (it works when I run it without the intro-slider addition) being this:
render() {
const contents = collection.map((item, index) => {
return (
<Card key={index}>
[[there's stuff here omitted]]
</Card>
)
});
return (
<View style={{flex:1}}>
<CardStack>
{contents}
</CardStack>
</View>
);
How can I get this to render after the introslider? Do I put all that inside the _onDone() function? (this doesn't work). Or is there a way write _onDone so that after the component, the regular part of the main app would proceed to go as before?
export default class App extends React.Component {
_onDone = () => {
// Something that lets me pass onto the main part of my app
}
render() {
return (
<AppIntroSlider
slides={slides}
onDone={this._onDone}
/>
// The main body of the app that I want React to get to after the <AppIntroSlider> component.
const contents = collection.map((item, index) => {
return (
<Card key={index}>
[[there's stuff here omitted]]
</Card>
)
});
return (
<View style={{flex:1}}>
<CardStack>
{contents}
</CardStack>
</View>
);
);
}
}
If you're not using a navigation library, I would suggest to simply use state:
constructor(props) {
super(props);
this.state = {
showRealApp: false
}
}
_onDone = () => {
this.setState({ showRealApp: true });
}
render() {
if (this.state.showRealApp) {
const contents = collection.map((item, index) => (
<Card key={index}>
{...}
</Card>
));
return (
<View style={{flex:1}}>
<CardStack>
{contents}
</CardStack>
</View>
);
} else {
return <AppIntroSlider slides={slides} onDone={this._onDone}/>;
}
}
You can also consult issue #18 on the react-native-app-intro-slider repo.
I want to render the list alternatively into two columns in a grid. I intend to do so using a flag variable in my component state and change its value each time I return a CardDetail.
I commented out the this.changeState() statements because they don't seem to work as intended.
Please help me figure this is out, I am new to React/React Native.
Thanks in advance.
renderAlbums() {
return this.state.albums.map(album =>{
this.rend2(album)
}
);
}
rend2(album){
if(this.state.flag === 0)
{
//this.changeState();
return (<CardDetail key={album.title} album={album} />);
}
else
{
//this.changeState;
return (<View />);
}
}
changeState(){
if(this.state.flag === 0)
{
this.setState({flag:1});
}
else
{
this.setState({flag:0})
}
}
render() {
console.log(this.state);
return (
<ScrollView>
<Grid>
<Col>{this.renderAlbums()}</Col>
<Col>{this.renderAlbums()}</Col>
</Grid>
</ScrollView>
);
}
}
I don't think you need to change the state in this case. Just pass a parameter to your renderAlbums() method and use it to pick every other album in your list:
renderAlbums(col) {
return this.state.albums.map( (album, index) => {
if (index % 2 == col)
return (<CardDetail key={album.title} album={album} />);
else
return (<View/>);
} );
}
Then in your render() method:
render() {
return (
<ScrollView>
<Grid>
<Col>{this.renderAlbums(0)}</Col>
<Col>{this.renderAlbums(1)}</Col>
</Grid>
</ScrollView>
);
}
This code has not been tested, but it should work.
Maybe change your approach a little bit and render both columns at the same time. So then you can switch between which column you want to render CardDetail.
return (
<ScrollView>
<Grid>
{this.state.albums.map((album, index) =>
<View key={index}>
<Col>{index % 2 === 0
? <CardDetail key={album.title} album={album} />
: <View />
}</Col>
<Col>{index % 2 !== 0
? <CardDetail key={album.title} album={album} />
: <View />
}</Col>
</View>
}
</Grid>
</ScrollView>
I want to call a function called 'test()' that is stated inside Main.js. Especially test() function will update the state of somestate inside Main.js and I put console log to see what's going on.
When the index of route is 1, as stated the below, the leftButton configuration(that is done in index.ios.js) is applied ( so it becomes 'click!' ). When I click this, then an error message is displayed, saying, Main.test is not a function.
Is there way that I can access test() function inside Main.js when clicking 'leftButton' in the navigator while staying in Main.js?
Do I have to do something inside index.ios.js or inside Main.js? Please share any idea with me!! (:
The below is snippet of my code ( both index.ios.js and Main.js )
////////////////////////////////////////////////////////////////
...
var Main = require('./Main');
...
render() {
return (
<Navigator
initialRoute={{ title: 'Log In Page', index: 0, component: FirstPage }}
configureScene={() => {
return Navigator.SceneConfigs.FloatFromRight;
}}
navigationBar={
<Navigator.NavigationBar
routeMapper={{
LeftButton: (route, navigator, index, navState) =>
{
if (route.index === 0) {
return null;
} else if(route.index ===1) {
return (
<TouchableHighlight onPress={() => { Main.test() } } >
<View style={styles.back}>
<Text>click!</Text>
</View>
</TouchableHighlight>
);
} else{
return (
<TouchableHighlight onPress={() => {navigator.pop(); console.log('ended..'); }}>
<View style={styles.back}>
<Text>Click</Text>
</View>
</TouchableHighlight>
);
}
},
Title: (route, navigator, index, navState) =>
{ return (<Text style={styles.route_title}> {route.title} {route.index} </Text>); },
}}
style={{backgroundColor: '#28b496'}} />
}
renderScene={(route, navigator) => React.createElement(route.component, { ...route.passProps, navigator })}
style={{padding: 0}} />
);
}
////////////////////////////////////////////////////////////////
..
class Main extends Component {
constructor(props) {
super(props);
..
}
test(){
console.log('I wish this will be triggered!');
console.log('This is the function that I want to call');
this.setState({
somestate: 'change to this man'
});
}
render(){
return(
<Text>HI</Text>
);
}
...
module.exports = Main;
////////////////////////////////////////////////////////////////
Looks like Main is a React component - in that case you cannot call test method directly unless you instantiate that component and access a reference to it (e.g. this.refs.main.test()).
I'd strongly suggest extracting test method out so that it's not component specific or making it static.
In case none of the above are possible, you could try rendering Main in below the <Navigator /> and using refs:
render() {
return (
<View>
<Navigator />
<Main ref="main" />
</View>
);
}
and accessing the method using refs as proposed at the beginning of the answer.
However, if your Main component is already in the tree (e.g. it's your parent component), you could use context and pass the method as a part of it.