React Native - Appended component in hook not responding as expected - react-native

I am working on a project that uses Google autocomplete to set locations. The project allows users to set pickup and destination location, and then they can also enter stop-by places up to additional 3, making it a total of 5.
Here's my sample code:
const placesRef = useRef([]);
const [stopspots, setStopSpots] = useState([]);
const [state, setState] = useState({
defaultPlacesInput: 'flex',
//and others
});
useEffect(() => {
placesRef.current = placesRef.current.slice(0, 5);
}, []);
const placesComponent = (i, placeholder) => {
return (<PlacesFrame key={i}>
...
<GooglePlacesAutocomplete
placeholder={placeholder}
minLength={2}
ref={el => placesRef.current[i] = el}
onPress={(data, details = null) => {
placesRef.current[i]?.setAddressText(data?.structured_formatting?.main_text);
setState({...state, defaultPlacesInput: 'flex'})
}}
enablePoweredByContainer={false}
fetchDetails
styles={{
textInput: [styles.input1,{paddingLeft:30}],
container: [styles.autocompleteContainer,{display:placesRef.current[i]?.isFocused() ? 'flex' : state.defaultPlacesInput}],
listView: styles.listView,
listView: styles.listView,
row: styles.row,
predefinedPlacesDescription: {
color: '#1faadb',
},
}}
query={{
key: GOOGLE_PLACES_API_KEY,
language: profile.language,
components: 'country:' + profile.iso,
}}
textInputProps={{
//value: '',
onChangeText: alterOtherFields
}}
renderRow={(data) => <PlaceRow data={data} />}
/>
...
</PlacesFrame>)
}
const stopByLocation = () => {
var counter = stopspots.length, obj = placesComponent(counter + 2, 'Drop off location');
setStopSpots([...stopspots, {
id: counter,
place: obj
}
])
}
And here is how the autocomplete component is rendered
return(
...
<View>
{placesComponent(0, 'Pick up location')}
{placesComponent(1, 'Drop off location')}
</View>
...
)
The output look like this
Everything works perfect when I call the placesComponent() function directly. But like I mentioned earlier, I want the users to be able to add up to 3 additional stop by locations, and because it is optional, additional fields is added by appending to hook, and then rendered. the code looks like this.
return(
...
<View>
{placesComponent(0, 'Pick up location')}
{placesComponent(1, 'Drop off location')}
//This will append more placed fields
{stopspots != '' ?
stopspots.map((item : {}) => ((item.place)))
: null}
<ClickableButton>
<TouchableOpacity activeOpacity={0.6} onPress={() => stopByLocation()}><AddPlaces><AntDesign name="plus" size={10} color="#444" /> Add</AddPlaces></TouchableOpacity>
</ClickableButton>
</View>
...
)
The outcome looks like this
I observed that each component binded to the hooks takes the early properties, and does not effect additional changes. While the first two fields rendered by calling the function directly does.
When I make changes to state.defaultPlacesInput (observe this in styles property of GooglePlacesAutocomplete), the changes only effect on the two components called directly.
Is there a module, or a systematic way to append the renderer function call, without using useState hooks to append the 3 additional fields?
Is it possible to expose stored properties in useState hooks to respond as the other two which observe the state changes? If yes, how?
Any contribution, suggestion will be accepted

Related

How to create a custom record action button inside a List component with React-Admin?

