How can I rerender only one item in a flatlist? - reactjs

I have products with a star icon to add this product in wishlist. I map 10 list of products and each map has 3 products like:
(I Map it in Pagerview to swipe to the next products)
Products Component
const ListProducts = [
{
id: 1,
products: [{
product_id: 1,
photos: [...]
}]
},
{
id: 2,
products: [{
product_id: 1,
photos: [...]
}]
}
{
id: 3,
products: [{
product_id: 1,
photos: [...]
}]
},
{
id: 4,
products: [{
product_id: 1,
photos: [...]
}]
}
...
];
function isEq(prev, next) {
if(prev.is_wishlist === next.is_wishlist) {
return true;
}
}
const Item = memo(({ id, photos, is_wishlist, onPress, onPressWishlist }) => {
const findProductIdInWishList = is_wishlist.find((el => el.product_id === id));
return (
<Button style={s.imgBtn} onPress={() => onPress(id)}>
<Button onPress={() => onPressWishlist(id)} style={s.starBtn}>
<AntDesign name={findProductIdInWishList ? 'star' : 'staro'} size={20} color={globalStyles.globalColor} />
</Button>
<ImageSlider photos={photos} />
</Button>
)
// #ts-ignore
}, isEq);
const wishlist = useSelector((state) => state.WishList.wishlist);
const dispatch = useDispatch();
const renderItem: ListRenderItem<IProduct> = ({ item }) => (
<Item
id={item.id}
photos={item.photos}
is_wishlist={wishlist}
onPressWishlist={handlePressWishList}
/>
)
const handlePressWishList = (product_id: string) => {
dispatch(addAndRemoveProductToWishList({product_id}));
};
List of Products component
Products Map:
<PagerView onPageSelected={(e) => handleSetAllIndexes(e.nativeEvent.position)} style={s.container} initialPage={index}>
{
allProducts.map((el, i) => (
<View style={s.viewsContainer} key={i}>
{ allIndex.includes(i) ? (
<View style={s.viewsInnerContainer}>
{ /* products */ }
<Products products={el.products as IProduct[]} total_price={el.total_price} product_name={el.packet_name} />
</View>
) : (
<View style={s.loadingContainer}>
<Loader size={'large'} color='#fff' />
</View>
)
}
</View>)
)
}
</PagerView>
if I click on star icon its dispatch and it goes fast but if I swipe to other products maybe to the last, then I press the star icon to dispatch then its a delay/lag you can see it
I dont add the full code because there are some snippets that has nothing to do with problem.
PS:
Video

