How to use map function to Mobx observable state? - reactjs

Now I would like to use Map function to show a component with each array.
But, my code with map is not working rightly.
In firestore, there are 3 document in "food" collection but this map function can only render one document.
This is my code below in render component.
renderItemBox() {
const { store } = this.props;
const items = store.restaurantStore.Items;
return items.map((value, index) => {
console.log(items);
return (
<TouchableOpacity key={index}>
<View style={styles.itemsBox}>
<View style={styles.itemsImageBox}>
<Image
source={require('../../assets/Images/Home/HomeListImageShop.jpg')}
style={styles.itemsImage}
/>
</View>
<View style={{ flexDirection: 'row' }}>
<Text style={styles.itemsName}>
{value[index].name}
</Text>
<Text style={styles.itemsTag}>
{value[index].tag}
</Text>
</View>
<View style={{ flexDirection: 'row' }}>
<Text style={styles.itemsDescription}>
{value[index].shortDescription}
</Text>
<TouchableOpacity
style={styles.likeButtonBox}
onPress={this.handleLikeButton.bind(this)}
>
{this.handleLikeImage()}
</TouchableOpacity>
</View>
</View>
</TouchableOpacity>
);
});
}
In addition to this, I set store component in my project.
This code above is importing observable state(array) from that store.
Below one is that store.
import { observable, action } from 'mobx';
import firebase from 'firebase';
class RestaurantStore {
#observable Items = [];
#action handleFirestoreCollectionOfFoods () {
const db = firebase.firestore();
db.collection('foods')
.get()
.then((snapshot) => {
const preItems = [];
snapshot.forEach((doc) => {
const docData = doc.data();
preItems.push(docData);
});
this.Items.push(preItems);
})
.catch((error) => {
console.log(error);
});
}
}
export default RestaurantStore;
Fortunetely, this store can import complete document from firestore.
I did check that with console.log.
So, does someone know how to use map function to mobx state completely?

Related

Invalid hook call React Native FlatList Navigation

I'm making a notes app in React Native and trying to make it so I can click on a note in a FlatList to edit it. I'm using react-router-native for this. I get an Error when clicking on any FlatList item. I know that this error has been asked on stack overflow before but the answers are all for class components, whereas I'm using functional components.
Error: Invalid hook call. Hooks can only be called inside of the body of a function component.
import { FlatList, Pressable, StyleSheet, View } from "react-native"
import { useNavigate } from "react-router-native"
import theme from "../theme"
import Text from "./Text"
const styles = StyleSheet.create({
separator: {
height: 10,
backgroundColor: theme.colors.background,
},
item: {
padding: 8,
backgroundColor: "white",
},
})
const ItemSeparator = () => <View style={styles.separator} />
const renderItem = ({ item }) => (
<View style={styles.item}>
<Pressable onPress={() => useNavigate(`/${item.id}`)}>
<Text fontWeight="bold" fontSize="subheading">
{item.title}
</Text>
<Text>{item.body}</Text>
</Pressable>
</View>
)
const NoteList = ({ notes }) => {
return (
<FlatList
data={notes}
ItemSeparatorComponent={ItemSeparator}
renderItem={renderItem}
keyExtractor={(item) => item.id}
/>
)
}
useNavigate is a React hook and can only be called by a React function component or other custom React hook. It cannot be called in nested functions/callbacks.
Move the useNavigate hook call to the NoteList component and refactor the renderItem callback to curry a passed navigate function.
const ItemSeparator = () => <View style={styles.separator} />;
const renderItem = (navigate) => ({ item }) => (
<View style={styles.item}>
<Pressable onPress={() => navigate(`/${item.id}`)}>
<Text fontWeight="bold" fontSize="subheading">
{item.title}
</Text>
<Text>{item.body}</Text>
</Pressable>
</View>
);
const NoteList = ({ notes }) => {
const navigate = useNavigate(); // <-- hook called in React function
return (
<FlatList
data={notes}
ItemSeparatorComponent={ItemSeparator}
renderItem={renderItem(navigate)} // <-- pass navigate
keyExtractor={(item) => item.id}
/>
);
};
Alternatively you could move the renderItem function declaration into the NoteList component so the navigate function is just closed over in callback scope.
const ItemSeparator = () => <View style={styles.separator} />;
const NoteList = ({ notes }) => {
const navigate = useNavigate();
const renderItem = ({ item }) => (
<View style={styles.item}>
<Pressable onPress={() => navigate(`/${item.id}`)}>
<Text fontWeight="bold" fontSize="subheading">
{item.title}
</Text>
<Text>{item.body}</Text>
</Pressable>
</View>
);
return (
<FlatList
data={notes}
ItemSeparatorComponent={ItemSeparator}
renderItem={renderItem}
keyExtractor={(item) => item.id}
/>
);
};

