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();
Related
I'm building a checklist app with multiple tabs. It works but when the list grows larger, it's not performing very snappy when I want to check 1 item for instance. I have the feeling this is because the entire state (consisting of all items in all tabs) is updated, when I just want to update 1 item. The tabs and items are generated dynamically (ie, at compile-time I don't know how many tabs there will be). Any idea how this could be done more efficiently?
This is the (stripped down) state provider:
export default class InpakStateProvider extends React.Component {
state = {projectName: " ", tabs: [{name: " ", items: [{checked: false, name: " "}]}]};
DeleteItem = (categoryname: string, itemname: string) => {
let stateTabs = this.state.tabs;
var tab = stateTabs.find((tab) => tab.name == categoryname);
if(tab){
let index = tab.items.findIndex(el => el.name === itemname);
tab.items.splice(index, 1);
}
this.setState({projectName: this.state.projectName, tabs: stateTabs})
};
CheckItem = (categoryname: string, itemname: string) => {
var tab = this.state.tabs.find((tab) => tab.name == categoryname);
if(tab){
let index = tab.items.findIndex(el => el.name === itemname);
tab.items[index] = { ...tab.items[index], checked: !tab.items[index].checked };
}
this.setState({projectName: this.state.projectName, tabs: this.state.tabs});
};
ClearChecks = () => {
let stateTabs = this.state.tabs;
stateTabs.forEach((tab) => {
let tabItems = [...tab.items];
tabItems.forEach((item) => item.checked = false);
});
this.setState({projectName: this.state.projectName, tabs: stateTabs})
}
render(){
return (
<Context.Provider
value={{
projectName: this.state.projectName,
tabs: this.state.tabs,
DeleteItem: this.DeleteItem,
CheckItem: this.CheckItem,
ClearChecks: this.ClearChecks,
}}
>
{this.props.children}
</Context.Provider>
);
}
}
The issue here is that all list components are being re-rendered upon updating the state. My advice is to move the state of checked inside of the list item component. Or if you don't want to do that, I advise you to read about React memoization.
If you go for the memoziation approach if you update the state, and the props of the list item didn't change, this will not re-render the unchanged components, it will only trigger the re-render for the components with the prop checked that has changed.
Here's the documentation for memoization if it helps: https://reactjs.org/docs/react-api.html.
Also, on another note, always go for FlatLists instead of using map. You won't notice a big difference with a small dataset, but performance takes a big hit with mid-large datasets.
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).
I have two different components one is CategoryList which is a flatlist and regionlist is a section list. I would like to display the CategoryList first and when item clicked the regionlist will show however I m not sure why it is not working. (I use context to store the state)
{!isToggle ? (
<CategoryList></CategoryList>
) : (
<RegionList style={styles.regionListStyle}></RegionList>
)}
I also create a button to see if it is a problem about the context but it is not.
const ToggleContext = createContext(true);
export const useToggle = () => {
return useContext(ToggleContext);
};
export function ToggleProvideData({children}) {
const [isToggle, setToggle] = useState(true)
return <ToggleContext.Provider value={{isToggle,setToggle}}>
{children}
</ToggleContext.Provider>;
}
I just wonder conditional render is it not working for flatlist?
UPDATE: I tried create a state to store the useContext isToggle but it only appears for like 1 sec
I guess isToggle as a boolean variable, hence you can use the below code for rendering conditionally
{ (isToggle === true) &&
<CategoryList></CategoryList>
}
{ (isToggle === false) &&
<RegionList style={styles.regionListStyle}></RegionList>
}
I've created some components in a for loop and push them to an array. Can i get any component from this array to change its props -like title ?
fields = [];
for (let i = 0; i < assets.fieldNames[assets.systemLang].length; i++) {
fields.push(
<InfoField
handlePress={() => this.fieldPressed(i)}
key={i}
title={assets.fieldNames[assets.systemLang][i]}
value="" />
);
};
.....
<View style={styles.infoFields}>
{
fields
}
</View>
and i have a function that i need something like this
changeComponentTitle = () => {
fields[indexForComponent].props.title = "new Title"
}
You'll need to make sure that you trigger a re-render in some way after you make the update (perhaps by putting fields in a state variable and calling setState as mentioned in the comments) but you can use cloneElement to essentially update a single prop of an existing element: https://reactjs.org/docs/react-api.html#cloneelement
changeComponentTitle = () => {
fields[indexForComponent] = React.cloneElement(fields[indexForComponent], {title: "new Title"});
}
I make us several Switch Components in one view. When I switch on one switch, I want all others to switch off. Currently, I set the boolean value property via the state. This results in changes happen abruptly because the switch is just re-rendered and not transitioned.
So how would you switch them programmatically?
EDIT 2: I just discovered that it runs smoothly on Android so it looks like an iOS-specific problem.
EDIT: part of the code
_onSwitch = (id, switched) => {
let newFilter = { status: null };
if (!switched) {
newFilter = { status: id };
}
this.props.changeFilter(newFilter); // calls the action creator
};
_renderItem = ({ item }) => {
const switched = this.props.currentFilter === item.id; // the state mapped to a prop
return (
<ListItem
switchButton
switched={switched}
onSwitch={() => this._onSwitch(item.id, switched)}
/>
);
};