I think there are a few issues in your code:
1. Wrong dependency list for useMemo.
In your Item component, you should pass the list of dependency, rather than a compare function:
const Item = memo(({ id, photos, is_wishlist, onPress, onPressWishlist }) => {
...
// #ts-ignore
}, isEq); // <- this is wrong
// Instead, you should do this:
}, [is_wishlist]); // <- this is correct, if you only want to update Item component when `is_wishlist` is changed
2. Never use index as key if item can be reordered
In your products maps component, you are doing:
allProducts.map((el, i) => (
<View style={s.viewsContainer} key={i}>
You should pass id instead, so React will not re-render all items when you insert a new item at the beginning or in the middle:
<View style={s.viewsContainer} key={el.id}>
3. Passing wishlist to all Items, however, wishlist will be updated whenever user click star button on any item.
This causes all Item to re-generate memoized component, because wishlist is changed.
What you want to do here is only passing essential data to Item, which is inWishList (or findProductIdInWishList in your code), which only get changed for affected item:
const renderItem: ListRenderItem<IProduct> = ({ item }) => {
const inWishList= wishlist.find((el => el.product_id === id));
return (
<Item
id={item.id}
photos={item.photos}
inWishList={inWishList}
onPressWishlist={handlePressWishList}
/>
)
}

I am going to edit my answer instead of comment. Before my code, let me explain first. In your current code, whenever 'allProducts' changes, everything will re-render. Whenever 'allIndex' changes, everything will re-render too. The longer the list, the more lag it will be.
Can you try 'useCallback' in this case?
const renderItem = React.useCallback((el, i) => (
<View style={s.viewsContainer} key={i}>
{allIndex.includes(i) ? (
<View style={s.viewsInnerContainer}>
<Products />
</View>
) : (
<Loading />
)}
</View>
),[allIndex])
{allProducts.map(renderItem)}
Now, renderItem will re-render when 'allIndex' changes. Instead of 'PagerView', I still recommend 'FlatList' with 'horizontal={true}' and 'some styles'. If still wanna use 'PagerView', how about 'Lazy' components? 'Lazy components' does not render the components before they came into user view. So, before they are seen, they do not take part in re-redner.

The issue is connected to a way how you edit your data to hold the new value. It looks like the act of adding an item to a wishlist causes all the previous items to re-render.
Therefore the first one works without an issue, but the last one takes a while as it needs to re-render all the other items before.
I would start by changing the key from index to an actual ID of a product block since that could prevent the other "pages" from re-rendering.
If that fails you will probably need to take this block of code into a workshop and check for useless re-renders.

Related

I don't get the expected value of a react props in child component

I'm trying to manage some clients in a react js application (that I'm maintaining), it was first written in classes components but I'm now adding a functional child component and im not sure if this is the source of the problem or not (react-table examples only use functional component)
I have a main component that will do the data GET from a rest API and save it in state "entries" then I passe it to a child component as a props to render the data in a react-table, the problem is in this section as I have some buttons to edit and delete the data in react-modal, when I try access the props.entries after the buttons clicks I have an empty array of props.entries.
Here's the sandbox of the issue : https://codesandbox.io/s/stale-prop-one-forked-r6cevx?file=/src/App.js
I did a console.log when the delete button is clicked, and you can see that en entries array is empty.
You need to pass the showEditModal & showEditModal in useMemo dependency array. Since you dependency array is empty, when you click on edit or delete, it just points to the old function reference and it have the old entries value.
You can check the entries values by console.log.
Hope this solve your problem
const showEditModal = useCallback(
(client_id) => {
const tmpClient = props.entries.filter(function (el) {
return el._id === client_id;
})[0];
setClient(tmpClient);
setEditModal(true);
console.log('aaa', props);
console.log(client_id);
console.log(props.entries);
console.log(tmpClient);
},
[props.entries]
);
const showDeleteModal = useCallback(
(client_id) => {
console.log('showDeleteModal entries : ', entries);
const tmpClient = entries.filter(function (el) {
return el._id === client_id;
})[0];
setClient(tmpClient);
setDeleteModal(true);
console.log('Delete', entries);
console.log(client_id);
console.log(tmpClient);
},
[props.entries]
);
const columns = React.useMemo(
() => [
{
Header: 'fact',
accessor: 'fact'
},
{
Header: 'Actions',
accessor: 'length',
Cell: ({ cell }) => (
<>
{cell.row.values.length}
<Tooltip title='Supprimer' placement='top'>
<IconButton
variant='outlined'
id={cell.row.values._id}
onClick={() => showDeleteModal(cell.row.values.length)}
>
<DeleteIcon />
</IconButton>
</Tooltip>
<Tooltip title='Modifier' placement='top'>
<IconButton
variant='outlined'
id={cell.row.values.length}
onClick={() => showEditModal(cell.row.values.length)}
>
<EditIcon />
</IconButton>
</Tooltip>
</>
)
}
],
[showEditModal, showDeleteModal]
);

How can I persist id when i refresh page?

Currently,I'm trying to store the index and id in localstorage so that i can get the specific id and Index when i refresh the page.
so I tried like this :
ParentComponent
//this index and id is what i want to get ///
const eachComponent = (index, id, name) => (
<DataSide id={id} key={index} onClick={() => setShow({ [id]: !name })}>
<SettingMenu
show={name}
chart={chart[index]}
changeLayout={changeLayout}
panelId={id}
panelIndex={index}
setChangeLayout={setChangeLayout}
/>
{chartcomponentsEle(
index,
id,
title,
barData,
scatterData,
bubbleData,
info
)}
</DataSide>
);
const layout = [
//this index and id is what i want to get ///
eachComponent(0, "first", show.first),
eachComponent(1, "second", show.second),
eachComponent(2, "third", show.third),
eachComponent(3, "fourth", show.fourth),
eachComponent(4, "fifth", show.fifth),
eachComponent(5, "sixth", show.sixth),
eachComponent(6, "seventh", show.seventh),
eachComponent(7, "eighth", show.eighth),
];
when i click menu there is Link like this:
...
<Box>
<Link
to={{
pathname: `/csvFile/${chart}`,
state: {
panelIndex: panelIndex,
panelId: panelId,
},
}}
>
<span>
<InsertDriveFileIcon style={{ fontSize: 30 }} />
<p>csv파일</p>
</span>
</Link>
</Box>
this is the child Component where i want to set the id and index to localstorage :
const { panelId, panelIndex } = location.state;
const { info } = state;
const { setInfo, AxisUpdate } = data;
let savedState = JSON.stringify(location.state);
localStorage.setItem("myState", savedState);
So i tried to get the item using getItem in the component where there is Link
but it didn't work at all
So I want to know how and where i should use getItem .
Thank you in advance
Make sure
localStorage.setItem("myState", savedState);
is getting called on click, pls debug.
As localStorage is global, you can easily get it in any scope with localStorage.getItem("myState");

What is the best way to update an object in a usestate and see the change immediately

const TestScreen = (props) => {
const [data, setData] = useState([
{ "id": "0", "user": "Lisa","amount": 1 },
]);
return(
<View style={{
flex:1,
alignItems:'center',
justifyContent:'center'
}}>
<Text>amount : {data[0].amount}</Text>
<Text>User : {data[0].user}</Text>
<Button title="update" onPress={()=>setData(??????)}></Button>
</View>
)
}
export default TestScreen;
what is the best way to add an amount number on the user Lisa? i can do
// setData([{ "id": "0", "user": "Lisa","amount": data[0].amount + 1}])
but what i have 5 users or 20?
even with a returning function nothing gets updated exept console logged witch show me the actual value
let countAddFunc =(getArr)=>{
let arr = getArr
arr[0].amount++
console.log(arr[0].amount);
return(
arr
)
}
<Button title="update" onPress={()=>setData(countAddFunc(data))}></Button>
One of the key concepts in React is Do Not Modify State Directly. This can be tricky sometimes, especially when dealing with nested data like in your example (a number as a property of an object inside an array).
Below, I refactored your code and added comments to help explain. By creating a function to update the state, and passing that function to each child component's props, the child component will be able to update the state by calling the function.
const {useState} = React;
// component dedicated to displaying each item in the array
const Item = (props) => (
<div style={{
flex: 1,
alignItems: 'center',
justifyContent: 'center',
}}>
<div>Amount: {props.item.amount}</div>
<div>User: {props.item.user}</div>
<button onClick={() => {
props.updateAmount(props.item.user);
}}>Increment</button>
</div>
);
const ItemList = () => {
const [data, setData] = useState([
{
id: '0',
user: 'Lisa',
amount: 1,
},
{
id: '1',
user: 'Walter',
amount: 3,
},
]);
const updateAmount = (user, changeAmount = 1) => {
// find the index of the item we want
const index = data.findIndex(item => item.user === user);
// return early (do nothing) if it doesn't exist
if (index === -1) return;
const item = data[index];
// don't modify state directly (item is still the same object in the state array)
const updatedItem = {
...item,
amount: item.amount + changeAmount,
};
// again, don't modify state directly: create new array
const updatedArray = [...data];
// insert updated item at the appropriate index
updatedArray[index] = updatedItem;
setData(updatedArray);
};
return (
<ul>
{data.map(item => (
<li key={item.id}>
<Item item={item} updateAmount={updateAmount} />
</li>
))}
</ul>
);
};
ReactDOM.render(<ItemList />, document.getElementById('root'));
<script src="https://unpkg.com/react#17.0.2/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom#17.0.2/umd/react-dom.development.js"></script>
<div id="root"></div>
can you try like this once, pass second param as user id you want to update
<Button title="update" onPress={()=>setData(countAddFunc(data, 0))}></Button>
let countAddFunc =(getArr, id)=>{
const arrCopy = [...getArr]
const user = arrCopy.find(u => u.id === id )
if (user) {
user.amount++
}
return arrCopy
}
actually you are modifying the state directly, and we can not update state directly and getArr is just a refrence to the data in state, so, we created a copy of array, and modified copied array, and then we set this new array into the state, about the code throwing undefined error, add a check, if (user) user.amount++ and make sure id you send onPress={()=>setData(countAddFunc(data, 0))} actually exist
Think of setState() as a request rather than an immediate command to update the component. For better perceived performance, React may delay it, and then update several components in a single pass. React does not guarantee that the state changes are applied immediately. setState() does not always immediately update the component. It may batch or defer the update until later. This makes reading this.state right after calling setState() a potential pitfall.
Read the official documentation for more info here
Also, alternatively you can use useRef or useEffect with dependency array if you want the callback to immediately fire and do the changes.

How can I conditionally change data on a flat list in React Native?

in my app I have such scenario:
when opening on first time I need to show all companies on screen, when user select a category I need to change the data of my FlatList to selected category companies. I use conditional select data for the FlatList but this is not working. here is my code:
const Home = (props) => {
const dispatch = useDispatch();
const companies = useSelector(state => state.companies.availableCompanies);
const selectedCatCompanies = useSelector(state => state.selectedCatCompanies.availableSelectedCatCompanies)
const loadCompanies = useCallback(async () => {
await dispatch(CompaniesActions.fetchCompanies(1, 1, 1));
}, [dispatch]);
useEffect(() => {
loadCompanies().then(() => {
setIsLoading(false)
})
}, [dispatch, loadCompanies]);
const renderCards = (itemData) => {
return (
<View style={styles.cardContainer}>
<Card
companyId={itemData.item.id}
companyName={itemData.item.name}
companyImage={itemData.item.image}
companyMainAddress={itemData.item.mianAddress}
companyPhoneNumber={itemData.item.telephoneNumber}
companyDetails={itemData.item.details}
onPress={() => { props.navigation.navigate('CardDetails')}}
/>
</View>
)
}
return (
<SafeAreaView style={styles.screen}>
<MainHeader />
<SearchBar />
<FlatList
ListHeaderComponent={
<>
<Text style={styles.timeText}>Choose appropriate time:</Text>
<View style={styles.timeContainer}>
<PrimaryButton
title='Choose a date'
hasArrow={true}
arrowSize={hp(2)}
/>
<PrimaryButton
title='Choose a time'
hasArrow={true}
arrowSize={hp(2)}
/>
</View>
<TypeSelection
options={[
{ name: 'Premium', selected: false },
{ name: 'LifeStyle', selected: false },
{ name: 'Many Amenities', selected: false },
{ name: 'Gourment', selected: false },
]}
selectedButtonStyles={styles.selectedButton}
selectedButtonTextStyles={styles.selectedButtonText}
/>
</>
}
nestedScrollEnabled={true}
showsVerticalScrollIndicator={false}
data={selectedCatCompanies == [] ? companies : selectedCatCompanies} //here is what I set
keyExtractor={item => item.id.toString()}
renderItem={renderCards}
/>
</SafeAreaView>
)
and here is my screenshot for better understanding:
I get my companies from 2 different APIs. with this code I only get my companies when user select a category but if non category selected it will not show me the result. hope I explain my question well.
I don't have your full code so fishing here. From your code guessing, you are using Redux.
Step 1) change selectedCatCompanies == [] to selectedCatCompanies.length === 0
[Thanks to Linda's input in the comment I modified this step 1]
If step 1 does not work out then,
Step 2) Try looking at CompaniesActions action creator and its corresponding reducer to confirm if you are handling both the use cases of companies and selectedCatCompanies. I think the issue should be in the actions/reducer.
If step 2 does not work out then,
Step 3) I would console.log the selectedCatCompanies state to validate if it is being recorded correctly in Redux. If not I would then go back to step 2.
If step 3 also does not work out then,
Step 3) If you are successfully logging the selectedCatCompanies state then you can try to conditionally two different FlatLists instead of trying to conditionally render two different data sources.