FlatList with Firestore? React Native

I'm sure this is something that is pretty common but can't seem to get it figured out. Each user has a "books" array in Firestore, and this is what I want to be returned in the Flatlist, where am I going wrong? The first day with firebase so I'm pretty sure its something basic. Thanks :)
<FlatList
data={() => { db.collection('users').doc(userEmail).data() }}
numColumns={2}
ListEmptyComponent={
<View style={styles.flatListEmpty}>
<Text style={{ fontWeight: 'bold' }}>Add Books Below</Text>
</View>
}
renderItem={({ item }) => (
<View style={styles.flatListStyle}>
<Text>book</Text>
</View>
)}
/>
then this is the database:
The problem is that query to firebase is asynchronous action, so you need to resolve that action then store the result to the state of component and assign that state to flatlist. Example:
const Component = () => {
const [data, setData] = useState();
const getData = async () => {
const snapshot = await db.collection('users').doc(userEmail).get()
setData(snapshot.data())
}
useEffect(() => {
getData()
}, [])
return (
<FlatList
data={data.books}
numColumns={2}
ListEmptyComponent={
<View style={styles.flatListEmpty}>
<Text style={{ fontWeight: 'bold' }}>Add Books Below</Text>
</View>
}
renderItem={({ item }) => (
<View style={styles.flatListStyle}>
<Text>book</Text>
</View>
)}
/>
)
}

How can I make componentDidMount render again?

