Adding loading state to map function causes items to display as loading - javascript

I have a map function displaying items:
{data.map((item, index) => ())}
Within this function I would like to have an onClick that conditionally displays a loading state when loading is true.
const [loading, setLoading] = useState(false)
{data.map((item, index) => (
<span
onClick={() =>
handleDelete()}>
{loading ? (
<LoadingSpinner />
) : (
<TrashIcon />)}
</span>
))}
When I do this it causes all items in the list to display the loading state, not just the item clicked on.
Is there a way I can achieve this using the index?

All your map items are listening to a single boolean state value, instead, you can keep track of the index of the clicked item, or if you want to be able to click and show the loading state for multiple items you can use an array or Set.
Below is an approach using Set
const [loadingIndices, setLoadingIndices] = useState(new Set());
{data.map((item, index) => (
<span
onClick={() =>
handleDelete(index)}>
{loadingIndices.has(index) ? (
<LoadingSpinner />
) : (
<TrashIcon />)}
</span>
))}
Now in your handleDelete function, you can add index of the clicked element to the set in state.
handleDelete = (selectedIndex) => {
setLoadingIndices((prev) => new Set([...prev, selectedIndex]));
// ...
// ...
// And to remove the element from loading state
setLoadingIndices((prev) => {
const updated = new Set(prev);
updated.delete(selectedIndex);
return updated;
});
};

You should move your state in to the items instead of the parent. Updating state inside the parent component will cause all children to re-render.
const childComponent = (props) => { // you can pass the main loading state as a starting point
const [loading,setLoading] = useState(props.loading) // initial loader state
if(loading) return <LoadingSpinner />
return (
<button onClick={()=> setLoading(prev => !prev)} />
)}
you can then mutate this to display different elements using if statements like above.

If you don't mind the re-render of children, You can use this index logic.
const [loading, setLoading] = useState(null);
const handleDelete = (index) => {
setLoading(index);
}
{data.map((item, index) => (
<span
onClick={() =>
handleDelete(index)}>
{loading === index ? (
<LoadingSpinner />
) : (
<TrashIcon />)}
</span>
))}

Related

How to add active on Click after map in child component? React

