How could I structure this React Native Section List implementation - reactjs

So I want to use RN Section list in a sort of unorthodox way.
I want the section list to pass off rendering to a component as the renderings won't be very uniform.
I want to use section list so as you scroll you still get to see the headers.
I made a component that takes in children and renders them in a section list like so:
class SomeSectionList extends Component {
render() {
let sections = React.Children.map(this.props.children, (Child, index) => {
return {title: Child.type.title, data: [''], renderItem: () => Child, index }
});
return (
<SectionList
renderSectionHeader={({section}) => {
return <Text style={{ fontWeight: "bold" }}>{section.title}</Text>
}}
sections={sections}
keyExtractor={(item, index) => item + index}
/>
);
}
}
And the usage would be something like:
<SomeSectionList>
<Comp1 />
<Comp2 />
</SomeSectionList>
However, my issue is. Say in this case Comp1 does not render anything from it's component, I want to be able to hide it's section from the section list.
How could the SomeSectionList component know that it didn't render anything or didn't have the data to render anything so it can hide it's section and it's header?
Any suggestions would be great. I feel like using SectionList for this is overkill (but it makes showing the headers nicer) so open to alternatives as well.

You can accomplish this using onLayout method that comes with View.
By which we can get the height of the component rendered. if it is 0 that means nothing is rendered inside it or else it contains some data.
See this Working example on snack
export default class App extends React.Component {
render() {
return (
<SomeSectionList>
<Comp1 />
<Comp2 />
<Comp1 />
<Comp2 />
<Comp1 />
</SomeSectionList>
);
}
}
class Comp1 extends React.Component {
render() {
return (
<View>
<Text>Comp11</Text>
</View>
);
}
}
class Comp2 extends React.Component {
render() {
return null;
}
}
class SomeSectionList extends React.Component {
constructor(props) {
super(props);
this.state = {
children: this.props.children,
};
}
onLayout = (event, index) => {
if (event.nativeEvent.layout.height <= 0) {
let oldProps = this.state.children;
oldProps.splice(index, 1);
this.setState({ children: oldProps });
}
};
render() {
let sections = React.Children.map(this.state.children, (Child, index) => {
return {
title: Child.type.title,
data: [''],
renderItem: () => (
<View onLayout={event => this.onLayout(event, index)}>
{this.state.children[index]}
</View>
),
index,
};
});
return (
<SectionList
renderSectionHeader={({ section }) => {
return <Text style={{ fontWeight: 'bold' }}>{section.title}</Text>;
}}
sections={sections}
keyExtractor={(item, index) => item + index}
/>
);
}
}
Here, first of all, we have assigned this.props.children into state. Then in onLayout method, we are checking if the current indexed child has 0 height or not. if yes then remove it from the array of children.
You'll see clearly that some views are deleting. for this thing what we have done in one scenario is put one loader that covers the whole SectionList area with position absolute and you can hide it when all things are rendered correctly.

Here is an alternate that I am thinking after the helpful advice of #JaydeepGalani!!
class SomeSectionList extends Component {
constructor(props) {
super(props)
this.state = {
hiddenChildren: {}
}
}
onLayout = (event, index) => {
if (event.nativeEvent.layout.height <= 0) {
const hiddenChildren = this.state.hiddenChildren
hiddenChildren[index] = true
this.setState({
hiddenChildren
})
} else {
const hiddenChildren = this.state.hiddenChildren
delete hiddenChildren[index]
this.setState({
hiddenChildren
})
}
}
render() {
let sections = React.Children.map(this.props.children, (Child, index) => {
return {
title: Child.type.title,
index,
data: [''],
renderItem: () => (
<View onLayout={event => this.onLayout(event, index)}>
{this.state.children[index]}
</View>
)}
});
return (
<SectionList
renderSectionHeader={({section}) => {
const index = section.index
if (this.state.hiddenChildren[index]) return
return <Text style={{ fontWeight: "bold" }}>{section.title}</Text>
}}
sections={sections}
keyExtractor={(item, index) => item + index}
/>
);
}
}
Since in the first implementation once the section got removed it's really hard to bring it back as the onLayouts don't get triggered. In this we still technically 'render' the section, but hide the header and since the section is of height 0 it won't show up, but it's still rendered and say at a later point in time that section changes and suddenly renders something it will now show up in the section list.
Curious on any feedback around this?

Related

Can I subscribe to this.props.navigation.state.params?

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);
}

React Component Props are receiving late. (Meteor JS)

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

Why is child component not receiving updated value from parent in React Native?