I'm fetching api(makeup API) in Explore component and using it also in Explorebutton.
Im taking brands as a button in ExploreButtons. When i click button in FlatList element in ExploreButtons I want to see images from api in second FlatList in ExploreButtons. Is there a way componentDidMount can rerender when i click button?
import React, { Component } from 'react'
import { View } from 'react-native'
import ExploreButtons from './ExploreButtons'
export default class Explore extends Component {
constructor(props) {
super(props);
this.state = {
isLoading: true,
makeupApi: 'http://makeup-api.herokuapp.com/api/v1/products.json',
}
}
callbackFunction = (item) => {
this.setState({
makeupApi: 'http://makeup-api.herokuapp.com/api/v1/products.json?brand=' + item,
})
}
async componentDidMount() {
try {
const response = await fetch(this.state.makeupApi);
const responseJson = await response.json();
this.setState({
isLoading: false,
dataSource: responseJson,
}, function () {
});
const reformattedArray = this.state.dataSource.map(obj => {
var rObj = {};
rObj = obj.brand;
return rObj;
});
this.setState({
duplicatesRemoved: reformattedArray.filter((item, index) => reformattedArray.indexOf(item) === index)
})
}
catch (error) {
console.error(error);
}
};
render() {
console.log(this.state.makeupApi)
return (
<View style={{ flex: 1 }}>
<ExploreButtons
api={this.state.dataSource}
removedDuplicatesFromAPI={this.state.duplicatesRemoved}
parentCallback={this.callbackFunction}
makeupApi= {this.state.makeupApi} />
</View>
)
}
}
export default class ExploreButtons extends Component {
getBrandImages = (item) => {
this.props.parentCallback(item)
}
render() {
return (
<View style={{ flex: 1 }}>
<View>
<FlatList
horizontal
showsHorizontalScrollIndicator={false}
data={this.props.removedDuplicatesFromAPI}
renderItem={({ item }) =>
<TouchableOpacity
style={styles.exploreButtons}
onPress={() => {
this.getBrandImages(item)
}}
>
<Text>{item}</Text>
</TouchableOpacity>
}
keyExtractor={item => item}
/>
</View>
<View>
<FlatList
data={this.props.api}
renderItem={({ item }) =>
<View>
<Image source={{ uri: item.image_link }}
style={{
alignSelf: "center",
width: '100%',
height: 300,
}} />
</View>
}
keyExtractor={item => item.id.toString()} />
</View>
</View>
)
}
}
You could just put all the logic inside componentDidMount on another function and call it when you call the callback. As a first very rough approach this would work:
Notes: you don't really need the API URL in the state, put the item on the state and construct the URL based on it.
import React, { Component } from 'react';
import { View } from 'react-native';
import ExploreButtons from './ExploreButtons';
export default class Explore extends Component {
API_URL = 'http://makeup-api.herokuapp.com/api/v1/products.json';
constructor(props) {
super(props);
this.state = {
isLoading: true,
item: null,
dataSource: null,
duplicatesRemoved: [],
};
}
getAPIURL(item) {
if(!item){
return API_URL
}
return `${API_URL}?brand=${item}`;
}
async fetchData(item) {
try {
const url = getAPIURL(item);
const response = await fetch(url);
const responseJson = await response.json();
this.setState({
isLoading: false,
dataSource: responseJson,
item,
});
const reformattedArray = responseJSON.map(({ brand }) => brand);
this.setState({
duplicatesRemoved: reformattedArray.filter(
(item, index) => reformattedArray.indexOf(item) === index,
),
});
} catch (error) {
console.error(error);
}
}
async componentDidMount() {
fetchData();
}
render() {
const { dataSource, duplicatesRemoved, item } = this.state;
return (
<View style={{ flex: 1 }}>
<ExploreButtons
api={dataSource}
removedDuplicatesFromAPI={duplicatesRemoved}
parentCallback={this.fetchData}
makeupApi={getURL(item)}
/>
</View>
);
}
}
export default class ExploreButtons extends Component {
getBrandImages = item => {
this.props.parentCallback(item);
};
render() {
const { removedDuplicatesFromAPI, api } = this.props;
return (
<View style={{ flex: 1 }}>
<View>
<FlatList
horizontal
showsHorizontalScrollIndicator={false}
data={removedDuplicatesFromAPI}
renderItem={({ item }) => (
<TouchableOpacity
style={styles.exploreButtons}
onPress={() => {
this.getBrandImages(item);
}}
>
<Text>{item}</Text>
</TouchableOpacity>
)}
keyExtractor={item => item}
/>
</View>
<View>
<FlatList
data={api}
renderItem={({ item }) => (
<View>
<Image
source={{ uri: item.image_link }}
style={{
alignSelf: 'center',
width: '100%',
height: 300,
}}
/>
</View>
)}
keyExtractor={item => item.id.toString()}
/>
</View>
</View>
);
}
}
How can I make componentDidMount render again?
Not sure what you mean, but I think what you are asking is How can I make componentDidMount *run* again?, and to do that, you would need to have the same code in callbackFunction to run that again. componentDidMount will only run after the first time the component render.
Also notice that if you want to rerender the FlatList you need to pass extraData so it know that it needs to rerender.

React-Native + Redux passing ID to component

