I'm having some trouble with updating some props. My app has a feed screen (listView) of posts (listItems). I’m also using redux.
If I click on a post, it'll take me to the comments screen. As it does so, it passes through the props of the listItem and fully displays the post text, image, author etc.
When in the commentScreen, If I click on the content (image/text), it'll take me through to the editPost screen. As it does this, it takes the props (image/text/uid) and populates the textInput and image field so the user can edit it. When done, I can click the saveEdit button. This will take the text, image and uid and update the post within firebase via an action. It will also pop() the screen and take me back to the commentsScreen.
The problem is, the edited post can't be seen. The original text/image is still there. If I look in my Firebase console, I can see that the edit HAS been saved.
If I pop the commentsScreen and go back to the feed, it's up to date. I can see that the post has been successfully edited. If I then click on the post again, it'll take me back to the commentsScreen, showing the updated post.
I guess this is because the listView in the feed itself is watching the firebase reference for the posts? and as such the post in the commentScreen won't change until I revisit the feed so it can update?
How can I work around this? Do I need to be doing something with componentWillUpdate or componentWillReceiveProps or something?
I have tried firing different get requests to firebase at different points but I've had no success so far.
This is my Feed:
class FeedComponent extends Component {
componentWillMount() {
this.props.postsFetch();
this.createDataSource(this.props);
}
componentWillReceiveProps(nextProps) {
// nextProps are the next set of props that this component
// will be rendered with
// this.props is still the old set of props
this.createDataSource(nextProps);
}
createDataSource({ posts }) {
const ds = new ListView.DataSource({
rowHasChanged: (r1, r2) => r1 !== r2
});
this.dataSource = ds.cloneWithRows(posts);
}
renderRow(posts) {
return <PostListItem posts={posts} />;
}
render() {
return (
<ListView
{...this.props}
enableEmptySections
removeClippedSubviews={false}
dataSource={this.dataSource}
renderRow={this.renderRow}
style={{ backgroundColor: '#D8D8D8' }}
/>
);
}
}
const mapStateToProps = state => {
const posts = _.map(state.feed, (val, uid) => {
return { ...val, uid };
});
return { posts };
};
export default connect(mapStateToProps, { postsFetch })(FeedComponent);
This then performs this action:
export const postsFetch = () => {
return (dispatch) => {
firebase.database().ref('/social/posts')
.on('value', snapshot => {
dispatch({ type: POSTS_FETCH_SUCCESS, payload: snapshot.val() });
});
};
};
and dispatches it to the reducer.
Here is the listItem:
#withNavigation
class PostItem extends Component {
state = {
result: ''
};
goToComments() {
this.props.navigation
.getNavigator('root')
.push(Router.getRoute('comments', { posts: this.props.posts }));
}
render() {
const {
text,
author,
vote_count,
comment_count,
image,
created_at
} = this.props.posts;
const fromNow = moment(created_at).fromNow();
return (
<Card style={styles.cardStyle}>
<TouchableOpacity onPress={this.goToComments.bind(this)}>
//Text/Image component using props
</TouchableOpacity>
</Card>
);
}
}
There, you can see that there's an goToComments function, it passes the post props
This is the comment screen:
class Comments extends Component {
render() {
return (
<View style={{ flex: 1 }}>
<PostSection
post={this.props.route.params.posts}
/>
</View>
);
}
}
This feeds the props into the postSection component which displays the post in the comments screen. It is this that doesn't update after I go to the edit screen, make the edit and come back:
#withNavigation
class PostSection extends Component {
editPost() {
this.props.startEdit(this.props.post);
this.props.navigation
.getNavigator('root')
.push(Router.getRoute('editPost'));
}
renderImage(image) {
if (image) {
return (
<View style={styles.imageContainer}>
<Image
style={styles.imageSize}
source={{ uri: image }}
/>
</View>
);
}
}
renderText(text) {
if (text) {
return <PostText text={text} />;
}
}
renderPost(text, image) {
// if (POST BELONGS TO CURRENT USER) {
return (
<TouchableOpacity onPress={this.editPost.bind(this)}>
{this.renderText(text)}
{this.renderImage(image)}
</TouchableOpacity>
);
}
render() {
const {
text,
author,
vote_count,
comment_count,
image,
created_at
} = this.props.post;
const fromNow = moment(created_at).fromNow();
return (
<View>
<TopBar
author={author}
fromNow={fromNow}
/>
{this.renderPost(text, image)}
<BottomBar
voteCount={vote_count}
commentCount={comment_count}
/>
</View>
);
}
}
export default connect(null, { startEdit })(PostSection);
Related
I have a PaymentMethodsScreen screen. On this screen there is a FlatList with PaymentCardItem components inside. And there is a checkbox inside the PaymentCardItem. When this checkbox checked I would like to update selectedCardToken state of PaymentMethodsScreen. But unfortunately I couldn't figure out how to do it. I tried to pass props but I was doing it wrong. Here is my code (without passing props).
How can I achieve that? Thank you very much for your helps.
const PaymentCardItem = ({ family, association, bin_number, token, isSelected }) => (
<View>
<RadioCheckbox
selected={ isSelected }
onPress={ () => this.setSelectedCardToken(token) // Something wrong here }
/>
<Text>{family}, {association}</Text>
<Text>{bin_number}**********</Text>
</View>
);
const PaymentMethodsScreen = ({navigation}) => {
const {state} = useContext(AuthContext);
const [cardList, setCardList] = useState(null) // This stores card list data from API request
const [selectedCardToken, setSelectedCardToken] = useState('test token')
const renderItem = ({ item }) => (
<PaymentCardItem
bin_number={item.bin_number}
family={item.family}
association={item.association}
token={ item.token }
isSelected={ (selectedCardToken == item.token) }
/>
);
return (
<SafeAreaView>
<View>
<FlatList
data={cardList}
renderItem={renderItem}
keyExtractor={item => item.alias}
/>
</View>
</SafeAreaView>
);
};
add onPress prop to PaymentCardItem:
// PaymentMethodsScreen
<PaymentCardItem
onPress={() => setSelectedCardToken(item.token)}
>
I don't know how the PaymentCardItem component is structured, but generally you should add onPress prop on the TouchableOpacity in the component or whatever is your onPress handler:
// PaymentCardItem component
<TouchableOpacity
onPress={() => props.onPress()}
>
You can pass down the handler function which gets called on checkbox being checked or unchecked to your PaymentCardItem component.
You can also pass setSelectedCardToken directly, but in case you have some extra logic before you update state, it's better to have a handler for more readability.
So, the code will be like below.
const PaymentMethodsScreen = ({ navigation }) => {
const { state } = useContext(AuthContext);
const [cardList, setCardList] = useState(null) // This stores card list data from API request
const [selectedCardToken, setSelectedCardToken] = useState('test token')
const handleCardTokenSelection = (isTokenSelected) => {
if(isTokenSelected) {
setSelectedCardToken(); // whatever logic you have
} else {
setSelectedCardToken(); // whatever logic you have
}
}
const renderItem = ({ item }) => (
<PaymentCardItem
bin_number={item.bin_number}
family={item.family}
association={item.association}
token={ item.token }
isSelected={ (selectedCardToken == item.token) }
handleCardTokenSelection={handleCardTokenSelection}
/>
);
return (
<SafeAreaView>
<View>
<FlatList
data={cardList}
renderItem={renderItem}
keyExtractor={item => item.alias}
/>
</View>
</SafeAreaView>
);
};
const PaymentCardItem = ({ family, association, bin_number, token, isSelected, handleCardTokenSelection }) => (
<View>
<RadioCheckbox
selected={ isSelected }
onPress={handleCardTokenSelection}
/>
<Text>{family}, {association}</Text>
<Text>{bin_number}**********</Text>
</View>
);
You need to set the state for PaymentCardItem not for the whole Flatlist, to show the item is selected.
I think you update the PaymentCardItem component to something like the below code(You can update the logic as per requirement)
class PaymentCardItem extends React.Component {
constructor(props) {
super(props);
this.state = {selectedCardToken: "", isSelected: false};
}
setSelectedCardToken=(token)=>{
if(selectedCardToken == token){
this.setState({
selectedCardToken: token,
isSelected: true
})
}
}
render() {
const { family, association, bin_number, token }=this.props;
const { isSelected } = this.state;
return (
<View>
<RadioCheckbox
selected={ isSelected }
onPress={ () => this.setSelectedCardToken(token)
/>
<Text>{family}, {association}</Text>
<Text>{bin_number}**********</Text>
</View>
);
}
}
I'm new to reactnative and firebase and i looked for any infomation on this but had no luck in figuring out how to do this. Basically i want to display data from firebase which is specific to the user currently logged in, ive gotten halfway thru but don't understand what to do next. can someone show me what the rest of the code needs to be
Heres my code:
class HomeScreen extends Component {
constructor() {
super();
this.state = {
uid: ''
}
}
readUserData() {
currentUser = firebase.auth().currentUser
var that = this
firebase.database().ref(`BorrowedBooks`).child(currentUser.uid).on('value', function (data) {
console.log(data.val())
});
}
signOut = () => {
firebase.auth().signOut().then(() => {
this.props.navigation.navigate('Login')
})
.catch(error => this.setState({ errorMessage: error.message }))
}
render() {
this.state = {
displayName: firebase.auth().currentUser.displayName,
uid: firebase.auth().currentUser.uid
}
return (
<View style={styles.container}>
<Text style = {styles.textStyle}>
Hello, {this.state.displayName}
</Text>
<View>
<FlatList/>
</View>
<Button
color="#3740FE"
title="Logout"
onPress={() => this.signOut()}
/>
<Button
color="#3740FE"
title="display books"
onPress={this.readUserData}
/>
</View>
);
}
}
This is what my firebase database looks like: firebase database
This is the response after console.log: console.log output
you need to store the data in an array before you can display it in FlatList
readUserData() {
currentUser = firebase.auth().currentUser
var that = this
firebase.database().ref(`BorrowedBooks`).child(currentUser.uid).on('value', function (data) {
//console.log(data.val())
let DATA = [];
if(data.exists()){
KEY = Object.keys(data.val());
KEY.forEach( (key_id) => {
let a = snapshot.val()[key_id];
a.key = key_id;
DATA.push(a);
})
that.setState({userData: DATA})
});
}
then you can display it in flatlist
<FlatList
data = {this.state.userData}
renderItem ={({item}) =>
//render Items here
}
/>
I am wonder if in screenA I have an object data = {} that will be changed dynamically, can I receive changes in screenB by just sending this props from screenA through this.props.navigation.navigate('screenB', {data})?
And in screenB to have a componentWillReceiveProps(nextProps) to get this changes through something like nextProps.navigation.state.param.data
Or there is a way to achieve this?
You can use onWillFocus of NavigationEvents, which fires whenever the screen is navigated to.
_willFocus = () => {
const { navigation } = this.props
const data = navigation.getParam('data', null)
if (data !== null) {
/* do something */
}
}
/* ... */
render () {
return (
<View>
<NavigationEvents onWillFocus={_willFocus()}
</View>
)
}
It is easy, just as you said: send some data navigation.navigate('screenB', { data }) and receive it in the screenB as navigation.state.params.data.
I agree with #FurkanO you probably show use Redux instead to control all the state of your app, but for simple stuff I think isn't necessary!
I made a simple snack demo to show you: snack.expo.io/#abranhe/stackoverflow-56671202
Here some code to follow up:
Home Screen
class HomeScreen extends Component {
state = {
colors: ['red', 'blue', 'green'],
};
render() {
return (
<View>
{this.state.colors.map(color => {
return <Text>{color}</Text>;
})}
<View>
<Text>Details Screen</Text>
<Button
title="Go to Details"
onPress={() => this.props.navigation.navigate('Details', { colors: this.state.colors })}
/>
</View>
</View>
);
}
}
Details Screen
class DetailsScreen extends Component {
state = {
colors: [],
};
componentWillMount() {
this.setState({ colors: this.props.navigation.state.params.colors });
}
render() {
return (
<View>
{this.state.colors.map(color => {
return <Text>{color}</Text>;
})}
<Text>Details Screen</Text>
</View>
);
}
}
Update
The question's author requested an update to add a setTimeout() to see the exact moment when the data is on the other screen, so it will look like this:
componentWillMount() {
setTimeout(() => {
this.setState({ colors: this.props.navigation.state.params.colors });
}, 3000);
}
I am creating a currency converter app and it will retrieve currency value from the API and multiply with the text input for the result. Both the API result and Text input are stored in State and passing as props to the Component
import React from 'react';
import { StyleSheet, Text, View,TextInput,Button } from 'react-native';
import DisplayResult from './src/DisplayResult'
export default class App extends React.Component {
state = {
currency:'',
pokeList: '',
}
placeNameChangeHandler=(val)=>{
this.setState({currency:val});
}
// console.log(this.state.currency);
async findCurrency () {
try {
//Assign the promise unresolved first then get the data using the json method.
const pokemonApiCall = await fetch('https://free.currconv.com/api/v7/convert?q=KWD_INR&compact=ultra&apiKey={my_api_Key}');
const pokemon = await pokemonApiCall.json();
this.setState({pokeList: pokemon['KWD_INR']});
// console.log(pokemon);
} catch(err) {
console.log("Error fetching data-----------", err);
};
<DisplayResult convert={this.state.pokeList} result={this.state.currency} />
}
render() {
return (
<View style={styles.container}>
<TextInput
placeholder="Currency"
value = {this.state.currency}
onChangeText={this.placeNameChangeHandler}
/>
<Button
title="Search"
onPress={this.findCurrency.bind(this)}
/>
</View>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
alignItems: 'center',
justifyContent: 'center',
},
});
DisplayResult
const DisplayResult =(props)=>{
const {convert,result} = props
console.log(convert);
return (
<View>
<Text>{result*convert}</Text>
</View>
)
}
export default DisplayResult;
I am trying to pass the API result and text input to the display component and this will multiply the values and will give the result.
Now this is not functioning or giving result
why this is not showing and where it's going wrong?
In your findCurrency method you just "call" the DisplayResult without returning it, but I don't think this is the good method to display your result.
You can use your component directly within the render method by testing your state variables, like this :
findCurrency = async () => {
try {
const pokemonApiCall = await fetch(
"https://free.currconv.com/api/v7/convert?q=KWD_INR&compact=ultra&apiKey={my_api_Key}"
);
const pokemon = await pokemonApiCall.json();
this.setState({ pokeList: pokemon["KWD_INR"] }); // You set your "pokeList" variable up
} catch (err) {
console.log("Error fetching data-----------", err);
}
}
Note that you remove the DisplayResult call here and the function became an arrowed function, then in your render method use the test to make your result appear only if pokeList isn't empty :
render() {
return (
<View style={styles.container}>
<TextInput
placeholder="Currency"
value={this.state.currency}
onChangeText={this.placeNameChangeHandler}
/>
<Button title="Search" onPress={this.findCurrency.bind(this)} />
{this.state.pokeList !== "" && (
<DisplayResult
convert={this.state.pokeList}
result={this.state.currency}
/>
)}
</View>
);
}
Then, you don't have to bind your function in the onPress method like this, JavaScript immediately calls the function if you do this, instead, use arrow functions, you can access this by doing so in your function AND the onPress method doesn't call it if you don't click on the button, you just have to specify which function to execute when clicked :
<Button title="Search" onPress={this.findCurrency} />
If you have parameters in your function, use an arrow function instead :
<Button title="Search" onPress={() => yourFunction(param)} />
This should do the trick.
Try writing your function like that :
const findCurrency = async() => {
// ...
};
and call it like that
<Button
title="Search"
onPress={() => this.findCurrency()}
/>
I personnaly never use .bind because I think this is very unclear.
try using conditional rendering,
if data fetched, then only render.
import React from 'react';
import { StyleSheet, Text, View,TextInput,Button } from 'react-native';
import DisplayResult from './src/DisplayResult'
export default class App extends React.Component {
state = {
currency: '',
pokeList: '',
}
placeNameChangeHandler=(val)=>{
this.setState({currency:val});
}
// console.log(this.state.currency);
this.findCurrency.bind(this);
async findCurrency () {
try {
//Assign the promise unresolved first then get the data using the json method.
const pokemonApiCall = await fetch('https://free.currconv.com/api/v7/convert?q=KWD_INR&compact=ultra&apiKey={my_api_Key}');
const pokemon = await pokemonApiCall.json();
this.setState({pokeList: pokemon['KWD_INR']});
// console.log(pokemon);
} catch(err) {
console.log("Error fetching data-----------", err);
};
}
render() {
return (
<View style={styles.container}>
<TextInput
placeholder="Currency"
value = {this.state.currency}
onChangeText={this.placeNameChangeHandler}
/>
<Button
title="Search"
onPress={this.findCurrency()}
/>
</View>
{
if(this.state.pokeList !== '' || this.state.currency !== '') ?
<DisplayResult convert={this.state.pokeList} result={this.state.currency} /> : <div></div>
}
);
}
}
I'm building react-native app, but my problem is linked with React itself.
It's an app that connects to external JSON, fetches data, and creates react component for each of item in that JSON data, so it's like 70 child components inside 1 wrapper. App is also using Navigator and phone storage but that's a part of the question.
To visualize:
Parent component (People) has methods to operate on a DB, it fetches data, creates component for each of item in array and exposes methods to child components (Person). Each person has a "add to favourites" button, this is a method updating empty star to full one (conditional rendering) if clicked, updates state of component and whenever local state has been changed it fires parents component to update it's state - and from there parent's component saves all data to DB. So I've made a link to synchronize Child's State -> Parent's State -> Local DB (phone's memory).
Problem is, it's quite slow when updating parent's state. It freezes for 1-1.5 sec. but it's all cool if I remove method to update parent's state (I've marked that in example attached).
Question 1: How to refactor this code to fix performance issue when updating parent's (People's state)?
Question 2: I'm open to any other suggestions and lessons how to improve quality of my code.
Here's a link to visualize my code, I've just removed few non-relevant methods and styles.
https://jsfiddle.net/xvgfx90q/
class People extends React.Component {
constructor() {
super();
this.state = {
peopleData: [],
database: {}
}
}
componentDidMount() {
this.fetchApi();
this.syncDatabase();
}
// function that connects to external JSON file and parses it
fetchApi() {... it sets peopleData state after promise has been resolved}
// function called from PersonSection to pass it's state and update main state of People
syncStates(data) {
const newState = this.state;
newState.database[data.id] = data;
this.setState(newState); // <-- !! PERFORMANCE DROP HERE !!
this.saveDatabase();
}
// connects to phone's DB and updates state with result of promise
async syncDatabase() {
AsyncStorage.getItem(this.state.DBKey).then((data) => {
let newState = {};
newState.database = JSON.parse(data);
this.setState(newState);
}).catch((error) => {
return error;
})
}
// saves current state to DB
async saveDatabase() {
AsyncStorage.setItem(this.state.DBKey, JSON.stringify(this.state.database));
}
renderTeams() {
return Object.keys(this.state.peopleData).map((team) => {
return (
<TeamSection key={team} teamName={team} membersList={this.state.peopleData[team]}>
{this.renderPeople(team)}
</TeamSection>
)
})
}
renderPeople(team) {
return this.state.peopleData[team].map((people) => {
return (
<PersonSection
key={people.id}
data={people}
database={_.has(this.state.database, people.id) ? this.state.database[people.id] : false}
navigator={this.props.navigator}
syncStates={this.syncStates.bind(this)}
/>
)
})
}
render() {
return (
<ScrollView style={styles.wrapper}>
<Options filterPeople={this.filterPeople.bind(this)} />
{this.renderTeams()}
</ScrollView>
)
}
}
class PersonSection extends Component {
constructor(props) {
super(props);
this.state = {
database: {
id: this.props.data.id,
name: this.props.data.name,
favourites: this.props.database.favourites
}
}
}
// updates components state and sends it to parent component
toggleFavourites() {
const newState = this.state.database;
newState.favourites = !newState.favourites;
this.setState(newState);
this.props.syncStates(this.state.database);
}
render () {
return (
<View>
<View>
<View>
<Text>{this.props.data.name}</Text>
<Text>{this.props.data.position}</Text>
<Text>{this.props.data.ext}</Text>
</View>
<View>
<TouchableOpacity onPress={() => this.toggleFavourites()}>
{ this.state.database.favourites
? <Icon name="ios-star" size={36} color="#DAA520" />
: <Icon name="ios-star-outline" size={36} color="#DAA520" />}
</TouchableOpacity>
</View>
</View>
</View>
)
}
};
export default PersonSection;
React.render(<People />, document.getElementById('app'));`
This is not a recommended way to do it, but basically you can just update the child state instead of the parent and passing it back down.
class People extends React.Component {
constructor() {
super();
this.state = {
peopleData: [],
database: {}
}
}
componentDidMount() {
this.fetchApi();
this.syncDatabase();
}
// function that connects to external JSON file and parses it
fetchApi() {... it sets peopleData state after promise has been resolved}
// function called from PersonSection to pass it's state and update main state of People
syncStates(data) {
this.state.database[data.id] = data;
this.saveDatabase();
}
// connects to phone's DB and updates state with result of promise
async syncDatabase() {
AsyncStorage.getItem(this.state.DBKey).then((data) => {
let newState = {};
newState.database = JSON.parse(data);
this.setState(newState);
}).catch((error) => {
return error;
})
}
// saves current state to DB
async saveDatabase() {
AsyncStorage.setItem(this.state.DBKey, JSON.stringify(this.state.database));
}
renderTeams() {
return Object.keys(this.state.peopleData).map((team) => {
return (
<TeamSection key={team} teamName={team} membersList={this.state.peopleData[team]}>
{this.renderPeople(team)}
</TeamSection>
)
})
}
renderPeople(team) {
return this.state.peopleData[team].map((people) => {
return (
<PersonSection
key={people.id}
data={people}
database={_.has(this.state.database, people.id) ? this.state.database[people.id] : false}
navigator={this.props.navigator}
syncStates={this.syncStates.bind(this)}
/>
)
})
}
render() {
return (
<ScrollView style={styles.wrapper}>
<Options filterPeople={this.filterPeople.bind(this)} />
{this.renderTeams()}
</ScrollView>
)
}
}
class PersonSection extends Component {
constructor(props) {
super(props);
this.state = {
database: {
id: this.props.data.id,
name: this.props.data.name,
favourites: this.props.database.favourites
}
}
}
// updates components state and sends it to parent component
toggleFavourites() {
const newState = this.state.database;
newState.favourites = !newState.favourites;
this.setState(newState);
this.props.syncStates(this.state.database);
}
render () {
return (
<View>
<View>
<View>
<Text>{this.props.data.name}</Text>
<Text>{this.props.data.position}</Text>
<Text>{this.props.data.ext}</Text>
</View>
<View>
<TouchableOpacity onPress={() => this.toggleFavourites()}>
{ this.state.database.favourites
? <Icon name="ios-star" size={36} color="#DAA520" />
: <Icon name="ios-star-outline" size={36} color="#DAA520" />}
</TouchableOpacity>
</View>
</View>
</View>
)
}
};
export default PersonSection;
React.render(<People />, document.getElementById('app'));