Dynamic build component with AsyncStorage - react-native

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>
)
}

Related

Why does my context consumer not automatically update?

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);
}
}

When state changes for graphql variable, result stays the same on react-native

I'm trying to create an app using shopify graphql api to create an ecommerce app on react native expo.
I have an onPress that calls a setState to change the state of the graphQL variable but the results don't change from the initial state of 'currentSubCategories'
const [currentSubCategories, setSubCategories] = useState(Categories[0].subCategory[0].handle);
let {
collection,
loading,
hasMore,
refetch,
isFetchingMore,
} = useCollectionQuery(currentSubCategories, first, priceRange);
const [currentCategory, setCategory] = useState({categories: Categories[0]});
const onSubCategorySelect = (subCategory) => { setSubCategories(subCategory.handle) }
onPress={() => onSubCategorySelect(item)}
function useCollectionQuery(
collectionHandle: string,
first: number,
priceRange: [number, number],
) {
let [isInitFetching, setInitFetching] = useState<boolean>(true);
let [isReloading, setIsReloading] = useState<boolean>(true);
let [collection, setCollection] = useState<Array<Product>>([]);
let isFetchingMore = useRef<boolean>(false);
let hasMore = useRef<boolean>(true);
let defaultCurrency = useDefaultCurrency().data;
let { data, loading, refetch: refetchQuery } = useQuery<
GetCollection,
GetCollectionVariables
>(GET_COLLECTION, {
variables: {
collectionHandle,
first,
sortKey: ProductCollectionSortKeys.BEST_SELLING,
presentmentCurrencies: [defaultCurrency],
},
notifyOnNetworkStatusChange: true,
fetchPolicy: 'no-cache',
});
let getMoreUntilTarget = async (
targetAmount: number,
cursor: string | null,
handle: string,
filter: [number, number],
) => {
let result: Array<Product> = [];
let moreData: Array<Product> = [];
let { data } = await refetchQuery({
first,
collectionHandle: handle,
after: cursor,
});
...
useEffect(() => {
if (!loading) {
isFetchingMore.current = false;
}
if (isInitFetching && !!data && !!data.collectionByHandle) {
let newCollection = mapToProducts(data.collectionByHandle.products);
hasMore.current = !!data.collectionByHandle?.products.pageInfo
.hasNextPage;
setCollection(newCollection);
setIsReloading(false);
setInitFetching(false);
}
}, [loading, isInitFetching]); // eslint-disable-line react-hooks/exhaustive-deps
return {
collection,
loading: isReloading,
hasMore: hasMore.current,
isFetchingMore: isFetchingMore.current,
refetch,
};
}
I'm using flatList to show the result
<FlatList
data={collection}
renderItem={({ item }) => (
<Text>{item.title}</Text>
)}
/>
According to docs you have to pass new variables to refetch otherwise refetch will use initial values.
In this case (custom hook) you have 2 ways to solvethis problem:
return variables from your custom hook (taken from useQuery),
return some own refetch function.
1st option needs 'manual' variables updating like:
refetch( { ...variablesFromHook, collectionHandle: currentSubCategories } );
In 2nd case you can create myRefetch (and return as refetch) taking collectionHandle parameter to call refetch with updated variables - hiding 'complexity' inside your hook.
Both cases needs refetch call after updating state (setSubCategories) so you should use this refetch inside useEffect with [currentSubCategories] dependency ... or simply don't use state, call refetch directly from event handler (in onSubCategorySelect).

How to use the $filter variable on graphql query under the Connect component?

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

Filtering normalized data structure

Forgive me, I'm new to normalizr+redux. I've managed to normalize my data and create a reducer and end up with :
state = {
installations:{
"1":{...},
"2":{...}
}
}
I would then like to filter this data for use in a UI component into two separate categories (in this case where the installation.operator is equal to the current user). I've managed an implementation that works however it seems exhaustive:
const mapStateToProps = (state, ownProps) => {
console.log("mapStateToProps", state.installations);
let assignedInstallations = Object.keys(state.installations)
.filter(i => {
return state.installations[i].operator == state.login;
})
.map(i => {
return state.installations[i];
});
let unassignedInstallations = Object.keys(state.installations)
.filter(i => {
return state.installations[i].operator != state.login;
})
.map(i => {
return state.installations[i];
});
return {
assignedInstallations,
unassignedInstallations,
loginUserId: state.login
};
};
I'm also new to ES6 and am not across all the new syntax shortcuts etc so I suspect there are much better ways to do this.
Is there a more succinct approach with a similar outcome?
you can do this with only one reduce():
const mapStateToProps = (state, ownProps) => {
console.log("mapStateToProps", state.installations);
let {assignedInstallations,
unassignedInstallations } = Object.keys(state.installations)
.reduce(function(acc, cur, i){
if(state.installations[i].operator == state.login){
acc.assignedInstallations.push(state.installations[i]);
}else{
acc.unassignedInstallations .push(state.installations[i]);
}
return acc
}, {assignedInstallations: [], unassignedInstallations: [] })
return {
assignedInstallations,
unassignedInstallations,
loginUserId: state.login
};
};
lodash (An utility library) have a notion of collection (Here is an example https://lodash.com/docs/4.17.4#filter for filter function). It takes as input Object or Array and returns an Array. It seems to fit to your needs. Here is the refactored code:
import {
filter,
} from 'lodash'
const mapStateToProps = (state, ownProps) => {
let assignedInstallations = filter(state.installations, installation => installation.operator == state.login);
let unassignedInstallations = filter(state.installations, installation => installation.operator != state.login);
return {
assignedInstallations,
unassignedInstallations,
loginUserId: state.login
};
};

React Native ListView - rowHasChanged doesn't fire

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
})
}