I'm a totally newbie with React and React-Admin. IMHO, I'm trying to achieve something simple that many people must have already done but I cannot find any kind of tutorial anywhere.
I'd like to add another button to the list of action buttons (show/edit) within each row in a <List> component. This button would archive the record.
My last try looks like the code below.
import React from 'react';
import {
Datagrid,
EmailField,
List,
TextField,
ShowButton,
EditButton,
DeleteButton,
CloneButton,
} from 'react-admin';
import { makeStyles } from '#material-ui/core/styles';
import ArchiveIcon from '#material-ui/icons/Archive';
const useRowActionToolbarStyles = makeStyles({
toolbar: {
alignItems: 'center',
float: 'right',
width: '160px',
marginTop: -1,
marginBottom: -1,
},
icon_action_button: {
minWidth: '40px;'
},
});
const ArchiveButton = props => {
const transform = data => ({
...data,
archived: true
});
return <CloneButton {...props} transform={transform} />;
}
const RowActionToolbar = (props) => {
const classes = useRowActionToolbarStyles();
return (
<div className={classes.toolbar}>
<ShowButton label="" basePath={props.basePath} record={props.record} className={classes.icon_action_button}/>
<EditButton label="" basePath={props.basePath} record={props.record} className={classes.icon_action_button}/>
<ArchiveButton {...props} basePath={props.basePath} label="" icon={<ArchiveIcon/>} record={props.record} className={classes.icon_action_button} />
<DeleteButton basePath={props.basePath} label="" record={props.record} className={classes.icon_action_button}/>
</div>
);
};
export const UserList = props => {
return (
<List
{...props}
sort={{ field: 'first_name', order: 'ASC' }}
>
<Datagrid>
<TextField source="first_name"/>
<TextField source="last_name"/>
<EmailField source="email"/>
<RowActionToolbar/>
</Datagrid>
</List>
)
};
Obviously, this code does not work because the <CloneButton> component get rid of the id the record. Moreover, except if I did something wrong - which is totally possible -, it makes a GET request to a create endpoint.
I'm using different routes in my dataProvider (The back end is using Django and Django rest framework). I want to send a PATCH to the detail endpoint, like the <Edit> component does.
I also tried with a <SaveButton>, but it fails too.
Uncaught TypeError: Cannot read property 'save' of undefined
at useSaveContext (SaveContext.js:23)
I guess the <SaveButton> must be within a <SimpleForm>?
I'd like the save behaviour of the <DeleteButton>, i.e. update the record from the list, display the notification that the record has been archived (with the Undo link), send the request to the back end, refresh the list.
Any guidance, directions would be very appreciated.
I don't know that this is a full answer, but felt like more than a comment...
You are trying to archive the existing record, not create a whole new record, right? CloneButton is supposed to be used to create a new record with a new ID (which is why your ID is going away), so you don't want to us it here. note that I've never used CloneButton. it is not fully documented so I could be wrong about its use.
I am thinking that you should use the useRecordContext hook within your Archive button to pull in all of the record's data, including the id; read this little section: https://marmelab.com/react-admin/Architecture.html#context-pull-dont-push
And I don't think transform is what you're looking for here. You will need to use one of the dataProvider hooks, i'm assuming useUpdate: https://marmelab.com/react-admin/Actions.html#useupdate
//first create component
const MyButton = (props: any) => {
const [sendEmailLoading, setSendEmailLoading] =
React.useState<boolean>(false);
const record = useRecordContext(props);
const sendEmail = (id: Identifier) => {
setSendEmailLoading(true)
dataProvider.sendEmail(
"notifications", { id: id })
.then(({ data }: any) => {
if (data && data.status == "success")
notify('Email send success', { type: 'success' });
setSendEmailLoading(false);
refresh();
});
};
return (
<ButtonMUI color='primary' size="small" onClick={() => {
sendEmail(record.id) }}>
{
!record.publish &&(
!sendEmailLoading ? (
translate('resources.notifications.buttons.send')
) : (
<CircularProgress size={25} thickness={2} />
)
)
}
</ButtonMUI>
)
}
//and second add to datagrid list
<Datagrid>
<NumberField source="id" />
<TextFieldRA source="subject" />
<DateField source="date" />
<BooleanField source="publish" />
{/* <EditButton /> */}
<ShowButton />
<MyButton />
</Datagrid>

React Native conditionally setting state

