How do I dynamically create the state so I can assign 'open'/'close' based on front-end requirements?
E.g.
I have a table with rows in which I have a Fab component. That component has to set a unique ID to open/close to handle the animation. I do not know how many of these rows I will have so I can't pre-populate the state with active1, active2 etc.
Code Example (In this example I am passing 'active1', 'active2' with the object manually, but realistically that isn't a real approach I feel.
{Object.keys(characters).map((k, i) => {
<Fab
active= // how do I specify the state of 'this' instance?
direction="left"
containerStyle={{}}
style={{
backgroundColor: "#5067FF"
}}
position="topRight"
onPress={() => this.setState({ //how so I specify 'this' instances state? })}>
...redacted
I've tried to add a sample similar to my code. Let's say I am passing in 5 Users and each User should have this Fab. At this point my state is blank so I have tried implementing a for each loop into componentdidmount to populate the state, this worked, but I had two issues based on how I managed state.
A) All fabs were open, always.
B) No fabs would ever open.
This is because of my onPress not updating the correct state.
I feel like I have to be way over-complicating this issue and I am tearing up my code to get it to work, so I figured I'd come here to ask.
Let me know what other code I can share.
A way of achieving this is to keep an activeArray in state,
which you can toggle activeness of an element buy pushing or removing the element (or just its id or some property which is unique) from activeArray.
So there should be a function on onPress like:
handlePress = (k) => {
if(this.state.activeArray.indexOf(k) > -1){
// remove element
// for example:
this.setState(state => ({activeArray: state.activeArray.filter(activeElement => activeElement !== k)}))
} else {
// push element
// for example:
this.setState(state => ({activeArray: [...state.activeArray, k]}))
}
}
Then for Fab component:
<Fab
active={this.state.activeArray.indexOf(k) > -1}
direction="left"
containerStyle={{}}
style={{
backgroundColor: "#5067FF"
}}
position="topRight"
onPress={() => this.handlePress(k)}>
This line:
active={this.state.activeArray.indexOf(k) > -1}
means if k is inside activeArray then active is true.
You can set the dynamic state using JSON bracket ([]) notation.
{
Object.keys(characters).map((k, i) => {
let dynamicStateName = "active"+i //it will set the dynamicStateName as active with index. ie, active1, active2 and so on
return(
{ this.state[dynamicStateName] !== false || this.state[dynamicStateName] !== undefined ?
(<Fab
active= {dynamicStateName}// specify the state of 'this' instance here as dynamicStateName
direction="left"
containerStyle={{}}
style={{
backgroundColor: "#5067FF"
}}
position="topRight"
onPress={() => {(this.state[dynamicStateName] != undefined) ? (this.state[dynamicStateName] ? this.setState({ [dynamicStateName]: false }) : this.setState({ [dynamicStateName]: true })) : this.setState({ [dynamicStateName]: true })} } /> // it will set the state as true if undefined. It will act as toggleable
):
( <button onPress={() => this.setState({ [dynamicStateName]: true })}>OpenFabButton<button>
// initially the dynamicState will be either undefined or false. At that time the button will be show. On clicking the button it will enable the fab component
)
)
})
}
This will fix yours
Related
I'm building an app where I have a Home page where I render data that I'm getting from an API in the form of cards, and a Favorites page, where the cards marked as favorite are displayed. I'm using React Router, and I have a Topbar component with a Nav and a Search, the latter is utilizing the grouped materialui autocomplete component.
In the Search component I'm trying to check whether the user is in the homepage or in the favorites page, in order to use 2 different arrays. The original array of data for the homepage, and the one with the favorited items for the favorites page and I'm getting the above error. I'm passing both arrays as props, and the search component initially worked with the original beers array. I'm not sure what I'm doing wrong, the error message is not helping me to identify the problem.
This is the logic that I wrote to differentiate among the pages:
let options;
document.location.pathname === '/favorites'
? (options = favoriteBeers)
: (options = beers);
And this is the code for the autocomplete component:
options.map(option => {
const firstLetter = option.name[0].toUpperCase();
return {
firstLetter: /[0-9]/.test(firstLetter) ? '0-9' : firstLetter,
...option,
};
});
<Autocomplete
options={options.sort(
(a, b) => -b.firstLetter.localeCompare(a.firstLetter)
)}
groupBy={option => option.firstLetter}
getOptionLabel={option => option.name}
getOptionSelected={(option, value) => option.id === value.id}
clearOnEscape
style={{ width: 400, margin: 'auto' }}
onChange={(e, value) => handleOpen(value)}
renderInput={params => (
<TextField {...params} placeholder='Search for beer...' />
)}
map returns a new array, this way you should assign its return value to some variable. also you should always spread your object first, otherwise it may overwrite the changes you are performing:
options = options.map(option => {
const firstLetter = option.name[0].toUpperCase();
return {
...option,
firstLetter: /[0-9]/.test(firstLetter) ? '0-9' : firstLetter,
};
});
Here is my componentDidMount() method :
componentDidMount() {
const subscription = accelerometer.subscribe(({ x, y, z, timestamp }) => {
x = Math.trunc(x*100);
this.setState({x})
});
}
In above method, every 100 millisecond state is changing. I used that state in my render() method as below :
render() {
const animatedImageStyle = StyleSheet.flatten([
styles.captureButton,
{
transform: [{rotateZ:this.state.x + 'deg'}]
}
])
return (
<SideMenu
menu={leftMenu}
isOpen={this.state.isOpenLeftMenu}
menuPosition={'left'}
bounceBackOnOverdraw={false}
onChange={(isOpenLeftMenu) => this.updateLeftMenuState(isOpenLeftMenu)}
>
<View>
<TouchableOpacity
activeOpacity={0.5}
onPress={(this.state.recordingMode == 'camera')?() => this.takePicture():() => this.toggleRecording()}
>
<Image
source={require('../assets/imgs/forRotate.png')}
style={animatedImageStyle}
/>
</TouchableOpacity>
</View>
</SideMenu>
)
}
Now, the problem is that when I trying to open sidemenu, it is not opening, I mean it opening but hanging too much. My whole app hanging too much.
I think that's because of below method :
updateLeftMenuState(isMenuOpen) {
this.setState({
isOpenLeftMenu:isMenuOpen
})
}
Notice that I am updating another state called isOpenLeftMenu, which may blocked during I update state x.
Can anyone tell me what't going wrong here ?
you can move the animation view in a separate component along with subscription logic. So the state update of that component won't affect the SideMenu component.
How can i only show the redo button only when the user has changed region?In my code right now it keeps flickering on and off as the region changes, not sure what is missing.
Code:
app.js
onregionchange() {
this.setState({redosearch: !this.state.redosearch })
}
render() {
const Showredo = ({redosearch }) => redosearch ? <View><Text> redo now <Text></View> : null
return(
<View>
{this.state.redosearch ? <ShowRedo redosearch={this.state.redosearch }/> : null}
<View>
<MapView
ref={(map) => (this.map = map)}
style={styles.map}
onRegionChange={this.onregionchange}
>
</MapView>
)
}
There are a few things I notice may be causing an issue here. First, it looks like you may be double checking the state value; inside the Showredo element property and again inside the return value. second, The code has two open View tags and two Text tags with no close. Third, I can't see if the function onregionchange is bound or not. And finally, you're returning two elements in the render function (or actually missing two view closing tags at the end)
try to change your code to this which should correct all of those:
onregionchange = () => {
this.setState({redosearch: !this.state.redosearch })
}
render() {
const { redosearch } = this.state;
return([
<View key="a_key_for_element_1">
{redosearch ? <View><Text> redo now </Text></View> : null}
</View>,
<MapView
key="a_key_for_element_2"
ref={(map) => (this.map = map)}
style={styles.map}
onRegionChange={this.onregionchange}
>
</MapView>]
);
}
I am using state to store the following data.
state = {
refresh: true,
isFetching: true,
showLoadingBottom: false,
data: []
};
on componentDidMount I manually call a function _fetchData which loads data into this.state.data.
When the flatlist is scrolled to the end it fires _fetchData twice which ends up returning the same data twice ( which is another problem, why does it fire twice? ).
Once the flatlist reaches the end ie, no more data is returned from the server, it goes into an endless loop since the onEndReached continuously fires over and over again even though no new data is returned from the server and this.state.data remains the same.
This is my render code
render() {
return (
<View
style={{
flex: 1
}}>
<FlatList
refreshControl={
<RefreshControl
refreshing={this.state.refresh}
onRefresh={() => {
this.setState({
refresh: true
}, this._fetchData);
}}
title={"Pull To Refresh"}
tintColor={darkGrey}
titleColor={darkGrey}/>
}
onEndReachedThreshold={0.5}
onEndReached={() => {
this.setState({
showLoadingBottom: true
}, () => {
this._fetchData();
});
}}
showsVerticalScrollIndicator={false}
data={this.state.data}
ListFooterComponent={() => {
return (
this.state.showLoadingBottom &&
<View style={{padding: 10}}>
<ActivityIndicator size="small" color={colorAccent}/>
</View>
);
}}
renderItem={this._renderItem}
keyExtractor={(item) => item.id.toString()}
removeClippedSubviews={true}
/>
</View>
);
}
Here is my solution that can maybe be changed to suit other peoples needs:
Basically the important parts are onEndReached={this.state.moreData && this.retrieveMore}. So you can test inside your onEndReached function weather there is anymore data (In my case if we only return 1 object i know it's finished) then set state this.state.moreData to false.
<SafeAreaView style={styles.container}>
<FlatList
data={Object.values(this.state.documentData)}
// Render Items
renderItem={({ item }) => (
<ItemSelector
item={item}
onPress={() => {this.selectItem(item)}}
/>
)}
// On End Reached (Takes in a function)
onEndReached={this.state.moreData && this.retrieveMore}
// How Close To The End Of List Until Next Data Request Is Made
onEndReachedThreshold={1}
ListEmptyComponent={
<Text>No jobs to show</Text>
}
/>
</SafeAreaView>
retrieveMore = async () => {
try {
// Set State: Refreshing
this._isMounted && this.setState({ refreshing: true });
fbDb.ref('job')
.orderByKey()
.startAt(this.state.lastVisible) //Start at the last item we found
.limitToFirst(this.state.limit) //Limit queries returned per page
.once('value', snapshot => {
//check if we got a result
if(snapshot.numChildren() > 1){
.....
this._isMounted && this.setState({
documentData: newstate,
lastVisible: lastVisible,
refreshing: false, //Hide loading icon
});
} else {
this._isMounted && this.setState({
refreshing: false, //Hide loading icon
moreData: false
});
}
});
}
catch (error) {
console.log(error);
}
};
I have a similar problem. In my case it is because the ListFooterComponent.
If you render the ListFooterComponent with this pattern or equivalent
onEndReachedThreshold={x} // for any x >= 0
ListFooterComponent={() => {
if (isDataFetching) {
return <SomeComponentWithSomeHeight />
} else {
return undefined;
}
}}
It will trigger onEndReached infinitely when the user scrolls down the end of the list (or if your content is not longer than the list's visibility area).
And it is because the presence and absence of the <SomeComponentWithSomeHeight /> affects the height of the content and thus triggers the endReached re-calculation.
And following are the possible solution I can think of.
Use negative onEndReachedThreshold that is always "higher" than the height of the ListFooterComponent. But I don't like this solution because it is difficult to know the "higher" (it is relative to the FlatList's visibility area). And the negative onEndReachedThreshold may cause some issue on Android.
Implement your own loading overlay outside of the FlatList so that the loading component does not affect the content height.
Set opacity = 0 to hide the ListFooterComponent instead of returning undefined, so that it is always there and the content height does not change when it becomes visible.
ListFooterComponent={() => {
return (
<View style={{ opacity: isDataFetching ? 1 : 0 }}>
<SomeComponentWithSomeHeight />
</View>
);
}}
You are using a component that is being rendered when you are loading data, right? So your flatlist needs to rerender. You meed to make sure you only call you fetch method once even thou your end is reached multiple times.
This is a basic example below :
renderItem: ({ item }) =>
<SwipeRow
ref={(SwipeRow) => { refSwipeRow = SwipeRow }} >
<TouchableOpacity
onPress={() => {
refSwipeRow.closeRow()
}
</TouchableOpacity>
</SwipeRow>
while onPress refSwipeRow.closeRow() is called but it only works on the last index, technically it's right because the while rendering the ref is getting overriden and at the end it only holds the last index reference.
How create a unique ref for each element.
When rendering FlatList/SectionList you should add a unique key prop to each rendered item. You can achieve this with using keyExtractor prop for the FlatList and SectionList. You can read about it more here.
And for your question, you can set refs into a single object with again a unique id. Then onPress fired you can use that unique value to close row.
For Example
renderItem: ({ item }) => (
<SwipeRow
ref={(SwipeRow) => { this.rowRefs[item.id] = SwipeRow; }} >
<TouchableOpacity
onPress={() => {
this.rowRefs[item.id].closeRow();
}
</TouchableOpacity>
</SwipeRow>
)
Update
To use this.rowRefs[item.id] you should declare it firs in component's constructor as an empty object like this,
constructor(props) {
super(props);
this.rowRefs = {};
}