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

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

Related

How can I rerender only one item in a flatlist?

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.

React Data Table Component - getting Warning: Each child in a list should have a unique "key" prop

I'm using React Data Table Component, and applied filter exactly same as here:
() => {
const [filterText, setFilterText] = React.useState('');
const [resetPaginationToggle, setResetPaginationToggle] = React.useState(false);
const filteredItems = fakeUsers.filter(
item => item.name && item.name.toLowerCase().includes(filterText.toLowerCase()),
);
const subHeaderComponentMemo = React.useMemo(() => {
const handleClear = () => {
if (filterText) {
setResetPaginationToggle(!resetPaginationToggle);
setFilterText('');
}
};
return (
<FilterComponent onFilter={e => setFilterText(e.target.value)} onClear={handleClear} filterText={filterText} />
);
}, [filterText, resetPaginationToggle]);
return (
<DataTable
title="Contact List"
columns={columns}
data={filteredItems}
pagination
paginationResetDefaultPage={resetPaginationToggle} // optionally, a hook to reset pagination to page 1
subHeader
subHeaderComponent={subHeaderComponentMemo}
selectableRows
persistTableHead
/>
);
}
And now I'm getting error message as
Warning: Each child in a list should have a unique "key" prop.
Check the top-level render call using . It was passed a child from Sample.
in FilterComponent (at --Sample.js:288)
I do have the id value in my data, and I also read the article this problem has solved from data-table-component but I'm still on getting this warning.
What do I have to prevent this message?
Is there alternate way to turn it off this message?
try this,
const filteredItems = fakeUsers.filter(
item => item.name && item.name.toLowerCase().includes(filterText.toLowerCase()),
);
let records = filteredItems.map((d) => ({ ...d, key: d.ID }));
..............
return (
<DataTable
title="Contact List"
columns={columns}
data={records}
pagination
paginationResetDefaultPage={resetPaginationToggle} // optionally, a hook to reset pagination to page 1
subHeader
subHeaderComponent={subHeaderComponentMemo}
selectableRows
persistTableHead
/>
I solved this problem, and now found out why I was the only one who faced this issue.
I was using another Memo for eanbling the control button.
The warning message was came because, there are 2 'memo'es.
So, I put diffrent key pros to each 'Memo'es as below
--
<FilterComponent key="func_filter" onFilter={e => setFilterText(e.target.value)} onClear={handleClear} filterText={filterText} />
const actionsMemo = useMemo(() =>
<ExportBtn key="func_excel" onExport={() => downloadCSV({rows})} />, [{rows}]);
Then warning message disappeared

React: Uncaught TypeError: Cannot read property 'localeCompare' of undefined

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

How to render a different component with React Hooks

I have a parent component with an if statement to show 2 different types of buttons.
What I do, on page load, I check if the API returns an array called lectures as empty or with any values:
lectures.length > 0 ? show button A : show button B
This is the component, called main.js, where the if statement is:
lectures.length > 0
? <div onClick={() => handleCollapseClick()}>
<SectionCollapse open={open} />
</div>
: <LectureAdd dataSection={dataSection} />
The component LectureAdd displays a + sign, which will open a modal to create a new Lecture's title, while, SectionCollapse will show an arrow to show/hide a list of items.
The logic is simple:
1. On page load, if the lectures.lenght > 0 is false, we show the + sign to add a new lecture
OR
2. If the lectures.lenght > 0 is true, we change and show the collpase arrow.
Now, my issue happens when I add the new lecture from the child component LectureAdd.js
import React from 'react';
import { Form, Field } from 'react-final-form';
// Constants
import { URLS } from '../../../../constants';
// Helpers & Utils
import api from '../../../../helpers/API';
// Material UI Icons
import AddBoxIcon from '#material-ui/icons/AddBox';
export default ({ s }) => {
const [open, setOpen] = React.useState(false);
const [ lucturesData, setLecturesData ] = React.useState(0);
const { t } = useTranslation();
const handleAddLecture = ({ lecture_title }) => {
const data = {
"lecture": {
"title": lecture_title
}
}
return api
.post(URLS.NEW_COURSE_LECTURE(s.id), data)
.then(data => {
if(data.status === 201) {
setLecturesData(lucturesData + 1) <=== this doesn't trigger the parent and the button remains a `+` symbol, instead of changing because now `lectures.length` is 1
}
})
.catch(response => {
console.log(response)
});
}
return (
<>
<Button variant="outlined" color="primary" onClick={handleClickOpen}>
<AddBoxIcon />
</Button>
<Form
onSubmit={event => handleAddLecture(event)}
>
{
({
handleSubmit
}) => (
<form onSubmit={handleSubmit}>
<Field
name='lecture_title'
>
{({ input, meta }) => (
<div className={meta.active ? 'active' : ''}>
<input {...input}
type='text'
className="signup-field-input"
/>
</div>
)}
</Field>
<Button
variant="contained"
color="primary"
type="submit"
>
ADD LECTURE
</Button>
</form>
)}
</Form>
</>
)
}
I've been trying to use UseEffect to trigger a re-render on the update of the variable called lucturesData, but it doesn't re-render the parent component.
Any idea?
Thanks Joe
Common problem in React. Sending data top-down is easy, we just pass props. Passing information back up from children components, not as easy. Couple of solutions.
Use a callback (Observer pattern)
Parent passes a prop to the child that is a function. Child invokes the function when something meaningful happens. Parent can then do something when the function gets called like force a re-render.
function Parent(props) {
const [lectures, setLectures] = useState([]);
const handleLectureCreated = useCallback((lecture) => {
// Force a re-render by calling setState
setLectures([...lectures, lecture]);
}, []);
return (
<Child onLectureCreated={handleLectureCreated} />
)
}
function Child({ onLectureCreated }) {
const handleClick = useCallback(() => {
// Call API
let lecture = callApi();
// Notify parent of event
onLectureCreated(lecture);
}, [onLectureCreated]);
return (
<button onClick={handleClick}>Create Lecture</button>
)
}
Similar to solution #1, except for Parent handles API call. The benefit of this, is the Child component becomes more reusable since its "dumbed down".
function Parent(props) {
const [lectures, setLectures] = useState([]);
const handleLectureCreated = useCallback((data) => {
// Call API
let lecture = callApi(data);
// Force a re-render by calling setState
setLectures([...lectures, lecture]);
}, []);
return (
<Child onLectureCreated={handleLectureCreated} />
)
}
function Child({ onLectureCreated }) {
const handleClick = useCallback(() => {
// Create lecture data to send to callback
let lecture = {
formData1: '',
formData2: ''
}
// Notify parent of event
onCreateLecture(lecture);
}, [onCreateLecture]);
return (
<button onClick={handleClick}>Create Lecture</button>
)
}
Use a central state management tool like Redux. This solution allows any component to "listen in" on changes to data, like new Lectures. I won't provide an example here because it's quite in depth.
Essentially all of these solutions involve the same solution executed slightly differently. The first, uses a smart child that notifies its parent of events once their complete. The second, uses dumb children to gather data and notify the parent to take action on said data. The third, uses a centralized state management system.

Accessing Apollo's loading boolean outside of Mutation component

The Mutation component in react-apollo exposes a handy loading boolean in the render prop function which is ideal for adding loaders to the UI whilst a request is being made. In the example below my Button component calls the createPlan function when clicked which initiates a GraphQL mutation. Whilst this is happening a spinner appears on the button courtesy of the loading prop.
<Mutation mutation={CREATE_PLAN}>
{(createPlan, { loading }) => (
<Button
onClick={() => createPlan({ variables: { input: {} } })}
loading={loading}
>
Save
</Button>
)}
</Mutation>
The issue I have is that other aspects of my UI also need to change based on this loading boolean. I have tried lifting the Mutation component up the React tree so that I can manually pass the loading prop down to any components which rely on it, which works, but the page I am building has multiple mutations that can take place at any given time (such as deleting a plan, adding a single item in a plan, deleting a single item in a plan etc.) and having all of these Mutation components sitting at the page-level component feels very messy.
Is there a way that I can access the loading property outside of this Mutation component? If not, what is the best way to handle this problem? I have read that you can manually update the Apollo local state using the update function on the Mutation component (see example below) but I haven't been able to work out how to access the loading value here (plus it feels like accessing the loading property of a specific mutation without having to manually write it to the cache yourself would be a common request).
<Mutation
mutation={CREATE_PLAN}
update={cache => {
cache.writeData({
data: {
createPlanLoading: `I DON"T HAVE ACCESS TO THE LOADING BOOLEAN HERE`,
},
});
}}
>
{(createPlan, { loading }) => (
<Button
onClick={() => createPlan({ variables: { input: {} } })}
loading={loading}
>
Save
</Button>
)}
</Mutation>
I face the same problem in my projects and yes, putting all mutations components at the page-level component is very messy. The best way I found to handle this is by creating React states. For instance:
const [createPlanLoading, setCreatePLanLoading] = React.useState(false);
...
<Mutation mutation={CREATE_PLAN} onCompleted={() => setCreatePLanLoading(false)}>
{(createPlan, { loading }) => (
<Button
onClick={() => {
createPlan({ variables: { input: {} } });
setCreatePLanLoading(true);
}
loading={loading}
>
Save
</Button>
)}
</Mutation>
I like the answer with React States. However, when there are many different children it looks messy with so many variables.
I've made a bit update for it for these cases:
const Parent = () => {
const [loadingChilds, setLoading] = useState({});
// check if at least one child item is loading, then show spinner
const loading = Object.values(loadingChilds).reduce((t, value) => t || value, false);
return (
<div>
{loading ? (
<CircularProgress />
) : null}
<Child1 setLoading={setLoading}/>
<Child2 setLoading={setLoading}/>
</div>
);
};
const Child1 = ({ setLoading }) => {
const [send, { loading }] = useMutation(MUTATION_NAME);
useEffect(() => {
// add info about state to the state object if it's changed
setLoading((prev) => (prev.Child1 !== loading ? { ...prev, Child1: loading } : prev));
});
const someActionHandler = (variables) => {
send({ variables});
};
return (
<div>
Child 1 Content
</div>
);
};
const Child2 = ({ setLoading }) => {
const [send, { loading }] = useMutation(MUTATION_NAME2);
useEffect(() => {
// add info about state to the state object if it's changed
setLoading((prev) => (prev.Child2 !== loading ? { ...prev, Child2: loading } : prev));
});
const someActionHandler = (variables) => {
send({ variables});
};
return (
<div>
Child 2 Content
</div>
);
};

Resources