I have a list of items and for each item in the list has an edit button to show a modal with that items details. Originally I had a single modal component in the parent and when I click the edit button it would pass the visible values up to the parent state to show the modal.
The problem is when I did that, the entire list would re render which I dont want because the list can have hundreds of items in it. So right now the solution I have is that in each item of the list I have a modal associated with it. It works but it doesnt seem right because I am duplicated code unnecessarily.
The code is too large to put on here but these are the relevant parts:
import Modal from '../Modal';
const CustomCard = ({
...omitted
}) => {
const [editCustomerModal, setEditCustomerModal] = useState(false);
const onEditModal = () => {
setEditCustomerModal(true);
};
return (
<>
<CustomerModal
onSuccess={handleUpdateCustomer}
onCancel={handleCancelModal}
visible={editCustomerModal}
title="Edit Customer"
details={{
.. ommitted
}}
/>
<Card />
....data
</Card>
</>
);
};
export default CustomCard;
import CustomCard from '../CustomerCard/index';
const CustomList = ({ dataSource }) => {
return (
<div>
{dataSource?.map(i => (
<CustomCard
...props ommitted
/>
))}
</div>
);
};
export default CustomerList;
import CustomerList from './components/CustomerList/index';
// import Modal from './components/AddCustomerModal';
const CustomersPage = () => {
const [editCustomerModal, setEditCustomerModal] = useState(false);
const [editCustomer, setEditCustomer] = useState(null);
return (
<>
// This is where I would want it ideally
<Modal
onSuccess={handleSaveCustomerEdit}
onCancel={handleCancelCustomerEdit}
visible={editCustomerModal}
details={editCustomer}
/>
<CustomerList
dataSource={data}
/>
</div>
{/* </ModalContext.Provider> */}
</>
);
};
export default CustomersPage;
You can define one selected useState atribute, that stores only the register that you want to pass to your modal.
For example:
import CustomerList from './components/CustomerList/index';
import Modal from './components/AddCustomerModal';
const CustomersPage = () => {
const [editCustomerModal, setEditCustomerModal] = useState(false);
const [editCustomer, setEditCustomer] = useState(null);
const [selected, setSelected] = useState(null);
return (
<>
<Modal
onSuccess={handleSaveCustomerEdit}
onCancel={handleCancelCustomerEdit}
visible={editCustomerModal}
details={editCustomer}
selectedCard={selected}
/>
{dataSource?.map(i => (
<CustomCard ...props ommitted setSelected={setSelected}/>
//somewere in CustomCard, trigger the event to set selected props that you want to pass for Modal
//remember to be careful with nullable object. use selectedCard?.something
))}
</>
);
};
export default CustomersPage;
Related
I have a parent component with a handler function:
const folderRef = useRef();
const handleCollapseAllFolders = () => {
folderRef.current.handleCloseAllFolders();
};
In the parent, I'm rendering multiple items (folders):
{folders &&
folders.map(folder => (
<CollapsableFolderListItem
key={folder.id}
name={folder.name}
content={folder.content}
id={folder.id}
ref={folderRef}
/>
))}
In the child component I'm using the useImperativeHandle hook to be able to access the child function in the parent:
const [isFolderOpen, setIsFolderOpen] = useState(false);
// Collapse all
useImperativeHandle(ref, () => ({
handleCloseAllFolders: () => setIsFolderOpen(false),
}));
The problem is, when clicking the button in the parent, it only collapses the last opened folder and not all of them.
Clicking this:
<IconButton
onClick={handleCollapseAllFolders}
>
<UnfoldLessIcon />
</IconButton>
Only collapses the last opened folder.
When clicking the button, I want to set the state of ALL opened folders to false not just the last opened one.
Any way to solve this problem?
You could create a "multi-ref" - ref object that stores an array of every rendered Folder component. Then, just iterate over every element and call the closing function.
export default function App() {
const ref = useRef([]);
const content = data.map(({ id }, idx) => (
<Folder key={id} ref={(el) => (ref.current[idx] = el)} />
));
return (
<div className="App">
<button
onClick={() => {
ref.current.forEach((el) => el.handleClose());
}}
>
Close all
</button>
{content}
</div>
);
}
Codesandbox: https://codesandbox.io/s/magical-cray-9ylred?file=/src/App.js
For each map you generate new object, they do not seem to share state. Try using context
You are only updating the state in one child component. You need to lift up the state.
Additionally, using the useImperativeHandle hook is a bit unnecessary here. Instead, you can simply pass a handler function to the child component.
In the parent:
const [isAllOpen, setAllOpen] = useState(false);
return (
// ...
{folders &&
folders.map(folder => (
<CollapsableFolderListItem
key={folder.id}
isOpen={isAllOpen}
toggleAll={setAllOpen(!isAllOpen)}
// ...
/>
))}
)
In the child component:
const Child = ({ isOpen, toggleAll }) => {
const [isFolderOpen, setIsFolderOpen] = useState(false);
useEffect(() => {
setIsFolderOpen(isOpen);
}, [isOpen]);
return (
// ...
<IconButton
onClick={toggleAll}
>
<UnfoldLessIcon />
</IconButton>
)
}
I'm new to React. I'm trying to add additional functionality of deleting the record from the list by setting the value.
here is my App.js
import React, { useState } from "react";
import data from "./data";
import List from "./List";
function App() {
const [movies, setMovie] = useState(data);
return (
<main>
<section className='container'>
<h3>{movies.length} Movies to Watch</h3>
<List movies={movies} setMovie />
<button onClick={() => setMovie([])}>clear all</button>
</section>
</main>
);
}
export default App;
In List.js, Im trying to delete the record when clicking on Watched button. Can I call setMovie inside the List component? is it a correct way?
List.js
import React from "react";
const List = ({ movies }, setMovie) => {
return (
<>
{movies.map((movie) => {
const { id, name, year, image } = movie;
return (
<article key={id} className='person'>
<img src={image} alt={name} />
<div>
<h4>{name}</h4>
<button
className='btn'
onClick={(id) =>
setMovie(movies.filter((movie) => movie.id !== id))
}
>
watched
</button>
<p>{year}</p>
</div>
</article>
);
})}
</>
);
};
export default List;
You have two mistakes in your code. First:
<List movies={movies} setMovie />
This shorthand assigns a value of true to setMovie. To assign the setMovie function to it, you must instead do:
<List movies={movies} setMovie={setMovie} />
And secondly this:
const List = ({ movies }, setMovie) => {
Should be this:
const List = ({ movies, setMovie }) => {
try:
<List movies={movies} setMovie={setMovie} />
this way the funcition will appear in the List component as a prop.
The way you were doing, it will just appear as true
I am using props from a parent component in a modal and I am unsure of how to make it show in a select dropdown dynamically. I see that the stubbed hardcoded 'Agent' value is being repeated but how do I actually make that dynamically show the values from the props I am receiving? What am I missing here?
React Modal Component
import React, { useState, useEffect } from 'react'
import Button from '#/components/Button.jsx'
import Loading from '#/components/Loading.jsx'
import Modal from '#/components/Modal.jsx'
/**
* Handles displaying the capabilities modal for creating new capabilities or updating exiting ones.
* #component
*/
export default function AddCapabilitiesModal({ profile, onClose }) {
const [error, setError] = useState(null)
const [loading, setLoading] = useState(false)
const [agents, setAgents] = useState([])
const [profiles, setProfiles] = useState([])
useEffect(() => {
if (profile) {
console.log(profile);
setAgents(profile.map((f) => ({ label: f.agent, value: f.agent })))
setLoading(false);
}
}, [])
/**
* Erases information of text field variables and closes modal.
*/
const closeModal = () => {
setError(false)
setLoading(false)
onClose()
}
const dummyClick = () => {
console.log('click')
}
return (
<div>
<Modal>
{error && (
<label className='high'>
Error: Please confirm inputs and try again.
</label>
)}
<div className='modal-title'>
<label>EDIT STATUS</label>
</div>
<br />
<Loading text='Saving Test...' visible={loading} />
{!loading && (
<div id='addCapSelectInput' className='sub-filters'>
<div className='lrow filter-options group light-grey-bg'>
<select>
{agents.map((agent) => (
<option key={agent} value={agent}>
Agent
</option>
))}
</select>
</div>
</div>
)}
<div className='modal-row top-horizontal-divider'>
<Button onClick={closeModal} className='modal-cancel'>
Cancel
</Button>
<Button
onClick={dummyClick}
className='modal-submit'
>
Submit
</Button>
</div>
</Modal>
</div>
)
}
As I can see your agent in state is a list of objects containing label and value keys. So you should just rewrite render part to something like this:
{
agents.map((agent) => (
<option key={agent.label} value={agent.value}>
{agent.label}
</option>
))
}
This is the function where I am passing the onClick prop (setShowModal is setState() from the useState hook):
<MyFunctionalComponent
onClick={() => setShowModal(true)}
...other props here
/>
This is the functional component that receives the prop:
export const MyFunctionalComponent = ({ onClick }) => {
return (
<section>
...other code here
{onClick && (<Button>{ctaText}</Button>)}
</section>
);
};
But the Button component never appears, because the prop onClick is undefined. When I console.log the prop inside the functional component, it initially prints the function in the console, but then prints two more times as undefined. Could someone explain why that would be? I got it to work by spreading ...props instead. But the console.log remains the same? I don't understand why. This is my first question on Stack Overflow, so feel free to give me feedback on how to ask better questions :)
The reason why you are receiving an 'undefined' response is because as #Zrogua mentioned, onClick is an event listener function rather than a persistent value (like state you define).
import React from "react";
const YourButton = ({ onClick }) => {
console.log(onClick);
return <section>{onClick && <button>here</button>}</section>;
};
const ParentDiv = () => {
return (
<div>
<h1>Button Props</h1>
<h2>Start editing to see some magic happen!</h2>
<YourButton onClick={() => console.log("CLICK")} />
</div>
);
};
export default ParentDiv;
Result of console.log():
function onClick() // index.js:27:25
The reason why this is because props are read-only. From the React Docs:
Whether you declare a component as a function or a class, it must never modify its own props ... Such functions are called “pure” because they do not attempt to change their inputs, and always return the same result for the same inputs.
Therefore your button will only show if the onClick function is defined. For example, if you did not give onClick a function or value, the button will not appear:
import React, { useState } from "react";
const YourButton = ({ onClick }) => {
console.log(onClick);
return (
<section>
{onClick && <button>This button is shown if a button is defined.</button>}
</section>
);
};
const ParentDiv = () => {
return (
<div>
<h1>Button Props</h1>
<YourButton onClick={() => console.log("CLICK")} />
<YourButton /> {/* You won't see this button because the function is not defined. */}
</div>
);
};
export default ParentDiv;
The button appears because the prop has a value that is not undefined (your onClick function), and because it is read-only, you cannot access that function in your child component.
Instead, (1) define the modal state in the parent component and (2) pass the state through props to the button like so:
import React, { useState } from "react";
const YourButton = ({ onClick }) => {
console.log(onClick);
return (
<section>
{onClick && <button>This button is shown if a button is defined.</button>}
</section>
);
};
const AltButton = ({ modal }) => {
return (
<section>
{modal && (
<button>This button is shown the modal state is passed.</button>
)}
</section>
);
};
const ParentDiv = () => {
const [modal, setModal] = useState(false);
return (
<div>
<h1>Button Props</h1>
<YourButton onClick={() => console.log("CLICK")} />
<YourButton />{" "}
{/* You won't see this button because the function is not defined. */}
<section>
<button onClick={() => setModal(!modal)}>OPEN MODAL</button>
</section>
{modal && <p>this is dependent on state</p>}
<AltButton modal={modal} />
</div>
);
};
export default ParentDiv;
Working CodeSandbox: https://codesandbox.io/s/stack-66715327-passingfunctions-92pzr
Finally, if I am reading between the lines and understanding correctly that you are looking to hide a button when a modal is open, here is a little modal wrapper trick I use for buttons that open modals: https://codesandbox.io/s/stack-66715327-modalwrapper-wvl54
You can't pass onClick, onClick is just an event listener. You should pass the state
<MyFunctionalComponent onClick={() => setShowModal(!showModal)}
showModal={showModal}
...other props here />
/>
export const MyFunctionalComponent = ({ showModal }) => {
return (
<section>
...other code here
{showModal && (<Button>{ctaText}</Button>)}
</section>
);
};
I believe this should work. Let me know if this is what you were looking for.
I think that rather then passing callback you should pass variable which decide if component should show or not. Check this example.
export const MyFunctionalComponent = ({ isShow, onClick }) => {
return (
<section>
...other code here
{isShow && <div>something</div>}
</section>
);
};
export default function App() {
const [showModal, setShowModal] = useState(false);
return (
<MyFunctionalComponent
isShow={showModal}
onClick={() => setShowModal(true)}
/>
);
}
I also suppose that you can make mistake and have something other on mind .. like this:
<section>
...other code here
<button onClick={ onClick }>something</button>}
</section>
I have a Dropdown made with React Hooks. The button should have Arrow, which rotate. My first Version works fine:
const DropdownMenu= (props) => {
const [open, setOpen] = useState(false);
const openDropdown = (): void => setOpen(prevState => !prevState);
return (
<div>
<Button
onClick={openDropdown}
dropdownIsOpen={open}
>
Text </Button>
<DropdownContent isOpen={isOpen} />
</div>
);
};
export default DropdownMenu;
const Button = (props) => {
return (
<Button Click={props.onClick}>
Text
<Arrow rotate={props.dropdownIsOpen} color={designTheme.color.primary} />
</Button>
);
};
export default Button;
But now I have multiple Dropdowns and want to use them with the same component. I gave the toggleNode as prop. Now the Arrow rotate but without the transition:
const DropdownButton = (props) => {
const [isOpen, setOpen] = useState(false);
const onToggle = (): void => setOpen(prevState => !prevState);
return (
<Dropdown
isOpen={props.isOpen}
onToggle={onToggle}
toggleNode={
<Button dropdownIsOpen={isOpen}>
Text
</Button>
}
/>
);
};
export default DropdownButton;
Have someboy an idea?
Thank you!
my guess is, you are exporting different Button
How can you use Button inside Button, possibly by importing Button from somewhere else. And then you are again exporting Button. This will be confuse for transpiler, as which Button to be exported.
You might want to rename your custom Button to something else
const Button = (props) => {
return (
<Button Click={props.onClick}>
Text
<Arrow rotate={props.dropdownIsOpen} color={designTheme.color.primary} />
</Button>
);
};
export default Button;
Given your Button component takes an onClick prop:
const Button = (props) => {
return (
<Button Click={props.onClick}>
Text
<Arrow rotate={props.dropdownIsOpen} color={designTheme.color.primary} />
</Button>
);
};
NOTE: As #SagarMore points out, there my also be a naming collision between some imported Button component and your Button component.
You may just need to pass a callback to Click (terrible name, BTW, should be onClick, hopefully it was just a typo):
const DropdownButton = (props) => {
const [isOpen, setOpen] = useState(false);
const onToggle = (): void => setOpen(prevState => !prevState);
return (
<Dropdown
isOpen={props.isOpen}
onToggle={onToggle}
toggleNode={
<Button onClick={onToggle} dropdownIsOpen={isOpen}>
Text
</Button>
}
/>
);
};
Passing onToggle to the inner button's onClick handler should now toggle the isOpen state of the DropDown.
This was my fault.
In my Dropdown component I render two different states conditional. So it renders the start or the end state and don't use the animation. I have to render a animation and don't render new when I click so the animation works.