I need to dynamically change the class to "active". I do this by checking indexes. The problem is that the active class changes IMMEDIATELY in all displayed li, and not in one particular one. This only happens when the logic is in the parent component, if you move the useState and onTypeActive to the child component everything works fine.
Parent component
const [activeSyze, setActiveSyze] = useState(0);
const onSyzeActive = (index) => {
setActiveSyze(index);
};
return (
<>
{pizzaJson.map((item, index) => (
<PizzaBlock
key={item.id}
onSyzeActive={(index) => onSyzeActive(index)}
activeSyze={activeSyze}
{...item}
item={item}
indexPizza={index}
/>
))}
</>.
Child comp
return (
<ul>
{sizes.map((size, index) => (
<li
key={index}
onClick={() => onSyzeActive(index)}
className={activeSyze === index ? "active" : ""}
>
{size} см.
</li>
))}
</ul>
)

React Toggle Hook State

I tried to toggle individual item but unfortunately whenever I try to toggle an item the other item gets affected. Here is my code:
const FAQ = () => {
const [open, setOpen] = useState(false);
const [data, setData] = useState(faqData);
return (
<FAQSection>
<FAQTitle>Frequently Asked Questions</FAQTitle>
<Questions>
<QuestionItemDetail>
{data.map((item) => {
const { id, question, answer } = item;
return (
<div key={id}>
<QuestionItem onClick={() => setOpen(!open)}>
<QuestionItemTitle>{question}</QuestionItemTitle>
{open ? <Close /> : <Add />}
</QuestionItem>
<ReadQuestion>
{open && (
<ReadQuestionDetail>
<ReadQuestionDesc>{answer}</ReadQuestionDesc>
</ReadQuestionDetail>
)}
</ReadQuestion>
</div>
);
})}
</QuestionItemDetail>
</Questions>
</FAQSection>
);
};
What might be wrong with this because I ensured the dummy data has a unique ID.
Because you use a boolean to control all open/close. You need to use index/id to control this.
const [open, setOpen] = useState(null);
...
onClick={() => setOpen(preOpen => preOpen === id ? null : id)}
...
{open === id && (<ReadQuestionDetail>...</ReadQuestionDetail>)}
Your open state is used for all of the items in your data array, which is why it affects all of the items when toggled.
I recommend:
putting all of the data item html/jsx inside a new component.
Inside this new component, create an open state like so:
const MyItemComponent = (id, question, answer) => {
const [open, setOpen] = useState(false);
return (
<div key={id}>
<QuestionItem onClick={() => setOpen(!open)}>
<QuestionItemTitle>{question}</QuestionItemTitle>
{open ? <Close /> : <Add />}
</QuestionItem>
<ReadQuestion>
{open && (
<ReadQuestionDetail>
<ReadQuestionDesc>{answer}</ReadQuestionDesc>
</ReadQuestionDetail>
)}
</ReadQuestion>
</div>
);
}
const FAQ = () => {
const [data, setData] = useState(faqData);
return (
<FAQSection>
<FAQTitle>Frequently Asked Questions</FAQTitle>
<Questions>
<QuestionItemDetail>
{data.map((item) => {
const { id, question, answer } = item;
return (
<MyItemComponent id={id} question={question} answer={answer} />
);
})}
</QuestionItemDetail>
</Questions>
</FAQSection>
);
};
This will give you an individual open state for each item.

HTML elements not rendering on Gatsby website

What I am doing:
Creating my first website in Gatsby.js. Trying to render an HTML element "onClick" of a navigation link. When someone clicks one of the links, I want it to show a dropdown menu.
export function DropDownMenu(props) {
return (
<p>{props}</p>
)
}
const Header = () => {
// const [open, setOpen] = useState(false);
return (
<Nav>
<StyledLinkBox to="/"><Logo/></StyledLinkBox>
<Bars />
<NavMenu>
{headerMenuData.map((item, index, dropdown) => (
<NavLink to={item.link} key={index} onClick={() => {
item.dropdown.map((item, index) => (
<DropDownMenu props={item} key={index}/>
))}
}>
{item.title}
</NavLink>
))}
<StyledButton>Early Access</StyledButton>
</NavMenu>
</Nav>
)
}
Notes:
I have tried to use useState to call another function here, but that doesn't seem to work, as then you have to click twice for anything to happen.
If you replace <DropDownMenu...> within the map function with a console.log, it will print out all the elements that need to appear, which is strange.
So if it can do that, and the mapping function is working correctly, why can't I see <p>{props}</p> for every item?
Use:
export function DropDownMenu({props}) {
return (
<p>{props}</p> /* same as props.props */
)
}
const Header = () => {
// const [open, setOpen] = useState(false);
return (
<Nav>
<StyledLinkBox to="/"><Logo/></StyledLinkBox>
<Bars />
<NavMenu>
{headerMenuData.map((item, index, dropdown) => (
<NavLink to={item.link} key={index} onClick={() => {
item.dropdown.map((item, index) => (
<DropDownMenu props={item} key={index}/>
))}
}>
{item.title}
</NavLink>
))}
<StyledButton>Early Access</StyledButton>
</NavMenu>
</Nav>
)
}
You are sending item as props and the component is also, by default, is getting props (if you send it), so you need to restructure it or access the data by props.props.
You can do:
<DropDownMenu item={item} key={index}/>
And:
export function DropDownMenu({item}) {
return (
<p>{item}</p> /* same as props.item */
)
}
For a more succinct approach.

How to pass data from mapped objects to its parent component in React?

I'm building a shopping cart app, the app includes each item as a card component. I rendered these cards by mapping some dummy object data like this:
const Home = () => {
const dummyData = [
{
id: 1,
title: 'tshirt',
price: 10
},
{
id: 2,
title: 'coat',
price: 20
}
]
const RenderCards = () => {
return (
dummyData.map(
(d) => {
return (
<Card key={d.id} title={d.title} price={d.price} handleAddToCart={handleAddToCart}/>
)
}
)
)
}
const handleAddToCart = () => {
// maybe access id and price here?
}
return (
<>
<RenderCards />
</>
)
}
and the Card component being rendered:
const Card = ({id, title, price}) => {
return (
<>
<div key={id}>
<p>{title}</>
<p>{price}</>
<button onClick={handleAddToCart}>Add to cart</button>
</div>
</>
)
}
Now on click of the button in each card, I'd like to send the data (the id of the card and the price of the item) back to the parent Home component. Say 2nd card is clicked, I want to have access to id and price in Home.
EDIT:
Maybe I didn't make myself clear, I'd like to access the clicked card's price and id in handleAddToCart function.
You can either pass the handler down and have the child pass the details to it, like this:
items.map(item => <Item addToCart={addToCart} {...item} />)
const Item = ({ id, name, addToCart }) =>
<div>
{name}
<button onClick={() => addToCart(id)}>Add to Cart</button>
</div>
Or pass down a values-included callback like this:
items.map(item => <Item addToCart={() => handleAddToCart(item.id)} {...item} />)
const Item = ({ id, name, addToCart }) =>
<div>
{name}
<button onClick={addToCart}>Add to Cart</button>
</div>
In <Home /> component, first you can introduce a new state with useState as:
const [selectedItem, setSelectedItem] = useState(null)
Then pass down through props the setSelectedItem in order to be able to trigger there:
<Card key={d.id} title={d.title} price={d.price} handleAddToCart={handleAddToCart}
setSelectedItem={setSelectedItem} />
Then in <Card /> component use as:
const Card = ({id, title, price, setSelectedItem}) => {
return (
<>
<div key={id}>
<p>{title}</>
<p>{price}</>
<button onClick={() => {
handleAddToCart();
setSelectedItem({ id, title, price});
}}>Add to cart</button>
</div>
</>
)
}
+1 suggestion:
I would pass down to <Card /> component the details in one attribute as:
<Card key={d.id}
data={d}
handleAddToCart={handleAddToCart}
setSelectedItem={setSelectedItem} />
Then destructure inside as:
const Card = (props) => {
const { data, setSelectedItem, handleAddToCart } = props
const { id, title, price } = data
// more code
}

Trying to update child component 1 with data from child component 2 without rerender child 2

I'm generating a heavy JSX array from a loop.
It creates a lot of table.
I would like to update a Badge with the data on a row selected. But it rerender all my tables. It's pretty long for updating a single badge.
I tried to use useMemo() to prevent the creation of the table if my data doesn't change, but the callback fonction from the parent does not update state.
A code example what i'm trying to do =>
function App() {
const [tableData, setTableData] = useState(null)
const [badgeData, setBadgeData] = useState(null)
const jsx = useMemo(() => createTable(tableData), [tableData])
function updateBadge(selectedRows) {
setBadgeData(addNewRow(selectedRows))
}
function createTable(data) {
let jsx = []
data.forEach((item) => {
jsx.push(<TableComponent data={data.var} onRowSelect={updateBadge}/>)
})
return jsx;
}
return (
<div>
<HandleDataGeneration setData={setTableData}/>
<MyBadgeComponent data={badgeData}/>
{jsx}
</div>
);
}
In this case, only the first call to updateBadge function rerender the parent, but not the nexts calls (i guess it's because i don't send the new props and the function is copied and not linked)
Maybe my architecture is bad, or maybe there is some solution for update this badgeComponent without rerender all my Tables. Thanks you for your help
EDIT:
TableComponent
const TableCompoennt = React.memo((props) => { // after edit but it was a classic fn
const classes = useStyles();
const [expanded, setExpanded] = useState(props.data ? `panel${props.i}` : false);
let disabled = false;
const handleChange = (panel) => (event, isExpanded) => {
setExpanded(isExpanded ? panel : false);
};
if (isNaN(props.data.var)) {
props.data.var = x
}
if (!props.data)
disabled = true;
return (
<ExpansionPanel TransitionProps={{ unmountOnExit: false }} expanded={expanded === `panel${props.i}`} onChange={handleChange(`panel${props.i}`)} disabled={disabled}>
<ExpansionPanelSummary
expandIcon={<ExpandMoreIcon/>}
aria-controls="panel1bh-content"
id="panel1bh-header"
>
<Tooltip title={props.data.var}>
<Typography className={classes.heading}>{props.data.var}-{props.data.var}</Typography>
</Tooltip>
{!disabled ?
<Typography
className={classes.secondaryHeading}>{expanded ? "click to hide data" : "click to display data"}</Typography> :
<Typography
className={classes.secondaryHeading}>no data</Typography>
}
</ExpansionPanelSummary>
<ExpansionPanelDetails>
<MyTable data={props.data} var={props.var}
key={props.i} id={props.i} style={{width: "100%"}} updateBadge={props.updateBadge}/>
</ExpansionPanelDetails>
</ExpansionPanel>
)
})
MyTable
export default React.memo((props) => { // same here
const [open, setOpen] = useState(false);
const [rowData, setRowData] = useState(null);
const [rows, setRows] = useState(props.myRates);
calcTotal(rows);
useEffect(() => {
setRows(props.myRates)
}, [props]);
return (
<div style={{width: "100%"}}>
{(rows && rows.length) &&
<div style={{width: "100%"}}>
<Modal open={open} rowData={rowData} setRowData={setRowData}
setOpen={(value) => setOpen(value)}/>
<Paper style={{height: 400, width: '100%'}}>
<SimpleTable
columns={columns}
rows={rows}
handleRowClick={(row) =>{
setOpen(true);
setRowData(row);
}}
handleSelect={(row) => {
if (!row.selected)
row.selected = false;
row.selected = !row.selected;
props.updateBadge(row)
}}
/>
</Paper>
</div>}
</div>
);
})
SimpleTable
const SimpleTable = React.memo((props) => {
const classes = useStyles();
let dataLabel = generateLabel(props.columns);
function getRowData(row) {
props.handleRowClick(row);
}
return (
<TableContainer component={Paper}>
<Table className={classes.table} aria-label="simple table">
<TableHead>
<TableRow>
{dataLabel}
</TableRow>
</TableHead>
<TableBody>
{props.rows.map((row) => (
<TableRow key={row.unique_code} selected={row.selected} hover onClick={() => {getRowData(row)}}>
{generateRow(props.columns, row)}
<TableCell onClick={(event) => {
event.stopPropagation();
}
}>
<Checkbox onClick={(event) => {
event.stopPropagation();
props.handleSelect(row);
}}
/>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
);
})
Instead of using useMemo inside the App Component, you must make use of React.memo for the TableComponent assuming its a functional component or extend it with React.PureComponent if its a class component
However in such a case, you must make sure that you are not recreating the updateBadge Function on each re-render. To make sure of that, use useCallback hook
Also don't forget to add a unique key to TableComponent's instances which are rendered from the loop
function App() {
const [tableData, setTableData] = useState(null)
const [badgeData, setBadgeData] = useState(null)
const updateBadge = useCallback((selectedRows) {
setBadgeData(addNewRow(selectedRows))
}, []);
return (
<div>
<HandleDataGeneration setData={setTableData}/>
<MyBadgeComponent data={badgeData}/>
{data.map((item) => {
return <TableComponent key={item.id} props={item} onRowSelect={updateBadge}/>)
})}
</div>
);
}
and in TableComponent
const TableComponent = React.memo((props) => {
// Rest of the TableComponent code
})
EDIT:
Adding a working demo based on your comment codesandbox: https://codesandbox.io/s/clever-fast-0cq32
Few changes
made of use of useCallback method and also converted state updater to use callback approach
Removed the logic for JSON.parse and JSON.stringify and instead stored the entire data in array. The reason for this is that everytime you use JSON.parse it returns you a new object reference and hence memo functionality fails in child since it just does a reference check. This will happen each time your component re-renders i.e on update of badge state.
if you still need to use JSON.parse and JSON.stringify, add a custom comparator that compares the data values deeply

Categories

Resources