When an arrow is clicked the Cart Item View needs to expand that particular view and collapse any others currently expanded. The item id of that product is passed to the parent component to update which view is to be expanded (active). Although, the id is being passed and set on the expandedItem property in the reducer this does not get updated to the child component (even though it's being passed as prop on the child component). When the child component is re-evaluated at the end the expandedViewItem is still 0, which is it's default value. Does anyone know how to get the child component to receive the updated expandedItem value? Why is it still 0??
PLEASE watch the video I made debugging this issue: https://youtu.be/qEgxqAyWDpY
Here is where the value is evaluated in the child component:
render () {
const isExpanded = product.id === this.props.expandedViewId;
Here is the entire child component class:
export default class CartProductItem extends Component {
constructor(props) {
super(props);
this.state = {showCounter: false};
}
expandCartProductItem(id) {
this.props.onExpandClick(id);
this.setState(this.state);
}
updateDisplay = (nextProps) => {
// Null check is needed here as 0 is a valid value
if (nextProps.activeIndex !== null && nextProps.activeIndex === this.props.index) {
this.setState({
showCounter: !this.state.showCounter
});
} else if (nextProps.activeIndex !== null && nextProps.activeIndex !== this.props.index) {
this.setState({
showCounter: false
});
}
}
componentWillReceiveProps(nextProps) {
console.log('nextProps: ', nextProps)
}
render() {
const serverUrl = getServerAddress();
const {product} = this.props;
const price = product.iogmodPrice ? product.iogmodPrice : product.price;
const isExpanded = product.id === this.props.expandedViewId;
const imageSrc = product.imageName
? 'https://b2b.martinsmart.com/productimages/'+ product.imageName.replace("Original", "Thumbnail")
: serverUrl + '/b2b/resources/images/nophoto.gif';
return (
<View style={styles.pContainer}>
<CartProduct
imageName={imageSrc}
name={product.description}
itemNum={product.id}
price={price}
pack={product.pack}
averageWeight={product.averageWeight}
cases={product.order_count}
/>
<TouchableOpacity style={styles.buttonContainer} onPress={() => this.expandCartProductItem(product.id)}>
{isExpanded ? <LineIcon name="arrow-up" style={styles.arrowIcon} /> : <LineIcon name="arrow-down" style={styles.arrowIcon} />}
</TouchableOpacity>
{isExpanded &&
<ExpandHeightView height={70}>
<View style={styles.counterContainerView}>
<QuantityCounter style={{width: '100%'}} product={product} />
</View>
</ExpandHeightView>
}
</View>
);
}
}
Here is the parent component function that passes the id to the action and the initial const declarations in the render:
expandAndCollapseItems = (id) => {
this.props.dispatch(collapseCartItemViews(id));
}
render() {
const {isLoading, displayDetails, sortCasesAsc, details, items, expandedItem} = this.props.orderInfo;
Here is the child component in the parent component where the expandedItem variable is being passed into it:
<FlatList
data={items}
keyExtractor={(item, index) => item.id.toString()}
renderItem={({item}) => <CartProductItem product={item} expandedViewId={expandedItem} onExpandClick={(id) => this.expandAndCollapseItems(id)} />}
/>
Finally, here is the reducer function updates the app state:
case types.SET_EXPANDED_STATE:
const id = action.id;
return {
...state,
expandedItem: id
}
Since you're using redux, you can just grab the expanded item id from the redux store in the child instead of passing it down from the parent.

React Native / React navigation, same level component

I have 2 classes: my default class HomeScreen used for the home page and another class MyList which I use to generate a flatlist on my HomeScreen.
My problem is that I do not succeed in building my navigation function in my MyList class.
I always get the following error: "Can't find variable: navigate".
I took a look at this Calling Navigate on Top Level Component but I really don't know how to implement it in my code.
Here's what I've tried:
class MyList extends React.Component {
_keyExtractor = (item, index) => item.note.id;
_renderItem = ({ item }) => (
<TouchableNativeFeedback
onPress={() => navigate('Note', { noteId: item.note.id })} >
<View>
<Text style={styles.noteElementTitle} >{item.note.title}</Text>
<Text style={styles.noteElementBody} >{item.note.body}</Text>
</View>
</TouchableNativeFeedback>
);
render() {
return (
<FlatList
data={this.props.data}
keyExtractor={this._keyExtractor}
renderItem={this._renderItem}
/>
);
}
}
export default class HomeScreen extends React.Component {
static navigationOptions = {
title: 'Notes',
headerStyle: { backgroundColor: 'rgb(255, 187, 0)' },
headerTitleStyle: { color: 'white' },
};
render() {
const { navigate } = this.props.navigation;
return (
<MyList
data={this.state.data}
load={this.state.load}
navig={this.props.navigation}
>
</MyList>
);
}
}
const Project = StackNavigator({
Home: { screen: HomeScreen },
NewNote: { screen: NewNoteScreen },
Note: { screen: NoteScreen }
});
AppRegistry.registerComponent('Project', () => Project);
Thanks for your help.
Because your MyList component is not part of your stack the navigation prop is not available for that.
You have 2 options.
First option you can pass the navigation prop manually to MyList
render() {
const { navigate } = this.props.navigation;
return (
<MyList
data={this.state.data}
load={this.state.load}
navigation={this.props.navigation}
>
</MyList>
);
}
Second option you can use withNavigation.
withNavigation is a Higher Order Component which passes the navigation
prop into a wrapped Component. It's useful when you cannot pass the
navigation prop into the component directly, or don't want to pass it
in case of a deeply nested child.
import { Button } 'react-native';
import { withNavigation } from 'react-navigation';
const MyComponent = ({ to, navigation }) => (
<Button title={`navigate to ${to}`} onPress={() => navigation.navigate(to)} />
);
const MyComponentWithNavigation = withNavigation(MyComponent);

Performance drop when updating state

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'));

Resources