firestore document id will be undefined after I update array - react-native

I have this flatlist which receive data from firestore and send as props to projectsummery.js
const ProjectList =({projects})=> {
return(
<FlatList
data={projects}
renderItem={(project,index)=>{
return(
<ProjectSummery project={project} key={project.item.id}
//keyExtractor={(item, index) =>item.id}/>
)
} }
/>
)
}
Here I have a button which which sends document id which is something like this
{project.item.id} == CSmet3tRjpjDcJ437M78
ProjectSummery.js
const ProjectSummery =(props)=> {
const {project,auth}=props
return(
<>
<View >
<Text> {project.item.title} </Text>
<Text>likes { project.item.likes.length}</Text>
<Text>{project.item.id}</Text>//document id in in firestore
<View>
<Button title='like' onPress{()=>props.likesPosts(project.item.id)}/>
</View>
</View>
const mapDispatchToProps=(dispatch)=>{
return{
likePosts:(postId)=>dispatch(likePosts(postId))
}
}
When I try to update array in firebase the first time it work but the second time the document id will be undefined. I use React-Native. Thanks for help...
export const likePosts = (postId) => {
return (dispatch,getState,{getFirebase,getFirestore})=>{
const profile=getState().firebase.profile
const authId=getState().firebase.auth.uid
const firestore=getFirestore()
firestore.collection('projects').doc(postId).update({
//this postId will be be undefined in the 2nd time
likes:firestore.FieldValue.arrayUnion({
likedAt:new Date(),
likedBy:authId,
name: profile.firstName
})
})
}}
The fist update postId == CSmet3tRjpjDcJ437M78 in the 2nd time postId will be undefined

What's happening is that when you click a like button the first time, it's working as expected so it gets the proper postId and then continues with the process you have defined. However, when you try the 2nd time it fails to fetch the postId as it's already liked.
The idea is that you'll need to either define an if statement and specify what should happen if it's already clicked and it get's clicked again (possibly storing the postId somewhere the first time and using it from there), or make an initial check that returns a specific message to the user if it's already clicked.
The issue has nothing to do with Firestore itself but with the button and the states of liked/unliked.
Here is one nice interactive example on codepen.io of a proper way of building like buttons using react. React Like Button
HTML
<div id="example"></div>
CSS
.btn-primary {
background-color: #23aa4e;
border-color: #177d37;
}
#example {
margin: 3rem;
}
.customContainer {
border: 1px solid black;
}
JS
class LikeButton extends React.Component {
constructor() {
super();
this.state = {
liked: false
};
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
this.setState({
liked: !this.state.liked
});
}
render() {
const text = this.state.liked ? 'liked' : 'haven\'t liked';
const label = this.state.liked ? 'Unlike' : 'Like'
return (
<div className="customContainer">
<button className="btn btn-primary" onClick={this.handleClick}>
{label}</button>
<p>
you {text} this. Click to toggle.
</p>
</div>
);
}
}
ReactDOM.render(
<LikeButton />,
document.getElementById('example')
)

Related

Mobx React `autorun` called more times on every change

I've setup a simple app store with a single numeric value, which I increment on every button click. My UI is simple: a single app <div> which contains a MyChild component that renders the number next to an increment button.
The app's autorun seems to behave correctly BUT every time I increment the value, MyChild's autorun fire extra times i.e. on page load it fires once. If I click the button, it fires twice. I click again, it fires 3 times, and so on. I expect that on every increment, autorun would fire once. What am I missing here?
Code is available on CodeSandbox
Here it is here as well:
import "./styles.css";
import * as React from "react";
import { observer } from "mobx-react-lite";
import { action, autorun, makeAutoObservable } from "mobx";
class AppStore {
v;
constructor() {
this.v = 0;
makeAutoObservable(this);
}
}
const appStore = new AppStore();
const MyChild = observer(() => {
console.log("MyChild render", appStore.v);
autorun(() => { // <------------------------ this gets fired extra times
console.log("mychild autorun " + appStore.v);
});
return (
<div style={{ backgroundColor: "lightBlue" }}>
mychild {appStore.v}
{": "}
<button
onClick={action(() => {
appStore.v += 1;
})}
>
INC
</button>
</div>
);
});
export default observer(function App() {
console.log("app render");
autorun(() => {
console.log("app autorun " + appStore.v);
});
return (
<>
<div style={{ backgroundColor: "gray", padding: "10px" }}>
main
<MyChild />
</div>
</>
);
})
I have found the reason (I'm new to Mobx-React, guess I should have figured it out)
According to this tip, I need to setup autorun inside a useEffect that happens on first render. I changed all my autoruns to:
React.useEffect(() => {
return autorun(...);
}, []);
and now they get fired once every render.

react-native-material-textfield Doesn't work as a controlled input

I'm using "react-native-material-textfield" for text inputs. I have a View to edit user details it fetch values from api when mounting and set it to state. But after upgrading "react-native-material-textfield" to "0.16.1" that original first name value is not shown in the text input after mounting. What I'm doing wrong here ?
constructor(props) {
super(props);
this.state = {
firstName: '',
};
}
componentDidMount(props) {
APIcall().then(data)=>{
this.setState({
firstName: data.firstName
});
}
}
<TextField
label="First Name"
value={this.state.firstName}
onChangeText={firstName => this.setState({firstName})}
/>
I ran into this after upgrading. In version 0.13.0 of the library, it switched to being a fully uncontrolled component according to the release notes.
Changed
defaultValue prop becomes current value on focus
value prop provides only initial value
Based on the current usage docs, there is now a method exposed for setting & getting the value using a ref to the component:
let { current: field } = this.fieldRef;
console.log(field.value());
(Personally, while I can maybe understand this improving performance because typing can often be fast for state updates, I'm not a fan of uncontrolled components since I want my state to drive the UI. I feel like this makes other live updates for validation very fiddly.)
In react-native-material-textfield, 'value' prop acts as default. To update the value you need to use ref. Get the ref using React.createRef(), then use setValue function
import React, { Component } from 'react';
import { TextField } from 'react-native-material-textfield';
import { View, Button } from 'react-native';
export default class TestComponent extends Component {
textField = React.createRef<TextField>();
constructor(props) {
super(props);
this.state = {
value: 'check',
};
}
onChangeText = () => {
// Send request via API to save the value in DB
};
updateText = () => {
if (this.textField && this.textField.current) {
this.textField.current.setValue('test');
}
};
render() {
return (
<View>
<TextField
label="Test value"
value={this.state.value}
onChangeText={this.onChangeText}
ref={this.textField}
/>
<Button onPress={this.updateText} />
</View>
);
}
}
Touch area in TextView
https://github.com/n4kz/react-native-material-textfield/issues/248
react-native-material-textfield
labelTextStyle={{ position: 'absolute', left: '100%' }}
label: {
fontFamily: fonts.Muli_SemiBold,
fontSize: 14,
letterSpacing: 0.1,
color: colors.gray90,
position: 'absolute', left: '100%'
},
<TextField
style={style.textInputRight}
labelTextStyle={style.label}
labelFontSize={16}}
onChangeText={value => onTextChange(value)}
/>

edit screen fields not populating in react-native

I try to open an edit screen for relevant record when user taps the row in the list. I see at debugger all of the props are passing successfully but somehow I cant show them on the screen. I searched a lot and I think the main problem is at onRowPress helper. When I press the row, I see in the debugger all the props have passed correctly. But there is an error that says
Failed prop type: Invalid prop value of type array supplied to
TextInput, expected string.
My question is how should I handle this error.
console.log
onRowPress() {
console.log(this.props.employee);
Actions.employeeEdit({ employee: this.props.employee });
}
I guess you are on "The Complete React Native and Redux Course", if so, i thing you missed:
-1 import Communications from "react-native-communications";
-2
class EmployeeEdit extends Component {
state = { showModal: false };
...
3- Above your render():
onTextPress() {
const { phone, shift } = this.props;
Communications.text(phone, `Your upcoming shift is on ${shift}`);
}
onAccept() {
const { uid } = this.props.employee;
this.props.employeeDelete({ uid });
}
onDecline() {
this.setState({ showModal: false });
}
-4 And finally, your main render() should be:
<Card>
<EmployeeForm />
<CardSection>
<Button onPress={this.onButtonPress.bind(this)}>Save Changes</Button>
</CardSection>
<CardSection>
<Button onPress={this.onTextPress.bind(this)}>Text Schedule</Button>
</CardSection>
<CardSection>
<Button onPress={() => this.setState({ showModal: !this.state.showModal })}>
Fire Employee
</Button>
</CardSection>
<Confirm
visible={this.state.showModal}
onAccept={this.onAccept.bind(this)}
onDecline={this.onDecline.bind(this)}
>
Are you sure you want to delete this?
</Confirm>
</Card>;
It takes time but I solved the problem. Main problem was the error below;
Failed prop type: Invalid prop value of type array supplied to
TextInput, expected string.
I followed all the code step to step and find that the reducer which update the props with the values I pass can't do this. I added a toString() method to the actions.payload.value and everything is ok. You have to pass a string to the Input component.
export default (state = INITIAL_STATE, action) => {
switch (action.type) {
case EMPLOYEE_UPDATE:
return { ...state, [action.payload.prop]: action.payload.value.toString() };
case EMPLOYEE_CREATE:
return INITIAL_STATE;
case EMPLOYEE_SAVE_SUCCESS:
return INITIAL_STATE;
default:
return state;
}
};

How to correctly large state updates in React Native?

I am writing a small ReactNative application that allows users to invite people to events.
The design includes a list of invitees, each of which is accompanied by a checkbox used to invite/uninvite said invitee. Another checkbox at the top of the list that performs a mass invite/uninvite on all invitees simultaneously. Finally a button will eventually be used to send out the invites.
Because the state of each of these elements depends changes made by the other I often need to re-render my entire UI whenever the user takes action on one of them. But while this works correctly it is causing me quite a few performance issues, as shown in this video
Here's the code I'm using:
import React, { Component } from 'react';
import { Container, Header, Title,
Content, Footer, FooterTab,
Button, Left, Right,
Center, Body, Text, Spinner, Toast, Root , CheckBox, ListItem, Thumbnail} from 'native-base';
import { FlatList, View } from 'react-native';
export default class EventInviteComponent extends Component {
constructor(props) {
super(props);
console.disableYellowBox = true;
this.state = {
eventName: "Cool Outing!",
invitees:[]
}
for(i = 0; i < 50; i++){
this.state.invitees[i] = {
name: "Peter the " + i + "th",
isSelected: false,
thumbnailUrl: 'https://is1-ssl.mzstatic.com/image/thumb/Purple111/v4/62/08/7e/62087ed8-5016-3ed0-ca33-50d33a5d8497/source/512x512bb.jpg'
}
}
this.toggelSelectAll = this.toggelSelectAll.bind(this)
}
toggelSelectAll(){
let invitees = [...this.state.invitees].slice();
let shouldInviteAll = invitees.filter(invitee => !invitee.isSelected).length != 0
let newState = this.state;
newState = invitees.map(function(invitee){
invitee.isSelected = shouldInviteAll;
return invitee;
});
this.setState(newState);
}
render() {
let invitees = [...this.state.invitees];
return (
<Root>
<Container>
<Content>
<Text>{this.state.eventName}</Text>
<View style={{flexDirection: 'row', height: 50, marginLeft:10, marginTop:20}}>
<CheckBox
checked={this.state.invitees.filter(invitee => !invitee.isSelected).length == 0}
onPress={this.toggelSelectAll}/>
<Text style={{marginLeft:30 }}>Select/deselect all</Text>
</View>
<FlatList
keyExtractor={(invitee, index) => invitee.name}
data={invitees}
renderItem={(item)=>
<ListItem avatar style={{paddingTop: 20}}>
<Left>
<Thumbnail source={{ uri: item.item.thumbnailUrl}} />
</Left>
<Body>
<Text>{item.item.name}</Text>
<Text note> </Text>
</Body>
<Right>
<CheckBox
checked={item.item.isSelected}/>
</Right>
</ListItem>}/>
</Content>
<Footer>
<FooterTab>
<Button full
active={invitees.filter(invitee => invitee.isSelected).length > 0}>
<Text>Invite!</Text>
</Button>
</FooterTab>
</Footer>
</Container>
</Root>);
}
}
In your code, in class method toggelSelectAll() {...} you modify the state directly by using this.state = ..., which is something to be avoided. Only use this.state = ... in your class constructor() {...} to initialize the state, and you should only use this.setState({...}) to update the state anywhere else.
Not sure if this should help your performance issues, but try replacing toggelSelectAll() with the following:
toggelSelectAll() {
const {invitees} = this.state;
const areAllSelectedAlready = invitees.filter(({isSelected}) => !isSelected).length === 0;
this.setState({
invitees: invitees.map(invitee => ({
...invitee,
isSelected: !areAllSelectedAlready
}))
});
}
Good luck! And, let me know if you would like me to refactor your above code to remove the 2nd this.state = ... in your constructor (which, once again, should be avoided when writing React).
I suggest:
Dividing your code by creating multiple components, so you won't have a massive render()
Using Redux to store invitee / global state, so you can choose which components should re-render in case of modifications
That's a good way to learn React Native!

Reacting to a component being scrolled off screen in a react-native ScrollView

Currently I am using a <ScrollView /> component to handle the scrolling for my items, as I will only ever have a maximum of three items I feel this is appropriate instead of introducing a <FlatList />. This component receives a prop called collapsed and onCollapseToggle which is used to modify the collapse prop that is passed to the child. I have also experimented with the child having it's collapsed variable in state, but it seems that modifying the child's state from the parent would be near-impossible.
When scrolling through the <ScrollView /> when a component is passed up (The user scrolls down far enough that the component is no longer displayed on the screen) I want to execute a function that could potentially change the collapsed value that's passed to the item being rendered. This way if a user as expanded an item to view more information about it, and then continues scrolling down the <ScrollView /> the item would be self-collapsing, without the user having the manually close it through some form of input.
I'm not currently sure about any way to go about this, and any help would be greatly appreciated. I will provide an example of the structure that I am working with, which may help someone come up with a solution. I do not mind restructuring my components.
class ContentInformation extends React.Component {
state = { content: [ ... ] };
onCollapseToggle = (index, displayed=true) => {
const { content } = this.state;
const arr = content.slice();
const item = arr[index];
if(!item) return;
if(!displayed) {
if(!item.collapsed) {
item.collapsed = true;
}
} else {
item.collapsed = !item.collapsed;
}
arr[index] = item;
this.setState({ content: arr });
}
render() {
return (
<ScrollView>
{ this.state.content.map((content, index) => (
<Item
key={content._id}
index={index}
onCollapseToggle={this.onCollapseToggle}
{...content} />
); }
</ScrollView>
);
}
}
So basically, as you can see, I only need to figure out when the <Item /> goes off-screen so I can call onCollapseToggle(index, false) to automatically re-collapse the component if it's open.
This is an example of how you can detect when an item is offscreen when scrolling.
state = { toggleDistance: 0 }
_handleScroll({nativeEvent: {contentOffset: {y}}}) {
const {toggleDistance} = this.state;
if (y >= toggleDistance) {
// The item is offscreen
}
}
render() {
<ScrollView
onScroll={this._handleScroll.bind(this)}>
<Item
onLayout={({nativeEvent: {layout: {y, height}}}) => this.setState({toggleDistance: (y + height)})}/>
...
</ScrollView>
}
Seems to me you need a FlatList or a ListView and ScrollView simply doesn't support this use case.
The hint is in the ScrollView description:
FlatList is also handy if you want ... any number of other features it supports out of the box.
I solve the problem improving the answer from ivillamil with some adjusts:
const [toggleDistance, setToggleDistance] = useState(0);
const [showBottom, setShowBottom] = useState(false);
const onScrollMoveFooter = (event: NativeSyntheticEvent<NativeScrollEvent>) => {
const currentOffset = event.nativeEvent.contentOffset.y;
if (event.nativeEvent.layoutMeasurement.height - currentOffset <
toggleDistance) {
setShowBottom(true);
} else {
setShowBottom(false);
}
};
And in the another component:
<Component
onLayout={({
nativeEvent: {
layout: { y, height },
},
}) => setToggleDistance(y + height)}
/>
and so I can conditionally hide what I wanted:
{showBottom && (<AnotherComponent/>)}