I have an array of "favorite" listings per user. I want to set a state variable depending on if it is rendering a favorite item or a regular (non-favorite) item. This is a little heart icon that is either filled or unfilled. It is incorrectly setting the state on first render. Correctly sets it after refresh.. I'm doing something wrong and have tried many things to no avail so if someone could just look at this code and tell me if you see any glaring flaws. If not then I will look elsewhere for the problem.
Behavior when app loads: I am doing console.log just after the state is set to show whether it's a favorite or not, and the contents of the favorite state variable (an image url, but in the console it shows it as either 21 for non-fav, or 22 for a fav). I can see that it is correctly pulling the array of favorites, and correctly identifying those that are and are not favorited (1 means its a favorite). It is however not setting the state variable correctly. Furthermore, it IS setting it correctly for only some of the listings. Currently I have all listings marked as a favorite, and it only messes up the first 10. After that they all set right. MORE bizarre, is upon refreshing the screen, it correctly sets all of them.
MainHeader.js (pulling data from db, setting initial array of favorite listings, and passing it to the messagesScreen component)
const [favsArray, setFavsArray] = useState("");
useEffect(() => {
lookupUser()
.then((snapshot) => {
if (snapshot.hasChildren()) {
snapshot.forEach(function(childSnapshot) {
let favs = childSnapshot.child("favorites").val();
setFavsArray(favs);
})
}
})
.catch((error) => {console.error('Error:', error)});
}, []);
return (
<NavigationContainer>
<View style={styles.headerContainer}>
<Image
style={styles.image}
source={require("../assets/newheader4.png")}
/>
</View>
<Tab.Navigator
tabBarOptions={{
activeTintColor: "blue",
inactiveTintColor: "black",
style: {},
tabStyle: {
width: "auto",
backgroundColor: "#e0d5f3",
borderTopWidth: 3,
borderBottomWidth: 3,
borderRightColor: "gray",
},
labelStyle: {
fontSize: 14,
fontWeight: "bold",
},
scrollEnabled: true,
}}
>
<Tab.Screen name="All Deals" children={()=><MessagesScreen favsArray={favsArray} setFavsArray={setFavsArray}/>} />
</Tab.Navigator>
</NavigationContainer>
MessagesScreen, receives favsArray and renders a FlatList with component Card which it feeds favsArray to.
<FlatList
data={messagesShow}
keyExtractor={(messagesShow) => messagesShow.id.toString()}
renderItem={({ item }) => (
<Card
price={item.currentPrice}
title={item.title}
image={item.image}
posted={item.postedDate}
discAmount={item.discountAmount}
discType={item.discType}
expiration={item.expiration}
promoCode={item.promoCode}
affLink={item.amzLink}
indexStore={item.indexStore}
store={item.store}
favsArray = {favsArray}
/>
)}
ItemSeparatorComponent={ListItemSeparator}
contentContainerStyle={styles.messagesList}
refreshing={refreshing}
onRefresh={() =>
db.ref('deals').once('value', (snapshot) =>{
let testData = [];
snapshot.forEach((child)=>{
// if (child.val().hasOwnProperty('title')){
testData.push({
id: child.key,
title: child.val().hasOwnProperty('title') ? child.val().title : 'NA',
currentPrice: child.val().price,
discountAmount: child.val().discAmt,
discType: child.val().discType,
promoCode: child.val().promoCode,
expiration: child.val().expDate,
postedDate: child.val().postDate,
image: { uri: child.val().imageLink},
amzLink: child.val().affLink,
category: child.val().category,
indexStore: child.val().indexStore,
store: child.val().store
})
// }
checkMessages(testData);
})
})
.then()
.catch((error) => {console.error('Error:', error)})
}
/>
Card component, this is in a FlatList where favsArray is passed as a prop (correctly verified by console), along with the individual listing data. If it finds the listing in the fav array, it should set to HeartFilled (1), if not set to HeartEmpty (0).
let test = [];
test = favsArray.split(',');
let isFav = 0;
let found = test.find(function (element) {
return element == indexStore;
});
if (found != undefined){
isFav = 1;
}
const [heartFilled, setHeartFilled] = useState( isFav == 1 ? require('../assets/heartFilled.png') : require('../assets/heartEmpty.png'));
console.log(isFav + ' ' + heartFilled);
Looking at my console, you can see it correctly shows each listing as a favorite, but for the first 10 listings it sets the state to the wrong image (21, shown in red). These should all be 22.

React Native: Extra empty space on top of the screen

I have a bug where a user clicks on a survey and then opens up what is called supporting information that expands the UI further, then the user selects his or her answer and clicks on the NEXT QUESTION button, at that point the whole top part of the screen drops down exposing this huge gap. This is the code I believe governs all that behavior:
class BallotSurveyDetails extends PureComponent {
componentDidUpdate(prevProps) {
if (prevProps.currentWizardPage !== this.props.currentWizardPage) {
this.scroll.props.scrollToPosition(0, 0, true);
}
}
render() {
const {
currentWizardPage,
selectedSurvey,
handleNextQuestionButtonPress,
handleResponseChanged,
loading,
responses,
handleSubmitButtonPress,
saving,
wizardPages
} = this.props;
if (!saving && loading) {
return <Loading />;
}
const isWizard = selectedSurvey.Layout !== "Wizard";
const isList = selectedSurvey.Layout !== "List";
const displayNextQ = isWizard && currentWizardPage < wizardPages;
const displaySubmit =
isList || (isWizard && currentWizardPage === wizardPages);
const sortedGroups = (selectedSurvey.QuestionGroups || []).sort(
(a, b) => a.Order - b.Order
);
const wizardGroup = isWizard ? sortedGroups[currentWizardPage - 1] : null;
return (
<SafeAreaView style={styles.container}>
{isWizard && wizardPages.length > 1 && (
<Card style={styles.pagination}>
<RadioPagination
numberOfPages={wizardPages}
currentPage={currentWizardPage}
/>
</Card>
)}
<KeyboardAwareScrollView
showsVerticalScrollIndicator={false}
extraScrollHeight={45}
innerRef={ref => {
this.scroll = ref;
}}
enableOnAndroid={true}
contentContainerStyle={{ paddingBottom: 90 }}
>
<View style={styles.headerContainer}>
<Text style={styles.ballotTitle}>{selectedSurvey.Name}</Text>
<Text style={styles.ballotSubtitle}>
{selectedSurvey.Description}
</Text>
</View>
{isList &&
What I tried to do to resolve this was add automaticallyAdjustContentInsets={false} inside the KeyboardAwareScrollView, did nothing to resolve the bug. Any ideas anyone?
I'm not sure what's causing this for you, but here are a few things that have corrected similar problems I've had in the past:
It can help to wrap every screen in a container with flex:1.
I had a similar case with conditionally rendering a search bar above a FlatList and I used this to fix it:
I added this to the top of my file.
import { Dimensions, other stuff you need} from 'react-native';
const deviceHieght = Dimensions.get('window').height;
and then I wrapped my FlatList in a view like this
<View style={this.state.showBar === false ? styles.containFlatlist : styles.containSearchFlatlist}>
and this is the styling it was referencing
containFlatlist: {
height: deviceHieght
},
containSearchFlatlist: {
height: deviceHieght-100
},
In a different similar case I had an issue like this with a screen that displayed photos on click within a scrollview. In that case I did this:
<ScrollView
ref={component => this._scrollInput = component}
>
Then in componentDidMount I put
setTimeout(() => {
this._scrollInput.scrollTo({ x: 0, animated: false })
}, 100)
I was also using react navigation in this case so I also did
return(<View style={styles.mainFlex}>
<NavigationEvents
onWillBlur={payload => this._scrollInput.scrollTo({x:0})}
/>
Followed by the rest of my code.
I hope one of those helps. Given that you're also dealing with a scrollview, my best guess is that the third fix is most likely to work in your situation.
So the appear is with this code snippet here:
componentDidUpdate(prevProps) {
if (prevProps.currentWizardPage !== this.props.currentWizardPage) {
this.scroll.props.scrollToPosition(0, 0, true);
}
}
In particular, this.scroll.props.scrollToPosition(0, 0, true);. In removing the whole component lifecycle method, the bug went away.

How to Highlight Updated Items onRefresh of FlatList

I set up a FlatList with an onRefresh function to update the state when the user drags down the screen. It works properly, however I was wondering how I can highlight items in the FlatList that have been updated after the refresh.
Say, for example, I want to change the background for a few seconds for any item in the list that was updated, then return to normal.
<FlatList
data={scores}
renderItem={({item}) => (
<View style={styles.scoreContainer}>
<ScoreRow data={item.away} />
<ScoreRow data={item.home} />
</View>
)}
keyExtractor={item => item.gameID}
refreshing={isRefreshing}
onRefresh={updateScores}
/>
The best I could do was add a useEffect in the ScoreRow component to detect if something changes within that component, but that only allows me to update one component at a time, not the entire View.
const [runUpdate, setRunUpdate] = useState(false)
const [runs, setRuns] = useState(data.R)
useEffect(() => {
if(runs !== data.R) {
setRunUpdate(true)
setRuns(data.R)
setTimeout(() => setRunUpdate(false), 10000)
}
}, [data.R])
I can't figure out how to detect a change on an an item in the View of the FlatList so that I can change the entire View the way I did each component.
You can achieve this by using data of FlatList. You have to make an extra parameter for this.
eg:
//Method to refresh data
_refreshMethod() {
// Do your code to fetch...
...
let newDataArray = data // Data fetch from server or some thing.
let updatedArray = []
newDataArray.map((data, index) => {
data["isNewItem"] = true;
updatedArray.push(data);
});
this.setState({scores: updatedArray})
this._callTimer()
}
// Method to update new item status after a delay
_callTimer() {
setTimeout(function() {
let updatedArray = []
this.state.scores.map((data, index) => {
data["isNewItem"] = false;
updatedArray.push(data);
});
this.setState({scores: updatedArray})
}, 3000); // The time you want to do...
}
Then change the style of row based on the state value.
<FlatList
data={this.state.scores}
renderItem={({item}) => (
<View style={item.isNewItem ? styles.yourNewItemStyle : styles.scoreContainer}>
<ScoreRow data={item.away} />
<ScoreRow data={item.home} />
</View>
)}
keyExtractor={item => item.gameID}
refreshing={isRefreshing}
onRefresh={updateScores}
extraData={this.state}
/>

How to loop and render elements in React-native?

Is it possible to loop an identical component in Render function?
Something like this:
...
onPress = () => {
...
};
initialArr = [["blue","text1"],["red","text2"]];
buttonsListArr = [];
for (let i = 0; i < initialArr.length; i++)
{
buttonsListArr.push(
<Button style={{borderColor:{initialArr[i][0]}}} onPress={this.onPress.bind(this)}>{initialArr[i][1]}</Button>
);
}
...
render() {
return (
<View style={...}>
{buttonsListArr}
</View>
)};
I mean this is just finite list of components, so any components like ListView/ScrollView etc is not applicable in this particular case. This is just syntax question.
You would usually use map for that kind of thing.
buttonsListArr = initialArr.map(buttonInfo => (
<Button ... key={buttonInfo[0]}>{buttonInfo[1]}</Button>
);
(key is a necessary prop whenever you do mapping in React. The key needs to be a unique identifier for the generated component)
As a side, I would use an object instead of an array. I find it looks nicer:
initialArr = [
{
id: 1,
color: "blue",
text: "text1"
},
{
id: 2,
color: "red",
text: "text2"
},
];
buttonsListArr = initialArr.map(buttonInfo => (
<Button ... key={buttonInfo.id}>{buttonInfo.text}</Button>
);
render() {
return (
<View style={...}>
{initialArr.map((prop, key) => {
return (
<Button style={{borderColor: prop[0]}} key={key}>{prop[1]}</Button>
);
})}
</View>
)
}
should do the trick
For initial array, better use object instead of array, as then you won't be worrying about the indexes and it will be much more clear what is what:
const initialArr = [{
color: "blue",
text: "text1"
}, {
color: "red",
text: "text2"
}];
For actual mapping, use JS Array map instead of for loop - for loop should be used in cases when there's no actual array defined, like displaying something a certain number of times:
onPress = () => {
...
};
renderButtons() {
return initialArr.map((item) => {
return (
<Button
style={{ borderColor: item.color }}
onPress={this.onPress}
>
{item.text}
</Button>
);
});
}
...
render() {
return (
<View style={...}>
{
this.renderButtons()
}
</View>
)
}
I moved the mapping to separate function outside of render method for more readable code.
There are many other ways to loop through list of elements in react native, and which way you'll use depends on what do you need to do. Most of these ways are covered in this article about React JSX loops, and although it's using React examples, everything from it can be used in React Native. Please check it out if you're interested in this topic!
Also, not on the topic on the looping, but as you're already using the array syntax for defining the onPress function, there's no need to bind it again. This, again, applies only if the function is defined using this syntax within the component, as the arrow syntax auto binds the function.
If u want a direct/ quick away, without assing to variables:
{
urArray.map((prop, key) => {
console.log(emp);
return <Picker.Item label={emp.Name} value={emp.id} />;
})
}