I am new to react-native and recently implemented Redux in my app.
The App is for ordering food in Restaurants :)
This is a snippet of my Restaurant-Details Screen with cards, which are representing the different menus:
<View style={{alignItems: 'center'}}>
{menus.map((item) => {
return (
<Card style={{width: '95%'}}>
<TouchableOpacity onPress ={() => this.props.navigation.navigate('Products', { menuId: item.sk_id } ,{bestellung})}>
<CardItem cardBody style={{justifyContent: 'center', alignItems: 'center'}}>
<Text style={styles.textdes}> {item.typ} </Text>
</CardItem>
</TouchableOpacity>
</Card>
)
})}
As you can see the onPress Function navigates to the Product Screen and passes a menuId! (Product Screen shows all the food of a specific menu)
This is my Product.js:
class Products extends Component {
constructor(props) {
super(props);
this.state = {
loading: 1
};
}
componentDidMount = () => {
this.props.fetchProducts();
this.state.menuId;
}
addItemsToCart = (product) => {
this.props.addToCart(product);
}
render() {
const { products, navigation } = this.props
const {params} = this.props.navigation.state;
const menuId = params ? params.menuId : null;
return (
<View style={styles.container}>
<Header style={styles.header}>
<Left>
<Button transparent onPress={() =>
this.props.navigation.goBack()}>
<Icon style={{ fontSize: 35, color: '#FFFFFF'}}
active name="arrow-back" />
</Button>
</Left>
<Body>
<Title style={{ fontSize: 25}}>{menuId}</Title>
</Body>
<Right style={{position: 'absolute', right: 20}}>
<Cart navigation={navigation}/>
</Right>
</Header>
<StatusBar barStyle="light-content" backgroundColor="#383838"
translucent={false} />
<View style={styles.bodyProducts}>
<View>
<Text style={{color:
'#000', fontSize: 50}}>{menuId}</Text>
</View>
<FlatList
data={products}
renderItem={({item}) =>
<Product item={item} addItemsToCart=
{this.addItemsToCart} product={item}/>}
keyExtractor ={(item) => item.id}
/>
</View>
</View>
);
}
}
const mapStateToProps = (state) => ({
products: state.products.items
})
export default connect(mapStateToProps {addToCart,fetchProducts})(Products);
The Product.js fetches products from my product.component.js:
class Product extends Component {
addToCart = () => {
this.props.addItemsToCart(this.props.item)
}
render() {
const { product, menuId } = this.props;
return (
<Card>
<View style={styles.container}>
<Image source={product.picture} style={{width:400,height:150}}/>
<View style={styles.productDes}>
<CardItem cardBody style={{justifyContent: 'center',
alignItems: 'center'}}>
<Text style={styles.font}>{menuId}</Text>
</CardItem>
<Text style={{fontSize: 20}}>€{(product.cost).toFixed(2)}
</Text>
<Text style={{fontSize: 16}}>{product.description}</Text>
<TouchableOpacity onPress={this.addToCart} style=
{styles.addBtn}>
<Text style={styles.text}>Hinzufügen</Text>
</TouchableOpacity>
</View>
</View>
</Card>
);
}
}
export default Product;
The question is now: How can I pass the menuId from product.js to
product.component.js without naviagtion?
I hope I could make clear, what my problem is and looking forward to your solutions and help :)
Maybe you should better separate your React components - which are basically views - and your reducers / side-effect (data fetching...) components.
State is in Redux Store, Views only display what's in store.
If you don't want to pass menuId through navigation params, you can add it to your store state - basically you add a field in your reducer's state to save the currently selected menuId.
In your Restaurant-Details screen, when calling onPress, you dispatch an action with the selectedMenuId to your redux store.
Then in your product screen, you retrieve the selectedMenuId from your redux store.
To understand Redux architecture , I like this gif:
Since you're already using Redux, why not store the menu ID you're currently viewing in your store? When the user selects a menu item, dispatch an action to set the current menu ID. Then use a selector to to get the menu ID in the component you need it.
I hope I haven't misunderstood your issue.

Add data in React Native and Save the State

