Single selection item that saves the state (REACT-NATIVE) - reactjs

I currently have 4 TouchableHighlights and my code looks as follows:
state:
this.state = {
selected: null,
selectedButton: ''
}
Touchable Highlight (they're all the same except for text)
<TouchableHighlight
onPress={this.selectedButton}
underlayColor='blue
style={[styles.box, { backgroundColor: (this.state.selected === true ? 'red' : 'blue') }]}>
<Text>1</Text>
</TouchableHighlight>
my functions:
selectedButton = () => {
this._handlePress('flag', '1')
this.setState({
selected: true,
});
};
_handlePress(flag, button) {
if (flag == 1) {
this.setState({ selected: true });
}
this.setState({ SelectedButton: button })
}
Current behaviour: Whenever I select one button, all become highlighted and cannot be unpressed.
Desired behaviour: I want only one button to be selected at the time with its state being saved somewhere.
Note: I could get the desired behaviour by creating 4 different functions that contain a different flag number, however, I'd like to get this done in the cleanest way possible.
Any guidance please?
Thank you

You can create an array of button texts, then use .map(), which provides the current index value, to iterate through them. For example:
render() {
const renderHighlight = (text, index) => (
<TouchableHighlight
onPress={() => {
if(this.state.selectedIndex === index) {
// "Unpress" functionality
this.setState({selectedIndex: undefined});
}
else {
this.setState({selectedIndex: index
}
})
style={this.state.selectedIndex === index ?
{backgroundColor: "#0F0" : {}}
>
<Text>{text}</Text>
</TouchableHighlight>;
);
const buttons = ["button 0", "button 1", "button 2"];
return buttons.map((text, i) => this.renderHighlight(text, i));
}

Related

View background won't change after clicking it and using ternary

So I am displaying 5 stars. They are all gray on default when they are not selected. When I click on the star, I want the background color to change of the View but it is not changing after clicking it. Also, if I click star 3, I want the stars before star 3 to also change the background color. But nothing is working.
I used console.log to view the changed list when handleStarClicked runs and it saves the changes but not sure what is going on. Would really appreciate some help, please
const stars = [{ id: 0, selected: false }, { id: 1, selected: false }, { id: 2, selected: false }, { id: 3, selected: false }, { id: 4, selected: false }];
const [holdStars, setHoldStars] = useState(stars);
const handleStarClicked = (id) => {
for (let i = 0 ; i < holdStars.length; i++) {
if (i <= id) {
holdStars[i].selected = true;
} else {
holdStars[i].selected = false;
}
}
setHoldStars(holdStars);
}
return (
<FlatList
data={holdStars}
horizontal
scrollEnabled={false}
renderItem={({ item }) =>
<Pressable onPress={() => handleStarClicked(item.id)} className="ml-1 p-1 rounded-xl items-center justify-center" style={{ backgroundColor: item.selected ? "#6ECCAF" : "#CCCCCC" }}>
<Icon name="star" type="AntDesign" size={32} color="white" />
</Pressable>
}
/>
)
The issue here is that you are modifying the holdStars array directly in the handleStarClicked function and that is causing the issue.
Try this :
const handleStarClicked = (id) => {
const newHoldStars = [...holdStars]; // create a new copy of the holdStars array
for (let i = 0 ; i < newHoldStars.length; i++) {
if (i <= id) {
newHoldStars[i].selected = true;
} else {
newHoldStars[i].selected = false;
}
}
setHoldStars(newHoldStars);
}

ReactJS: How to set state of an object selected via radio button?

In my UsedComponents component I have a list of mapped radio buttons that each return componentType as value.
this.props.usedComponents.map((component, index) => (
<RadioButton
key={index}
id={component.componentType}
icon={ButtonIco}
label={component.componentName}
name="select"
value={component.componentType}
inputClass={classes.RadioInput}
labelClass={classes.RadioLabel}
selected={component.selected}
handleChange={this.props.selectComponent}
/>
))
My state looks like this:
constructor(props) {
super(props);
this.state = {
components: components,
usedComponents: [components[0], components[2], components[3]],
};
this.selectComponentHandler = this.selectComponentHandler.bind(this);
}
components is an imported array of objects that each look something like this:
{
componentType: "headerLogoNavigation",
componentName: "Header 02",
padding: "small",
fontSize: "small",
fontColor: "#1f1f1f",
fontFamily: "Sans-serif",
backgroundColor: "#ffffff",
image: placeholderLogo,
selected: false,
isEditing: false,
margins: false,
roundCorners: "none",
mobile: "false"
}
In my Page component I'm trying to pass a selectComponentHandler prop to my UsedComponents component that should select a component based on a value of a selected radio button and set its state to selected: true. For an added bonus it should set the state of any previously selected component to selected: false.So far I managed to figure out how to select the component but I'm not able to update its state. My final attempt to create this handler before I gave up looks like this:
selectComponentHandler = event => {
this.setState(prevState => {
let selected = prevState.usedComponents.filter(item => item.componentType === event.target.value);
selected.selected = 'true';
return { selected };
});
};
and it's an attempt to filter the prevState inside the setState for the componentType that matches event.target.value of the radio button and set it's state, but I messed up the logic or the syntax and my head is about to explode so I can't figure out what I did wrong.
Can someone help me figure this out?
I figured it out. It's a bit hacky but it works.
selectComponentHandler = event => {
const value = event.target.value;
this.setState(prevState => {
let selected = prevState.usedComponents.filter(item => item.componentType === value).shift();
let unSelected = prevState.usedComponents.filter(item => item.selected === true).shift();
if(unSelected) {
unSelected.selected = false;
}
selected.selected = true;
return { unSelected, selected };
});
};

MaterialTable - A state mutation was detected between dispatches

I have two sets of data that i want to display in table data so depending on the requirement, trying to set state of tabledata with different data.
Table Component:
<Grid item xs={12} md={8}>
{this.state.showTable && this.state.tableData !== null && (
<MaterialTable
title="Manage Blogs"
columns={this.state.columns}
data={this.state.tableData}
actions={[
{
icon: "open_in_new",
tooltip: "Open Blog",
onClick: (blog, rowData) => {
// Do save operation
this.props.history.push("partner/blog/" + rowData._id);
}
}
]}
/>
)}
</Grid>
Following are the two places, where i am changing state of tableData:
changeSelectedComponent = label => {
debugger;
if (this.state.selectedComponent.toString() !== label)
this.setState(state => ({ selectedComponent: label }));
if (label === "Blogs") {
if (
this.props.partnerBlogs === null &&
this.props.apiCallsInProgress === 0
) {
this.props.actions
.loadPartnerBlogs(this.props.auth0UserProfile.sub)
.catch(error => {
alert("Loading events failed" + error);
});
} else if (this.props.apiCallsInProgress === 0) {
this.setState({ tableData: this.props.partnerBlogs, showTable: true });
}
} else if (label === "Your Events") {
if (
this.props.partnerEvents === null &&
this.props.apiCallsInProgress === 0
) {
this.props.actions
.loadPartnerEvents(this.props.auth0UserProfile.sub)
.catch(error => {
alert("Loading events failed" + error);
});
} else if (this.props.apiCallsInProgress === 0) {
this.setState({ tableData: this.props.partnerEvents, showTable: true });
}
}
};
Another place after receiving new props:
componentWillReceiveProps(nextProps) {
debugger;
if (
this.props.partnerBlogs !== nextProps.partnerBlogs &&
nextProps.apiCallsInProgress === 0
) {
this.setState({
tableData: nextProps.partnerBlogs,
showTable: true
});
}
if (
this.props.partnerEvents !== nextProps.partnerEvents &&
nextProps.apiCallsInProgress === 0
) {
this.setState({
tableData: nextProps.partnerEvents,
showTable: true
});
}
}
Initial state of tableData is null. There is no issue when i am setting state for the first time but while changing state, getting following error
A state mutation was detected between dispatches, in the path
partnerEvents.0.tableData. This may cause incorrect behavior.
(http://redux.js.org/docs/Troubleshooting.html#never-mutate-reducer-arguments)
Don't know what's wrong in my code, Thanks for the help
materialTable mutated your data adding the object TableData that contain row 'Id' and 'checked' elements, so I recommend use this in your code if you want to avoid the mutation:
data={this.state.yourData.map(x =>Object.assign({}, x))}
Please, let me know if that avoid the mutation issues that you are having.
Best Regards

Increment value in react native

I'm getting data from a payload which has a total number of likes on each post. On the user screen, there's an icon for the user to like a post and what i want to achieve is when the user taps on it, the value show be increased to plus 1 against that particular post
VIEW:
{
posts.map((item, i) => {
return (
<View key={i} style={styles.user}>
<Card>
<ListItem
titleStyle={{ color: '#36c', fontWeight:'500' }}
titleNumberOfLines={2}
hideChevron={false}
roundAvatar
title={item.headline}
avatar={{uri:'https://s3.amazonaws.com/uifaces/faces/twitter/brynn/128.jpg'}}
/>
<Text style={{marginBottom: 10, fontSize:16, color:'#4a4a4a', fontFamily:'HelveticaNeue-Light'}}>
{item.text}
</Text>
<TouchableOpacity style={styles.likeContainer}>
<Text style={{fontSize:14}}>{item.likesCount}{"\n"}</Text>
<Icon
onPress={()=>onLikePost(item)}
name='md-thumbs-up'
type='ionicon'
iconStyle={[(item.isLiked=== true) ? styles.likedColor : styles.unLikedColor]}
/>
</TouchableOpacity>
</Card>
</View>
);
})
}
CONTAINER:
state = {
posts : [],
id: '',
user: ''
}
componentDidMount = () => {
const { navigation } = this.props;
this.setState({
id : navigation.getParam('id'),
user: navigation.getParam('user')
}, ()=> this.getData())
}
getData = () => {
const api = create({
baseURL: 'https://url.com/api',
headers: {'Accept': 'application/json'}
});
api.get('/groups/'+`${this.state.groupID}`+'/posts').then((response) => {
let data = response.data.data
this.setState({ posts: data });
console.log(JSON.stringify(this.state.posts))
})
}
onLikePost = (item) => {
item.likeCount = item.likeCount+1
}
You are storing posts data in state variable so use setState to update it. Use map and check for each post, whenever id (unique property of each post) matches to id of the clicked item, increase its likesCount otherwise return the same data.
Write it like this:
onLikePost = (item) => {
this.setState(prevState => ({
posts: prevState.posts.map(el => el.id === item.id? {...el, likesCount: el.likesCount+1} : el)
}))
}
Update: Put the check before updating the count value and change the isLiked bool also.
onLikePost = (item) => {
this.setState(prevState => ({
posts: prevState.posts.map(el => {
if(el.id === item.id) {
return {
...el,
isLiked: !el.isLiked,
likesCount: !el.isLiked? el.likesCount+1 : el.likesCount-1,
}
}
return el;
})
}))
}
Note: I am assuming each post has a key id unique value, if it doesn't exist then use any other unique property of the each post.
If array sequence is not an issue, you can use item index and use setState to update it.
<Icon
onPress={()=>onLikePost(i)}
...
/>
...
onLikePost = (i) => {
let posts = this.state.posts;
posts[i].likesCount = !posts[i].isLiked ? posts[i].likesCount + 1 : posts[i].likesCount - 1;
posts[i].isLiked = !posts[i].isLiked;
this.setState({ posts: posts})
}

React Native ListView renderRow method not firing with proper dataSource

I have been using React Native for some time now and I have never had this problem before. Perhaps because usually my data object is in-fact an object where as currently it is an array of objects. It is crucial that it remains an array so that I can maintain the correct order. This is for a messenger app so once I have sorted the messaged by date I NEED it to stay that way.
The Problem
The new message in the conversation will NOT render! I use REDUX/Firebase to sync with my remote DB and when I send a message through the app I am listening for the new messages and then updating the state. I am not using the child_appended event but the value event because I want the entire inbox of messages on a change. All of this happens correctly and without errors.
The dataSource object in state updates just fine, I can do a row count and see that it updates correctly. I can also look to the last object in the incoming array and see my current message just added with the right text and date. However when stepping through the renderMessageRow function, I can see that every row renders EXCEPT the new row. WTF... When I pause or print inside this row rendering function, the parameters just stop at the object right before the new message. However, the real fun begins when I print out this.state.dataSource from inside the renderMessageRow method. When you do this, THE NEW MESSAGE IS RIGHT THERE!! I can see it and the row count shows it has increased by one in the dataSource object. lol
I have tried changing this implementation so many ways, adding in a ton of slices or spread operators to make sure it's not the same array going in as it was previously in state before the change. Nothing is working. What's weirder is that this operation worked fine before I changed the dataSource coming in from REDUX from an object of objects (can't maintain order) to an array of objects. When it was an object of objects the new message always showed up just in random places throughout the list...
The Code
export default class SingleConvoView extends Component {
//rN Lifecycle ----------------------
constructor(props) {
super(props);
this.dataProtocol = new ListView.DataSource({
rowHasChanged: (row1, row2) => row1 !== row2,
});
this.state = {
conversationListViewHeight: 0,
dataSource: this.dataProtocol.cloneWithRows(this.props.messages),
};
}
componentWillReceiveProps(nextProps, nextState) {
if(deepEqual(this.props.messages, nextProps.messages, {strict: true}) === false) {
//This fires every time the REDUX state changes without any problems at all
//The messages property here has the new message appended to it
//The row count shows 1 more showing than before sending the message
this.updateDataSource(nextProps.messages);
}
}
componentDidReceiveProps(nextProps, nextState) {
//Tried this just incase, didn't work, commented out...
//this.state.dataSource.rowShouldUpdate('s1', (nextProps.messages.length - 1));
}
render() {
return (
<View style={[
styles.container,
{
width: this.props.display.width,
height: this.props.display.height,
}
]}>
{ this.renderConversation() }
</View>
);
}
renderConversation() {
if(this.state.dataSource.getRowCount() > 0) {
return (
<View style={{ height: (this.props.display.height - headerBarHeight - 50) }}>
<ListView
onLayout={event => {
// console.log('on layout event: new content size is: ', event.nativeEvent.layout.height);
this.setState({ conversationListViewHeight: event.nativeEvent.layout.height });
}}
onContentSizeChange={(newWidth, newHeight) => {
let totalContentHeight = newHeight - this.state.conversationListViewHeight + headerBarHeight;
if(this.state.conversationListViewHeight === 0 || newHeight < this.state.conversationListViewHeight) totalContentHeight = 0;
this.conversationScrollViewRef.scrollTo({ x: 0, y: totalContentHeight, animated: false });
}}
scrollEnabled={true}
removeClippedSubviews={false}
dataSource={this.state.dataSource}
renderRow={this.renderMessageRow.bind(this)}
pageSize={this.state.dataSource.getRowCount()}
ref={ref => { this.conversationScrollViewRef = ref; }}
renderScrollComponent={this.renderScrollComponent.bind(this)} />
</View>
);
} else {
return null;
}
}
renderScrollComponent(props) {
return (
<ScrollView
contentContainerStyle={{ paddingBottom: 20 }}
style={[
styles.conversationBox,
{ width: this.props.display.width - mainContainerSideMargins }
]} />
);
}
renderMessageRow(message, sectionID, rowID, highlightRow) {
let messageUser = message.userMessage ? 'You' : (this.props.senderFirstName || 'no name'),
messageTime = '', messageTitle = '';
if(message.hasOwnProperty('created')) {
let currentSentDate = new Date(message.created);
messageTime = `${currentSentDate.toLocaleDateString('en-US', DATE_DISPLAY_OPTIONS)}`;
}
messageTitle = message.userMessage ? `${messageTime}: ${messageUser}` : `${messageUser}: ${messageTime}`;
return (
<View style={styles.messageRow}>
<Text style={[
bodyFontStyle,
styles.messageOwnerHeader,
{
color: message.userMessage ? brand_blue_color : primary_color,
alignSelf: message.userMessage ? 'flex-end' : 'flex-start',
}
]}>
{ messageTitle }
</Text>
<View
shadowRadius={2}
shadowOpacity={1}
shadowColor={'rgba(0, 0, 0, 0.4)'}
shadowOffset={{width: -1, height: 1}}
style={styles.messageBodyContainer}>
<Text style={[
styles.messageBody,
{ textAlign: message.userMessage ? 'right' : 'left' }
]}>
{ message.body }
</Text>
</View>
</View>
);
}
//Functionality ---------------------
updateDataSource(data) {
if(typeof data != 'undefined' || data != null) {
let tempData = data.slice();
this.setState({
dataSource: this.state.dataSource.cloneWithRows(tempData),
});
}
}
}
I have solved this for now by adding in the initialListSize prop as well. I can't imagine this being the permanent solution however since no one else has responded I am answering this for Google searchers. If anyone else gives a better answer I will remove this and give them the credits.
renderConversation() {
if(this.state.dataSource.getRowCount() > 0) {
return (
<View style={{ height: (this.props.display.height - headerBarHeight - 50) }}>
<ListView
onLayout={event => {
this.setState({ conversationListViewHeight: event.nativeEvent.layout.height });
}}
onContentSizeChange={(newWidth, newHeight) => {
let totalContentHeight = newHeight - this.state.conversationListViewHeight + headerBarHeight;
if(this.state.conversationListViewHeight === 0 || newHeight < this.state.conversationListViewHeight) totalContentHeight = 0;
this.conversationScrollViewRef.scrollTo({ x: 0, y: totalContentHeight, animated: false });
}}
initialListSize={this.state.dataSource.getRowCount()}
scrollEnabled={true}
removeClippedSubviews={false}
dataSource={this.state.dataSource}
renderRow={this.renderMessageRow.bind(this)}
pageSize={this.state.dataSource.getRowCount()}
ref={ref => { this.conversationScrollViewRef = ref; }}
renderScrollComponent={this.renderScrollComponent.bind(this)} />
</View>
);
} else {
return null;
}
}

Resources