How to set parent state from FlatList component? - reactjs

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

Related

Child component not re rendering after Parent state changes

I have a RadioGroup component that contains multiple RadioButton components. Here's the code for the RadioGroup component:
const RadioGroup = ({radioGroupData}) => {
const [radioGroupRefreshData, setRadioGroupRefreshData] = useState(radioGroupData);
const handleClick = (index) => {
setRadioGroupRefreshData(radioGroupRefreshData.map((obj, i) => {
if(i !== index) {
return {text: obj.text, isSelected: false};
}
return {text: obj.text, isSelected: true};
}));
};
return (
<View style={styles.container}>
{
radioGroupRefreshData.map((obj, i) => {
return <RadioButton index={i}
text={obj.text}
isSelected={obj.isSelected}
onClick={handleClick} />
})
}
</View>
);
}
The RadioGroup component has a state variable (an array) called radioGroupRefreshData. when each RadioButton is defined inside the RadioGroup, the handleClick function is passed as a prop in order to be called when a RadioButton is clicked. Here is the code for the RadioButton component:
const RadioButton = (props) => {
const [isSelected, setIsSelected] = useState(props.isSelected);
const initialRenderDone = useRef(false);
useEffect(() => {
if(!initialRenderDone.current) {
initialRenderDone.current = true;
}
else {
props.onClick(props.index);
}
}, [isSelected]);
const handlePress = () => {
if(!isSelected) {
setIsSelected(true);
}
}
return (
<TouchableOpacity style={styles.outsideContainer} onPress={handlePress}>
<View style={styles.radioButtonContainer}>
{ (isSelected) && <RadioButtonInnerIcon width={15} height={15} fill="#04004C" /> }
</View>
<Text style={styles.radioButtonText}>{props.text}</Text>
</TouchableOpacity>
);
}
From what I know, each RadioButton component should re render when the Parent's variable radioGroupRefreshData changes, but the RadioButton component's are not re rendering.
Thank you in advance for any help that you can give me!
Since you have a state in RadioButton you need to update it when the props change. So in RadioButton add useEffect like this:
useEffect(() => {
setIsSelected(props.isSelected);
},[props.isSelected]);
Also you don't have to mix controlled and uncontrolled behaviour of the component: do not set RadioButton state inside RadioButton since it comes from the RadioGroup

how to make flatelist a reusable component

i want to make my flatlist reusable but i am facing some difficulties while passing props.
code for the reusable component
const ListItemView = function (props) {
console.log(props);
return (
<View>
<FlatList
//data={props.data}
keyExtractor={props.keyp}
renderItem={props.disptext}
/>
</View>
);
};
when i run console.log on the props i get this
{"disptext": undefined, "keyp": [Function anonymous]}
this is how i am passing props from the parent screen
const keyf = () => {
console.log('keyf');
//for the key extractor
return (item => item.index);
};
const rendertext = () => {
console.log('rendertext');
//for rerender function of the flatlist
({ item }) => {
return (
<View>
<Text>holaa</Text>
<Text>{item.name}</Text>
</View>
);
}
};
return (
<View style={style.container}>
<ListItemView
//data={con}
keyp={keyf()}
disptext={rendertext()}
/>
</View>
);
};
please help
You are directly calling the function but you just need to give a reference of it...so that it is called only on some event.
It should be like this:
<ListItemView
//data={con}
keyp={keyf}
disptext={rendertext}
/>
or
<ListItemView
//data={con}
keyp={()=>keyf()}
disptext={()=>rendertext()}
/>

Auto focus Input within React Native Viewpager