TypeError: Cannot read property 'map' of undefined even though component isn't called yet

One of the pages in an app I'm currently building is a 'Category' page. It renders a table of rows from a particular category. These rows also have comments on them.
col x
col y
Comments
row1 x
row1 y
view
row2 x
row1 y
view
I'm using react redux, when I go to a 'Category' page, no comments have been added to state yet, only the relevant rows to the category have been added (their comments are in a different table in my database. I am trying to make a component (the 'view' button) that will open up a dialog where the comments for the chosen row are fetched and displayed.
The problem is that as soon as I go to the Category page, I get the error in my title (the error pointing to the map function I've starred below)
class MyComments extends Component {
state = {
open: false,
oldPath: "",
newPath: "",
};
handleOpen = () => {
let oldPath = window.location.pathname;
const { categoryId, rowId } = this.props;
const newPath = `/categories/${categoryId}/row/${rowId}`;
if (oldPath === newPath) oldPath = `/categories/${categoryId}`;
window.history.pushState(null, null, newPath);
this.setState({ open: true, oldPath, newPath });
this.props.getRow(this.props.rowId);
};
handleClose = () => {
window.history.pushState(null, null, this.state.oldPath);
this.setState({ open: false });
this.props.clearErrors();
};
render() {
const {
row: { comments },
UI: { loading },
} = this.props;
const commentsDialog = loading ? (
<div>
<CircularProgress size={200} thickness={2} />
</div>
) : (
<List>************
{comments.map((comment) => (
<ListItem>{comment.body}</ListItem>
))}
</List>***********
);
return (
<Fragment>
<Button onClick={this.handleOpen}>
<UnfoldMore color="primary" />
</Button>
<Dialog
open={this.state.open}
onClose={this.handleClose}
fullWidth
maxWidth="sm"
>
<Button onClick={this.handleClose}>
<CloseIcon />
</Button>
<DialogContent>{commentsDialog}</DialogContent>
</Dialog>
</Fragment>
);
}
}
MyComments.propTypes = {
clearErrors: PropTypes.func.isRequired,
getRow: PropTypes.func.isRequired,
rowId: PropTypes.string.isRequired,
row: PropTypes.object.isRequired,
UI: PropTypes.object.isRequired,
};
const mapStateToProps = (state) => ({
row: state.data.row,
UI: state.UI,
});
const mapActionsToProps = {
getRow,
clearErrors,
};
export default connect(mapStateToProps, mapActionsToProps)(MyComments);
One thing you might notice is that I push to a new url when the dialog is opened. One thing I did to debug this was I commented out the comments.map above (surrounded by *'s), then I was able to click on the dialog without error and go to the new url. once on the url, I changed my code back to the comments.map, and they displayed in a list in the dialog. So my problem is, my app is trying to render these options before they have been added to my state. I guess I'm wondering why my app cares about this map function before the dialog has opened?
My 'getRow' function returns a json as below so that's not the problem, I've console logged it to make sure the data is being fetched correctly, between this and what I just said above, I dont think that my actions or reducers are the problem, so I didnt add them but let me know if theyre relevant.
{
"categoryId": "id",
"index": "2",
"body": "New test",
"disapproveCount": 0,
"approveCount": -1,
"createdAt": "2021-03-05T23:16:26.142Z",
"rowId": "id",
"comments": [
{
"index": 2,
"body": "hello",
"rowId": "id"
}
]
}
I've had this problem for about a week but I finished everything else first as I couldn't figure it out and also thought it would be too difficult to explain on stackoverflow. I hope this makes some sort of sense, still don't know if im explaining it correctly/fully so I'll be very grateful/surprised with any suggestions.
Also, ideally I would just like to display these comments not in a dialog, so that no buttons need to be pressed to see them, but I was having this problem before I tried a dialog, and I thought loading a dialog and on a new url would fix the problem, i.e., the options wouldn't be attempted to be rendered until I opened this new page/url
This could possible be relevant:
Category page (called document)
class document extends Component {
componentDidMount() {
const categoryId = this.props.match.params.categoryId;
this.props.getCategory(categoryId);
}
render() {
const categoryId = this.props.match.params.categoryId;
const { category, loading } = this.props.data;
let placeholder = !loading ? (
<div>
<center>
<h1>{categoryId}</h1>
<h1> </h1>
</center>
<br></br>
<Paper>
<TableContainer>
<TableHead>
(...)
</TableHead>
<TableBody>
{category.map((row) => (
<TableRow key={row.rowId}>
(...)
<TableCell align="left">
<MyComments rowId={row.rowId} categoryId={categoryId} />
</TableCell>
(...)
</TableRow>
))}
</TableBody>
</TableContainer>
</Paper>
<br></br>
<MyForm categoryId={categoryId} />
</div>
) : (
<p>Loading...</p>
);
return <div>{placeholder}</div>;
}
}
document.propTypes = {
getCategory: PropTypes.func.isRequired,
data: PropTypes.object.isRequired,
};
const mapStateToProps = (state) => ({
data: state.data,
});
export default connect(mapStateToProps, { getCategory })(document);
I'm not sure if this is the exact problem, but I think a Tl;Dr is: why does the map function in my dialog care about something undefined when it hasn't been called yet?
Any help greatly appreciated!
Place a ? symbol after the object key. It will check if the object key's value is undefined or null before proceeding further.
<List>
{comments?.map((comment) => (
<ListItem>{comment.body}</ListItem>
))}
</List>

Resources