I'm building a basic React Native application where the user enters his name in a TextInput and on button press his name along with his image is added to a ScrollView in another View. Images are stored and named in accord with the name of the person. Example - Name: 'ABC', the image fetched from assets will be 'ABC.jpg'.
I'm able to do this for one person, but every time I add a new entry the previous one gets overwritten. How can I retain the previous entry yet add another entry?
Home
import React, {Component} from 'react';
import { StyleSheet, Text, View, Image, ScrollView, Button, TouchableWithoutFeedback, TextInput} from 'react-native';
import { createStackNavigator } from 'react-navigation';
class HomeScreen extends React.Component {
render() {
const { navigation } = this.props;
const name0 = navigation.getParam('name0', 'NO-ID');
return (
<View style={styles.container}>
<ScrollView vertical={true} contentContainerStyle={{flexGrow: 1}}>
<Text style={styles.category}>Category 1</Text>
<ScrollView horizontal={true} showsHorizontalScrollIndicator={false}>
<TouchableWithoutFeedback onPress={() => {
this.props.navigation.navigate('Details', {
name: name0,
intro: 'lorem ipsum',
detail1: 'XYZ',
detail2: 'ABC',
});
}}>
<View style={styles.view}>
<Image source={require('./assets/rohit.jpg')} style={styles.image}></Image>
<Text style={styles.text}>{name0}</Text>
</View>
</TouchableWithoutFeedback>
</ScrollView>
</ScrollView>
</View>
)
}
}
Add Data
class AddData extends React.Component {
constructor(props) {
super(props);
this.state = { text: '' };
}
render() {
function onPressLearnMore() {
alert('Hi')
}
return (
<View style={{flex: 1, flexDirection: 'column', justifyContent:'center', alignItems: 'center'}}>
<TextInput
style={{height: 40,width:200}}
onChangeText={(text) => this.setState({input: text})}
/>
<Button
onPress={() => {
this.props.navigation.navigate('Home', {
name0: this.state.input
});
}}
title="Pass Data"
/>
</View>
);
}
}
Navigator
const RootStack = createStackNavigator(
{
Home: HomeScreen,
Details: DetailsScreen,
Data: AddData
},
{
initialRouteName: 'Data',
}
);
export default class App extends React.Component {
render() {
return <RootStack />;
}
}
Screen 1
Screen 2
Because you aren't saving your data from the user input anywhere, simply passing it back to home, it is getting overwritten and as your code sits, it is working! ;)
To simply put a bandaid on what you're doing, because this method of passing data is slightly taboo.. and you cannot achieve what you're asking without some sort of state persistence like AsyncStorage, or... to not have two screens. Because everytime React re-renders your screen any data that was in your state object disappears...
Inside your home component, on did mount you could store user input to state as objects inside an array. IN the below example I am using the user data you are sending back to home, however this would need to be data retrieved from some sort of persistence.
class HomeScreen extends React.Component {
constructor(props) {
super(props)
this.state = {
usersArray: []
}
}
componentDidMount() {
let userInfo = this.props.navigation.getParam('name0', 'NO-ID');
userInfo !== undefined ? this.setState({usersArray:
[...this.state.usersArray, userInfo]}) : console.log('NO USER DATA to add')
}
render() {
const { navigation } = this.props;
const name0 = navigation.getParam('name0', 'NO-ID');
return (
<View style={styles.container}>
<ScrollView vertical={true} contentContainerStyle={{flexGrow: 1}}>
<Text style={styles.category}>Category 1</Text>
<ScrollView horizontal={true} showsHorizontalScrollIndicator={false}>
<TouchableWithoutFeedback onPress={() => {
this.props.navigation.navigate('Details', {
name: name0,
intro: 'lorem ipsum',
detail1: 'XYZ',
detail2: 'ABC',
});
}}>
<View style={styles.view}>
<Image source={require('./assets/rohit.jpg')} style={styles.image}></Image>
{/* <Text style={styles.text}>{name0}</Text> */}
{ this.state.usersArray.map((ele, index) => {
return <Text key={index} style={styles.text}>{ele.name}</Text>
})}
</View>
</TouchableWithoutFeedback>
</ScrollView>
</ScrollView>
</View>
)
}
}
Please keep in mind you may need to change your user data to be objects for this rest and spreading to work.

Resources