I am working on a react native app and I have an array of components that I need to be updated from different sections of my app. I am using context to be able to provide different consumers with this array, as well as the ability to the update the array.
As you can see in the code below I am passing my array, as well as a function to update the list, as the value for my provider. I then am using "header-right" in the navigation to call the function that updates the array in the provider state. I have confirmed that the array is updating and will only re-render the consumers when a change state in another component causes a re-render.
What I need is the change state of the provider to re-render all consumer components when it is changed, not when another components state change has occurred.
const DbContext = React.createContext("DEFAULT");
export default class DbProvider extends Component {
constructor(props){
super(props);
this.state = {
choreList: [],
refresh:null,
}
}
componentDidMount(){
sqlQueries.getChores().then(row => this.setChoresList(row));
}
updateChoreList = (choresAdded, offset) => {
console.log("K");
if(offset == 1){ // if adding chores
var tempList = this.state.choreList;
tempList.push(choresAdded);
this.setState({
choreList: tempList,
})
}else if(offset == -1){ // if removing a chore
for(var i = 0; i < this.state.choreList.length; i++){
for(var j = 0; j < choresAdded.length; j++){
if(this.state.choreList[i].state.name == choresAdded[i].state.name){ // if chore being removed is i in state choreList
var tempList = this.state.choreList; // remove and update state choreList
tempList.splice(i, 1);
this.setState({
choreList: tempList,
});
break;
}
}
}
}
}
setChoresList(row){
var tempList = [];
for(var i = 0; i < row._array.length; i++){
tempList.push(<Chore choreName={row._array[i].name} dayId={row._array[i].id} nav={this.props} inDB={true} key={row._array[i].name}/>);
}
this.setState({
choreList: tempList,
})
}
render () {
return (
<DbContext.Provider value={{choreList: this.state.choreList, update: this.updateChoreList}}>
{this.props.children}
</DbContext.Provider>
)
}
}
export { DbContext, DbProvider }
I am then in another component using this list to display something
render(){
return (
<DbProvider>
<DbContext.Consumer>
{globalChores => globalChores.choreList == 0
? <NoChores nav={this.props.nav} dayState={this} />
: (
<View style={styles.ChoreDayContainer}>
<View style={styles.ChoreListContainer}>
{globalChores.choreList}
</View>
<AddChoresBtn nav={this.props.nav} dayState={this}/>
</View>)}
</DbContext.Consumer>
</DbProvider>
);
}
and finally I am updating the provided array like so
componentDidMount(){
this.props.navigation.setOptions({
headerRight: () => (
<DbProvider>
<DbContext.Consumer>{(db) =>(
<Button
onPress={ () => this.dataToDB(db)}
title="Save"
color="#fff"
/> )}
</DbContext.Consumer></DbProvider>
),
})
}
dataToDB(db){
db.update(this.state.choreShowingList, 1);
}
}
Related
I have a FlatList of items that has a "remove" button next to it.
When I click the remove button, I am able to remove the item from the backend BUT the actual list item is not removed from the view.
I am using useState hooks and it was to my understanding that the component re-renders after setState happens.
The setState function is used to update the state. It accepts a new
state value and enqueues a re-render of the component.
https://reactjs.org/docs/hooks-reference.html
What am I missing with how state is set and rendering?
I don't want to use the useEffect listener for various reasons. I want the component to re-render when the locations state is updated....which I am pretty sure is happening with my other setStates....not sure if I am totally missing the mark on what setState has been doing or if it's something specific about setLocations().
const [locations, setLocations] = useState(state.infoData.locations);
const [locationsNames, setLocationsNames] = useState(state.infoData.names]);
...
const removeLocationItemFromList = (item) => {
var newLocationsArray = locations;
var newLocationNameArray = locationsNames;
for(l in locations){
if(locations[l].name == item){
newLocationsArray.splice(l, 1);
newLocationNameArray.splice(l, 1);
} else {
console.log('false');
}
}
setLocationsNames(newLocationNameArray);
setLocations(newLocationsArray);
};
...
<FlatList style={{borderColor: 'black', fontSize: 16}}
data={locationNames}
renderItem={({ item }) =>
<LocationItem
onRemove={() => removeLocationItemFromList(item)}
title={item}/> }
keyExtractor={item => item}/>
UPDATED LOOP
const removeLocationItemFromList = (item) => {
var spliceNewLocationArray =locations;
var spliceNewLocationNameArray = locationsNames;
for(f in spliceNewLocationArray){
if(spliceNewLocationArray[f].name == item){
spliceNewLocationArray.splice(f, 1);
} else {
console.log('false');
}
}
for(f in spliceNewLocationNameArray){
if(spliceNewLocationNameArray[f] == item){
spliceNewLocationNameArray.splice(f, 1);
} else {
console.log('false');
}
}
var thirdTimesACharmName = spliceNewLocationNameArray;
var thirdTimesACharmLoc = spliceNewLocationArray;
console.log('thirdTimesACharmName:: ' + thirdTimesACharmName + ', thirdTimesACharmLoc::: ' + JSON.stringify(thirdTimesACharmLoc)); // I can see from this log that the data is correct
setLocationsNames(thirdTimesACharmName);
setLocations(thirdTimesACharmLoc);
};
This comes down to mutating the same locations array and calling setState with the same array again, which means that the FlatList which is a pure component will not re-render since the identity of locations has not changed. You could copy the locations array to newLocationsArray first (similarly with the newLocationNameArray) to avoid this.
var newLocationsArray = locations.slice();
var newLocationNameArray = locationsNames.slice();
I am trying to build my sub-component in function _buildComponent, and put result into render(), just have a look at my code below
the problem I met was the AsyncStorage.getItem() is running async, causing it render nothing there in render() method
...react
_buildComponent = async (key) => {
let val = await AsyncStorage.getItem(key)
console.log(key + ' : ' +val);
debugger;//code will run to here after ScrollableTabView finish rendering. but I need to build Arr first.
if(val == 1) return <PopularView tabLabel={key}>{key}</PopularView>
}
render() {
let Arr = Constants.TABS.map(item =
return this._buildComponent(item).done();
})
debugger;//code will run into here directly without waiting building Arr above, making Arr was null when rendering ScrollableTabView
return (
<ScrollableTabView
tabBarBackgroundColor='#2196F3'
tabBarInactiveTextColor='mintcream'
tabBarTextStyle={{marginTop:27}}
initialPage={0}
renderTabBar={() => <ScrollableTabBar/>}
>
{Arr}// Arr is null here because the _buildComponent method was not finish yet.
{/* <PopularView tabLabel='Java'>Java</PopularView>
<PopularView tabLabel='IOS'>IOS</PopularView>
<PopularView tabLabel='Android'>Android</PopularView>
<PopularView tabLabel='Javascript'>Javascript</PopularView> */}
</ScrollableTabView>
)
}
...
I have explain my issue in comment, please check it, thanks guys. I do not know what's the best practise to prepare variable before running render().
Try this solution which dynamically add tabs (Updated)
_buildComponent = async () => {
let tabsData = [];
await AsyncStorage.multiGet(Constants.TABS).then(response => {
Constants.TABS.forEach((item,index) =>
{
if(response[index][1] == 1) {
tabsData.push(response[index][0]); // key
}
}
);
// This only render once
this.setState({ tabLabels: tabsData })
})
}
componentDidMount() {
this._buildComponent()
}
render() {
const tabLabelList = this.state.tabLabels.map((key) => {
return (
<PopularView tabLabel={key}>{key}</PopularView>
)
})
return (
<ScrollableTabView
tabBarBackgroundColor='#2196F3'
tabBarInactiveTextColor='mintcream'
tabBarTextStyle={{marginTop:27}}
initialPage={0}
renderTabBar={() => <ScrollableTabBar/>}
>
{tabLabelList}
</ScrollableTabView>
)
}
I have a simple query auto-generated from aws AppSync, and I'm trying to use the Connect Component, with a FlatList and use a TextInput to filter and auto-update the list. But I confess I didn't found out a way to do that... any hints?
Tried to find more information about this without success...
Auto-Generated query:
export const listFood = `query ListFood(
$filter: ModelFoodFilterInput
$limit: Int
$nextToken: String
) {
listFood(filter: $filter, limit: $limit, nextToken: $nextToken) {
items {
id
name
description
...
My current code, which I don't quite know where to place my filter value:
<Connect query={graphqlOperation(queries.listFood)}>
{
( { data: { listFood }, loading, error } ) => {
if(error) return (<Text>Error</Text>);
if(loading || !listFood) return (<ActivityIndicator />);
return (
<FlatList
data={listFood.items}
renderItem={({item}) => {
return (
<View style={styles.hcontainer}>
<Image source={{uri:this.state.logoURL}}
style={styles.iconImage}
/>
<View style={styles.vcontainer}>
<Text style={styles.textH3}>{item.name}</Text>
<Text style={styles.textP}>{item.description}</Text>
</View>
</View>
);
}}
keyExtractor={(item, index) => item.id}
/>
);
}
}
</Connect>
What I aim is mainly to filter by item.name, refreshing the list while typing from a TextInput, probably going somewhere on the $filter variable...
Ok, I think I've figured out the usage with the AWS AppSync Out-of-the-box queries...
query MyFoodList{
listFood(
filter: {
name: {
contains:"a"
}
}
) {
items {
id
name
}
}
}
And it is finally working properly with this disposition on my react-native code:
<Connect query={ this.state.filter!=="" ?
graphqlOperation(queries.listFood, {
filter: {
name: {
contains: this.state.filter
}
}
})
:
graphqlOperation(queries.listFood)
}>
I still didn't manage to make the sort key work yet... will try a little more and open another topic for it if I didn't get anything...
This is filter in use in React / Javascript:
const [findPage, setFindPage] = useState('') // setup
async function findpoints() {
// find user & page if exists read record
try {
const todoData = await API.graphql(graphqlOperation(listActivitys, {filter : {owner: {eq: props.user}, page: {eq: action}}}))
const pageFound = todoData.data.listActivitys.items // get the data
console.log('pageFound 1', pageFound)
setFindPage(pageFound) // set to State
} catch (err) {
console.log(err)
}
}
The async / wait approach means the code will try to operate, and move on to other areas of your code putting data into findPage through setFindPage when and if it finds data
After adding observable properties to an array with this action
#action
improvePrograms = function() {
let data = this.dataPro;
this.dataProImproved = data;
for (i = 0; i < data.length; i++) {
extendObservable(this.dataProImproved[i], {
wishedFor: false,
inCart: false,
sold: false
});
}
};
I tried to change the values of the properties with:
#action
addToWishList = function(item) {
for (i = 0; i < this.dataProImproved.length; i++) {
if (this.dataProImproved[i].program_id === item.program_id && this.dataProImproved[i].wishedFor === false) {
this.dataProImproved[i] = { ...this.dataProImproved[i], wishedFor: true };
console.log("Program added to wish list: ", this.dataProImproved[i]);
this.getWishList();
}
}
};
everything seemed fine but the component where I use the observable property for changing colors doesn't react or rerender even when the value of the properties are changed to true.
<TouchableOpacity onPress={!this.props.program.wishedFor ? this.onSelectWish : this.onDeselectWish}>
<FontAwesome
style={{ padding: 7 }}
name={this.props.program.wishedFor === false ? "heart-o" : "heart"}
size={28}
color={!this.props.program.wishedFor ? "#c5d1e8" : "#ff1d25"}
/>
</TouchableOpacity>
Can someone please explain to me what I am doing wrong here. The class component is decorated with #observer and injected with the store.
So I made it work passing the data array through:
#computed get getDataProImproved(){return this.dataProImproved}
And don't forget to slice the array before passing it to the FlatList...
Everything reacts fine...
I am trying to implement an infinite scroll in React Native. Below is the source of the component:
var React = require('react-native');
var server = require('../server');
var Post = require('./Post');
var SwipeRefreshLayoutAndroid = require('./SwipeRefreshLayout');
var backEvent = null;
var lastPostId = "";
var isLoadingMore = false;
var isLoadingTop = false;
var onEndReachedActive = false;
var {
StyleSheet,
ListView,
View,
Text,
Image,
ProgressBarAndroid,
BackAndroid
} = React;
class Stream extends React.Component {
constructor(props) {
super(props);
this.ds = new ListView.DataSource({
rowHasChanged: (row1, row2) => {
console.log("rowHasChenged FIRED!!");
return false;
}
});
this.state = {
dataSource: this.ds.cloneWithRows(['loader']),
hasStream: false,
posts: []
};
}
componentDidMount() {
BackAndroid.addEventListener('hardwareBackPress', () => {
this.props.navigator.jumpBack();
return true;
}.bind(this));
server.getStream('', '', 15).then((res) => {
lastPostId = res[res.length-1].m._id;
this.setState({
posts: res,
hasStream: true,
dataSource: this.ds.cloneWithRows(res)
}, () => onEndReachedActive = true);
})
}
onRefresh() {
var posts = this.state.posts;
var firstPost = posts[0].m._id;
console.log(this.state.dataSource._rowHasChanged);
isLoadingTop = true;
server.getStream('', firstPost, 4000)
.then(res => {
console.log(posts.length);
posts = res.concat(posts);
console.log(posts.length);
this.setState({
dataSource: this.ds.cloneWithRows(posts),
posts
}, () => {
this.swipeRefreshLayout && this.swipeRefreshLayout.finishRefresh();
isLoadingTop = false;
});
}).catch((err) => {
isLoadingTop = false;
})
}
onEndReached(event) {
if(!onEndReachedActive) return;
if(this.state.loadingMore || this.state.isLoadingTop)return;
isLoadingMore = true;
var posts = this.state.posts;
server.getStream(posts[posts.length-1].m._id, '', 15)
.then(res => {
console.log('received posts');
posts = posts.concat(res);
lastPostId = posts[posts.length-1].m._id;
this.setState({
dataSource: this.ds.cloneWithRows(posts),
posts
}, ()=>isLoadingMore = false);
})
}
renderHeader() {
return (
<View style={styles.header}>
<Text style={styles.headerText}>Header</Text>
</View>
)
}
renderRow(post) {
if(post === 'loader') {
return (
<ProgressBarAndroid
styleAttr="Large"
style={styles.spinnerBottom}/>
)
}
let hasLoader = post.m._id === lastPostId;
let loader = hasLoader ?
<ProgressBarAndroid
styleAttr="Large"
style={styles.spinnerBottom}/> : null;
return (
<View>
<Post
post={post}/>
{loader}
</View>
)
}
render() {
return (
<ListView
style={styles.mainContainer}
dataSource={this.state.dataSource}
renderRow={this.renderRow.bind(this)}
onEndReached={this.onEndReached.bind(this)}
onEndReachedThreshold={1}
pageSize={15} />
);
}
}
The problem is that whenever I append (or prepend) new data, the rowHasChanged method of the DataSource doesn't fire. It just re-renders every row, even tho nothing has changed (except the new data).
Any idea why the method is bypassed?
Edit: Pass a function to setState to avoid race conditions
I just figured it out. If you are having the same issue, check the point at which you change your state with the new dataSource. Mine was like this:
this.setState({
dataSource: this.ds.cloneWithRows(posts)
});
Instead you should always use the dataSource from the previous state, like this:
this.setState(state => ({
dataSource: state.dataSource.cloneWithRows(posts)
}))
Cheers!
this worked for me, hope this helps. I created a new dataSource and assigned the updated data to it on state change as follows:`
var dataSource = new ListView.DataSource(
{rowHasChanged: (r1, r2) => ( r1 !== r2)});
this.setState({ dataSource : dataSource.cloneWithRows(posts) });
Now, the new data is assigned and the view is rendered correctly. Note that posts array that is assigned now holds the updated data. Still wondering though if it's the best way to do it but it works!
I agree it seems to make sense that you should always use the dataSource from the previous state.
Yet when I setState this way, rowHasChanged gets called for all rows, however, rowHasChanged always returns false and no rows are rendered??? Why?
// This is callback handler that the ListView DetailView will
// call when a ListView item is edited
onChange(waypoint: Object){
console.log('Callback: rowNumber= ', waypoint.rowNumber);
console.log(' length(m)= ', waypoint.distance.meters);
var itemListChanged = this.state.itemList;
itemListChanged[waypoint.rowNumber-1] = waypoint;
this.setState({
dataSource: this.state.dataSource.cloneWithRows(itemListChanged),
});
},
If I setState this way, renderRow is called for all rows unconditionally without rowHasChanged ever being called. Which is correct?
this.setState({
dataSource: ds.cloneWithRows(itemListChanged),
});
ListView, datasource, and react-native are a hard learning curve coming from C#/C/C++.
for anyone still having issue with rowHasChanged called but are still returning false the following snippets might help
the datasource is initialized like usual:
let ds = new ListView.DataSource ({
rowHasChanged: (a, b) => {
const changed = (a !== b)
return changed
}
})
this.data = []
this.state = {
listDataSource: ds.cloneWithRows(this.data)
}
here is the function which will update a row
updateRow = (row, rowId, sectionId) => {
// make a shallow clone from the stored data array
let blob = this.data.concat()
// modify the row, since we are using the triple equal operator, we need to make sure we are giving it a new object (new ref)
blob[rowId] = Object.assign({}, blob[rowId], {label: blob[rowId].label + '..cape..deh'})
// tell react to update the source
this.setState({
listDataSource: this.state.listDataSource.cloneWithRows(blob)
}, () => {
// we need to update our data storage here! after the state is changed
this.data = blob
})
}