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'));
Related
UNSAFE_componentWillMount(){
this.props.EmployeeFetch();
}
renderRow(employee){
return <ListItem employee={employee}/>;
}
render(){
return(
<FlatList style={{flex:1,height:100}}
data = {this.props.employees}
/>
);
}
}
const mapStateToProps=state=>{
const employees = _.map(state.employees,(val,uid)=>{
return {...val,uid};
});
return {employees};
}
export default connect(mapStateToProps, {EmployeeFetch})(EmployeeList);
Here I am fetching data from firebase. At first it came null and after some time data came. So, how will I re-render the new data using the Flatlist and componentWillRecieveProps()?
you should use 2 return function.
1 which return loading (activity indicator)
2 which return data if data exists in any state.
constructor(props) {
super(props);
this.state = { isLoading: true};
}
render() {
if(this.state.isLoading){
return(
<View>
<ActivityIndicator size="large" color="#0c9"/>
</View>
)}
return(
<FlatList style={{flex:1,height:100}}
data = {this.props.employees}
/>
);
const mapStateToProps=state=>{
const employees = _.map(state.employees,(val,uid)=>{
return {...val,uid};
});
if(employees){
this.state.isLoading = false;
}
return {employees};
}
note: do not forget to import react components
and also if you are using react navigation then try to use to navigation events for data fetch api rather than using UNSAFE_componentWillMount() or find some other solution.
So componentWillReceiveProps is deprecated, you should use getDerivedStateFromProps.
You can use it like this with your example:
static getDerivedStateFromProps(props, state) {
const { employees } = this.props;
return employees;
}
Then in your render:
render() {
const { employees } = this.props;
// then use map(employees)
}
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 trying to implement react native search filter. I have my data array on the parent screen and I applied the search filter on the parent file and passed the filtered data array as props to the child screen. But no filter is happening. I am confused or no clue what wrong i am doing. I have the following codes:
ParentScreen.js
import SearchInput, { createFilter } from 'react-native-search-filter';
...
const KEYS_TO_FILTERS = ['title']
export default class ProductScreen extends Component {
constructor(props) {
super(props)
this.state = {
items: API,
searchTerm: '',
}
}
searchUpdated(term) {
this.setState({ searchTerm: term })
}
render() {
const filteredItems = this.state.items.filter(createFilter(this.state.searchTerm, KEYS_TO_FILTERS))
let cardGridWithBodyProps = {
navigation: this.props.navigation,
api: filteredItems,
gridNumber: 2,
}
return (
<Container>
<ScrollView showsVerticalScrollIndicator={false}>
<View>
<Header>
<SearchInput
onChangeText={(term) => { this.searchUpdated(term) }}
placeholder="Search"
/>
</Header>
</View>
<View>
<CardGridWithBody {...cardGridWithBodyProps} />
</View>
</ScrollView>
</Container>
)
}
}
ChildScreen.js
export default class CardGridWithBody extends Component {
constructor(props) {
super(props)
this.state = {
items: this.props.api
}
}
renderGrid(gridArray) {
return gridArray.map((row, rowIndex) => (
<Row key={rowIndex}>
{row.map((col, colIndex) => (this.renderColumn(col, rowIndex,colIndex)))}
</Row>
));
}
renderColumn(colItem, rowIndex, colIndex) {
return (
<Col key={colIndex}>
<Text>{colItem.title}</Text>
</Col>
)
}
renderContent() {
let gridArray = this.state.items
return this.renderGrid(gridArray)
}
render() {
return (
this.renderContent()
)
}
}
Instead of saving the data in state, access it directly from props. If you save it in state, you'll need to update it manually using lifecycle methods such as shouldComponentUpdate or getDerivedStateFromProps
renderContent = () => {
let gridArray = this.props.api;
return this.renderGrid(gridArray);
}
In parent screen convert
searchUpdated(term) {
this.setState({ searchTerm: term })
}
to
searchUpdated = term => {
this.setState({ searchTerm: term })
}
and in your child component you can do
static getDerivedStateFromProps(nextProps, prevState) {
if (nextProps.api !== prevState.api) {
return { api: nextProps.api };
}
return null;
}
componentDidUpdate(prevProps, prevState) {
if (
this.state.items !==
prevState.items
) {
this.setState({ items: api });
}
}
I am working on a react-native and meteor js project.
My problem is that the props received from withTracker() function are only received in componentDidUpdate(prevProps) I don't get them in constructor or componentWillMount.
Another issue is when i pass props directly from parent to child. it receives them late due to which my component does not update
iconGroups prop comes from withTracker() method
and openSection props which i am using in this showGroupIcons()
is passed directly from parent to this component.
I want to open Accordian section that is passed to it via parent. but problem is in componentDidUpdate(prevProps) I am changing state due to which component re-renders.
openSection variable by default value is Zero. when props arrvies it value changes which i required But Accordian does not update.
Below is my code
import React, { Component } from 'react';
import Meteor, { withTracker } from 'react-native-meteor';
import {
View, Image, ScrollView, TouchableOpacity,
} from 'react-native';
import PopupDialog from 'react-native-popup-dialog';
import {Text, Icon, Input, Item, List,} from 'native-base';
import Accordion from 'react-native-collapsible/Accordion';
import { Col, Row, Grid } from 'react-native-easy-grid';
import styles from './styles';
import CONFIG from '../../config/constant';
import {MO} from "../../index";
const staticUrl = '../../assets/img/icons/';
class IconPickerComponent extends Component {
constructor(props) {
super(props);
this.state = {
dataSource: [],
itemName: 'apple1',
activeSections: 0,
showAccordian: true,
accordianData: []
};
}
componentDidUpdate(prevProps) {
if(prevProps.iconGroups !== this.props.iconGroups) {
let images = this.props.iconGroups.map(icon => icon.images);
let flatten = [].concat.apply([], images).map(img => { return {name: img, icon: CONFIG.ICON_URL+img+'.png'} })
this.setState({ filteredItems: flatten, dataSource: flatten, accordianData: this.props.iconGroups });
}
}
componentDidMount() {
this.props.onRef(this);
}
componentWillUnmount() {
this.props.onRef(null);
}
method() {
// this.setState(...this.state,{
// searchText: ''
// })
this.iconPicker.show(); // show icon picker
}
onSearchChange(text) {
this.setState({
showAccordian: !(text.length > 0)
});
const searchText = text.toLowerCase();
const filteredItems = this.state.dataSource.filter((item) => {
const itemText = item.name.toLowerCase();
return itemText.indexOf(searchText) !== -1;
});
this.setState({ filteredItems });
}
onIconSelect(item) {
this.setState({
itemName: item,
});
this.iconPicker.dismiss();
if (this.props.onIconChanged) {
this.props.onIconChanged(item);
}
}
_renderSectionTitle = section => {
return (
<View style={styles.content}>
<Text></Text>
</View>
);
};
_renderHeader = section => {
return (
<View style={styles.accordHeader}>
<Text style={{color: 'white'}}>{this.state.showAccordian} - {section.group}</Text>
<Text>
<Icon style={styles.downArrow} name="ios-arrow-down" />
</Text>
</View>
);
};
_renderContent = section => {
return (
<View style={styles.accordContent}>
{
section.images.map((img, key) => (
<TouchableOpacity onPress={() => this.onIconSelect(img)} key={key}>
<View style={styles.iconsGrid}>
<Image style={styles.image} source={{uri: CONFIG.ICON_URL+ img + '.png'}}/>
</View>
</TouchableOpacity>
))
}
</View>
);
};
_updateSections = activeSections => {
this.setState({ activeSections });
};
hasGroupIcons() {
return this.props.iconGroups.length > 0;
};
showGroupIcons() {
if(this.state.showAccordian){
let openSection;
if(!!this.props.openSection) {
let groupIndex = this.state.accordianData.findIndex(icon => icon.group === this.props.openSection);
if(groupIndex !== -1) {
openSection = groupIndex;
} else {
openSection = 0;
}
} else {
openSection = 0;
}
return(<Accordion
sections={this.state.accordianData}
activeSections={this.state.activeSections}
renderSectionTitle={this._renderSectionTitle}
renderHeader={this._renderHeader}
renderContent={this._renderContent}
onChange={this._updateSections}
initiallyActiveSection={openSection} />);
} else {
return(<View style={{flexWrap: 'wrap', flexDirection: 'row'}}>
{
this.state.filteredItems.map((item, key) => (
<TouchableOpacity onPress={() => this.onIconSelect(item.name)} key={key}>
<View style={styles.iconsGrid}>
<Image style={styles.image} source={{uri: item.icon}}/>
</View>
</TouchableOpacity>
))
}
</View>)
}
};
render() {
return (
<PopupDialog
overlayOpacity={0.8}
overlayBackgroundColor="#414141"
dialogStyle={styles.dialogBox}
containerStyle={styles.dialogContainer}
ref={(popupDialog) => { this.iconPicker = popupDialog; }}
>
<ScrollView>
<View style={styles.dialogInner}>
<Item searchBar rounded style={styles.searchbar}>
<Icon style={styles.searchIcon} name="search" />
<Input onChangeText={this.onSearchChange.bind(this)} style={styles.inputSearch} placeholder="Search" />
</Item>
{
this.hasGroupIcons() && this.showGroupIcons()
}
</View>
</ScrollView>
</PopupDialog>
);
}
}
export default withTracker(params => {
MO.subscribe('ipSubsId3', 'IconGroups');
return {
iconGroups: MO.collection('IconGroups', 'ipSubsId3').find({}),
};
})(IconPickerComponent);
I am new to react. I am assuming when props change component re-renders.
Use this life cycle method
static getDerivedStateFromProps(prevProps, prevState) {
if(prevProps.iconGroups !== this.props.iconGroups) {
let images = this.props.iconGroups.map(icon => icon.images);
let flatten = [].concat.apply([], images).map(img => { return {name: img, icon: CONFIG.ICON_URL+img+'.png'} })
this.setState({ filteredItems: flatten, dataSource: flatten, accordianData: this.props.iconGroups });
}
}
getDerivedStateFromProps is invoked right before calling the render method, both on the initial mount and on subsequent updates. It should return an object to update the state, or null to update nothing.
Read more about this lifecycle method here
I have fixed this issue. Actually my concepts were not right. I thought props are first received in constructor and componentWillMount. But I get all props in render() and everything works fine i dont have to use any lifecycle method to use props now
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);