I'm using a React Native Viewpager to take in user entry, and move to the next page on button press. Important to note that moving to the next page happens on button press, and not by normal scrolling, which is disabled.
The best way I could think to handle this was to have a state on the ViewPager, which would propagate into the child Entries.
ViewPager.tsx:
export default function ViewPager({ route, navigation }) {
const ref: React.RefObject<ViewPager> = React.createRef();
const [currentPage, setCurrentPage] = useState(0);
let setEntryPage = (page: number) => {
ref.current?.setPage(page);
setCurrentPage(page);
}
return (
<View style={{flex: 1}}>
<ViewPager
style={styles.viewPager}
initialPage={0}
ref={ref}
scrollEnabled={false}
>
{
GlobalStuff.map((entry, index) => {
return (
<Entry
key={index}
index={index}
pagerFocusIndex={currentPage}
pagerLength={quizDeck?.litems.length!}
setEntryPage={setEntryPage}
/>
)
})
}
</ViewPager>
</View>
);
};
Entry.tsx:
export function Entry(props: EntryProps) {
const inputRef: React.RefObject<Input> = React.createRef();
if (props.pagerFocusIndex === props.index) {
inputRef.current?.focus();
}
return (
<View>
<Input
// ...
ref={inputRef}
/>
<IconButton
icon="arrow-right-thick"
color={colorTheme.green}
onPress={() => {
props.index !== props.pagerLength - 1 ?
props.setEntryPage(props.index + 1) :
props.navigation!.reset({ index: 0, routes: [{ name: recapScreenName as any }] });
}}
/>
// ...
Unfortunately, inputRef appears to be null, and there is probably a better way of achieving what I'm trying to achieve anyway.
Anything in your render loop will be called every time the component renders.
// This is called on every render
const inputRef: React.RefObject<Input> = React.createRef();
// So is this, it's always null
if (props.pagerFocusIndex === props.index) {
inputRef.current?.focus();
}
Put side effects in effects.
// Untested
const inputRef = useRef();
useEffect(() => {
if (props.pagerFocusIndex === props.index) {
inputRef.current?.focus();
}
}, [inputRef.current, props.pagerFocusIndex, props.index]);

is there a way to memoize a function that returns a random color in every iteration in react

I am building a react native contact app that generates a random color for every contact number. I want to memoize the returned value of the random color generator function on every iteration so when I am searching the contacts, the color won't be changing on every action.
currently, I am using react's useMemo to memoize the function result, but I am getting an invalid hook because I can not use hooks inside a normal function with a loop.
As my code below the useMemo hook is called only on the first render or when the component re-renders. I am only able to return one value because I can't use useMemo inside the map.
here is my code ⬇. Thanks for helping
const Contacts = () => {
const [contact, setContact] = useState({});
const randomColor = useMemo(() => randomColorGenerator(), []);
useEffect(() => {}, []);
const renderContacts = item => {
return item.phoneNumbers.map(element => (
<TouchableOpacity
activeOpacity={1}
key={element.digits.toString()}
}}
>
<View>
<View>
<Text>{item.firstName}</Text>
<Text>{element.digits}</Text>
</View>
</View>
</TouchableOpacity>
));
};
const renderList = () => {
return (
<FlatList
keyboardShouldPersistTaps="handled"
data={contact}
keyExtractor={(item, index) => index.toString()}
renderItem={({ item }) => {
return <View>{renderContacts(item)}</View>;
}}
/>
);
};
return (
<View>
<Text style={Styles.textStyle}>All Contacts</Text>
{renderList()}
</View>
);
};
Make the function - a React functional component. Then you'll be able to use React.useMemo inside it. Also, this answer and this answer might help clarify better
// define it as a functional component
const RenderContacts = ({ item }) => {
// useMemo inside that
const randomColor = useMemo(() => randomColorGenerator(), []);
// TODO: use randomColor somewhere
return item.phoneNumbers.map(element => (
<TouchableOpacity ...>
...
</TouchableOpacity>
));
)
}
const Contacts = () => {
...
// extract this out into its own component
// const renderContacts = item => {
// return item.phoneNumbers.map(element => (
// <TouchableOpacity
// activeOpacity={1}
// key={element.digits.toString()}
// }}
// >
// <View>
// <View>
// <Text>{item.firstName}</Text>
// <Text>{element.digits}</Text>
// </View>
// </View>
// </TouchableOpacity>
// ));
// };
const renderList = () => {
return (
<FlatList
...
renderItem={({ item }) => {
// return <View>{renderContacts(item)}</View>;
// render the component, don't call it as a function
return <View><RenderContacts item={item} /></View>;
}}
/>
);
};
return (
<View>
<Text style={Styles.textStyle}>All Contacts</Text>
{renderList()}
</View>
);
};
I would just suggest creating your own pure-javascript wrapper around randomColorGenerator that caches values based on a key to give a consistent result. Something like:
const memoizedRandomColorGenerator = function() {
const cache = {};
return function(k) {
if (typeof cache[k] !== 'undefined') {
return cache[k];
}
cache[k] = randomColorGenerator();
return cache[k];
}
}() // note that we are immediately invoking this function to close over the cache
Define that somewhere outside of your component so it is only created once.
Then use it in component
const renderContacts = item => {
return item.phoneNumbers.map(element => (
<TouchableOpacity
activeOpacity={1}
key={element.digits.toString()}
}}
>
<View style={{
backgroundColor: memoizedRandomColorGenerator(element.digits.toString())
}}>
<View>
<Text>{item.firstName}</Text>
<Text>{element.digits}</Text>
</View>
</View>
</TouchableOpacity>
));
};
Color can be either property of the contact, that can be set earlier and stored inside the contact.
Or color can be function of the ${contact.firstName} ${contact.lastName}.
You can get hexadecimal hash from the string, then color from hash.
This way you get persistent assignment between contacts and colors.
useMemo is intended for heavy calculations, that returns the same result, not random.

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

Resources