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.
Related
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>
))}
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
See that loop loading two components. I would like to display only <Image /> by default, but when I click this element, I want it to turn into <YouTube /> (only the one I press, the others are still <Image />). I can do this on a class component, but I want to use a hook
export const MusicVideos = () => {
const [selectedVideo, setVideo] = useState(0);
return (
<Wrapper>
{videos.map(video => (
<div key={video.id}>
<Image src={video.image} hover={video.hover} alt="thumbnail" />
<YouTube link={video.link} />
</div>
))}
</Wrapper/>
);
};
you can bind onClick for your image and setVideo to video.id and compare with video.id to render image or video.
export const MusicVideos = () => {
const [selectedVideo, setVideo] = useState(0);
return (
<Wrapper>
{videos.map(video => (
{selectedVideo !== video.id ?
<Image onClick={() => setVideo(video.id) src={video.image} hover={video.hover} alt="thumbnail" /> :
<YouTube link={video.link} />
))}
</Wrapper/>
);
};
Create a component like this and pass it to the loop;
const YouTubeToggle = (video) => {
const [selectedVideo, setVideo] = useState(0);
return (
<div key={video.id}>
{selectedVideo == 0 &&
<Image src={video.image} onClick={() => setVideo(!selectedVideo)} hover={video.hover} alt="thumbnail" />
}
{selectedVideo != 0 &&
<YouTube link={video.link} />
}
</div>
);
}
export const MusicVideos = () => {
const [selectedVideo, setVideo] = useState(0);
return (
<Wrapper>
{videos.map(video => (
<YouTubeToggle video={video} />
))}
</Wrapper/>
);
};
There a two components one is <Form /> and other is <LoginForm />.
<LoginForm /> looks like
const LoginForm = () => {
return (
<Form
inputs={[
//some objects here
]}
onSubmit={(data, setError) => {
setError('some error')
}}
/>
);
};
The <Form /> component looks like.
const Form = ({onSumbit, inputs}) => {
const [error, setError] = useState('');
return (
<>
{error ? <div>{error}</div> : null}
{//rendering inputs here}
<button onClick={() => onSubmit('some data which is not relative to problem', setError)}>
</>
)
}
Now when the button is clicked on onSubmit() should run. And it should call setError which should show some error but its not showing any error. Its also not showing any kind of error.
Note: This is only the relevant part of code. Code is actually large. But I am sure that only this part have some basic flaw.
Sometimes you declared onSumbit and sometimes onSubmit, you need to be consistent:
const LoginForm = () => {
return (
<Form
onSubmit={(data, setter) => {
setter('some error');
}}
/>
);
};
const Form = ({ onSubmit, inputs }) => {
const [error, setError] = useState('');
return (
<>
{error ? <div>{error}</div> : null}
<button
onClick={() => {
onSubmit('some data which is not relative to problem', setError);
}}
>
Submit
</button>
</>
);
};
Here is my code. It works fine. Maybe there is some typo in your code.
const Form = ({onSubmit, inputs}) => {
const [error, setError] = useState('');
return (
<>
{error ? <div>{error}</div> : null}
<button onClick={() => onSubmit('some data which is not relative to problem', setError)}>
Hi
</button>
</>
)
}
const LoginForm = () => {
return (
<Form
inputs={[
//some objects here
]}
onSubmit={(data, setError) => {
setError('some error')
}}
/>
);
};
You had typos and syntax errors previously so this couldn't have worked for you anyway.
This seems to do what you meant.
const Form = ({ onSubmit, inputs }) => {
const [error, setError] = useState('');
return (
<>
{error ? <div>{error}</div> : null}
{// rendering inputs here
}
<button onClick={() => onSubmit('data', setError)} />
</>
);
};
const ListView = () => {
return(
<ul>
<ListItem modal={<Modal />} />
</ul>
)
};
const ListItem = (props) => {
const [visible, setVisible] = useState(false);
const toggle = () => setVisible(!visible)
return (
<>
<li>
ListItem
</li>
<ModalWrapper toggle={toggle}>{props.modal}</ModalWrapper>
</>
)
}
const ModalWrapper = (props) => {
if(!props.visible) return null;
return (
<>
{props.children}
</>
)
}
const Modal = ({ toggle }) => {
/* I would like to use toggle() here. */
return (
<>
<div onClick={toggle} className="dimmer"></div>
<div className="modal">modal</div>
</>
)
}
I have a function toggle() in <ListItem /> as shown above.
I am struggling to use toggle() in <Modal />.
Is it possible or are there any suggestions?
You need to inject toggle to ModalWrapper children, be careful not to override toggle prop on Modal after it.
const ModalWrapper = ({ children, visible, toggle }) => {
const injected = React.Children.map(children, child =>
React.cloneElement(child, { toggle })
);
return <>{visible && injected}</>;
};
Refer to React.cloneElement and React.Children.map.
Demo: