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:
Related
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.
So basically I have 2 pieces, the sidebar, then the opener. I'm trying to setup a ref that will connect the sidebar to the current opener. The opener is a functional component, and no matter what I do the current value is null. Am I missing something? I'm just trying to resize a component. My goal is to be able to resize the shown sidebar with the opener.
Here's part of the Render function.
render() {
const { selected, isSidebar, selectedType, search, active } = this.state;
const { pending, callback, resource } = this.props;
const pendingLengh = pending ? pending.length : 0;
const callbackLength = callback ? callback.length : 0;
const isResource = !resource || !Object.keys(resource).length;
return (
<div className="newPatientPage mainPage">
{this.renderMetadata()}
<SubTopBar
title="New Patient Processing"
noLeftSide={true}
subStatus={this.getStatus(pendingLengh, callbackLength)}
isBarcode={!isResource}
sideComponent={this.renderSideComponent()}
/>
{
active ?
<SnapshotSideBar
ref={this.sidebarRef}
patientResource={this.props.patientResource}
isShow={isSidebar}
settup={this.state.settup}
isScan={true}
handleCloseSidebar={this.handleCloseSidebar}
/> :
<NewPatientSideBar
ref={this.sidebarRef}
stepProps={this.state.stepProps}
selected={selected}
isShow={isSidebar}
handleCloseSidebar={this.handleCloseSidebar}
/>
}
<SidebarExtension sidebarToggle={this.toggleSidebar} sidebarReference={this.sidebarRef} sidebarState={isSidebar}/>
Here's the SidebarExtension component
const SidebarExtension = ({
sidebarToggle,
sidebarReference,
sidebarState,
...restProps
}) => {
const [xPos, setXPos] = useState(0);
const [width, setWidth] = useState();
const [openerPosition, setOpenerPosition] = useState(50);
const [isOpen, setIsOpen] = useState(false);
const toggleSidebar = () => {
sidebarToggle();
setIsOpen(!isOpen);
};
useEffect(() => {
setIsOpen(sidebarState);
}, [sidebarState])
if ((!isOpen && !sidebarState)) {
return (
<>
<div
className="resizeHandle"
style={{
right: "0Px",
}}
onClick={toggleSidebar}
>
<LeftCharvenSVG />
</div>
</>
);
}
return (
<>
<div
className="resizeHandle active"
onClick={toggleSidebar}
onMouseDown={startResize}
>
<LeftCharvenSVG />
</div>
</>
);
};
export default SidebarExtension;
Here's what the constructor looks like.
Main Constructor
From the docs https://reactjs.org/docs/forwarding-refs.html you need to wrap your functional component in React.forwardRef()
Example
const FancyButton = React.forwardRef((props, ref) => (
<button ref={ref} className="FancyButton">
{props.children}
</button>
));
// You can now get a ref directly to the DOM button:
const ref = React.createRef();
<FancyButton ref={ref}>Click me!</FancyButton>;
In your case that would be:
const SidebarExtension = React.forwardRef(({
sidebarToggle,
sidebarReference,
sidebarState,
...restProps
}, ref) => {
const [xPos, setXPos] = useState(0);
const [width, setWidth] = useState();
const [openerPosition, setOpenerPosition] = useState(50);
const [isOpen, setIsOpen] = useState(false);
const toggleSidebar = () => {
sidebarToggle();
setIsOpen(!isOpen);
};
useEffect(() => {
setIsOpen(sidebarState);
}, [sidebarState])
if ((!isOpen && !sidebarState)) {
return (
<>
<div
className="resizeHandle"
style={{
right: "0Px",
}}
ref={ref}
onClick={toggleSidebar}
>
<LeftCharvenSVG />
</div>
</>
);
}
return (
<>
<div
className="resizeHandle active"
onClick={toggleSidebar}
onMouseDown={startResize}
>
<LeftCharvenSVG />
</div>
</>
);
});
export default SidebarExtension;
I have a list of Child objects mapped from my Parent component in my React App.
When a Child item is clicked, I need the props.name of that item to be pushed to the selectedItems array in the Parent component via the handleClick function.
How can I achieve this?
function Parent() {
let selectedItems = [];
const result = Data.filter((e) => selectedItems.includes(e.id));
return (
<div className="App">
<main className="products-grid flex flex-wrap">
{Data.map((item, i) => {
return <Child
key={item.id}
name={item.name} />
})}
</main>
</div>
);
}
export default App
const Child = (props) => {
const [clickCount, setClickCount] = useState(0);
function handleClick() {
setClickCount(prevClickCount => prevClickCount + 1);
}
return (
<div
className="product"
onClick={() => handleClick()}
>
<p>{props.name}</p>
<p>{clickCount > 0 ? <p>Selected: {clickCount}</p> : <p>Not Selected</p>}</p>
<img src={props.img} alt="" />
</div>
);
}
I would recommend using hooks for the 'selectedItems' in the parent component, as to trigger a re-render when the name changes.
You can pass functions from the parent to the child component using props.
Below I've passed the 'addToSelectedItems' function down to the child and triggered it in the handleClick method.
const Parent = () => {
const [selectedItems, setSelectedItems] = useState([]);
function addToSelectedItems(name){
setSelectedItems([...selectedItems, name]);
}
return (
<div className="App">
<main className="products-grid flex flex-wrap">
{Data.map((item, i) => {
return <Child
key={item.id}
name={item.name}
addToSelectedItems={addToSelectedItems}
/>
})}
</main>
</div>
);
}
export default App
const Child = (props) => {
const [clickCount, setClickCount] = useState(0);
function handleClick(name) {
setClickCount(prevClickCount => prevClickCount + 1);
props.addToSelectedItems(name);
}
return (
<div
className="product"
onClick={(props.name) => handleClick(props.name)}
>
<p>{props.name}</p>
<p>{clickCount > 0 ? <p>Selected: {clickCount}</p> : <p>Not Selected</p>}</p>
<img src={props.img} alt="" />
</div>
);
}
I'd do something like this, but I have not tested it, it might need some fine tuning
function Parent() {
let selectedItems = [];
const result = Data.filter((e) => selectedItems.includes(e.id));
const [clickCount, setClickCount] = useState(0);
function handleClick(name) {
setClickCount(prevClickCount => prevClickCount + 1);
selectedItem.push(name)
}
return (
<div className="App">
<main className="products-grid flex flex-wrap">
{Data.map((item, i) => {
return <Child
key={item.id}
name={item.name}
clickCount={clickCount}
handleClick={handleClick} />
})}
</main>
</div>
);
}
export default App;
const Child = (props) => {
let { handleClick, name, clickCount, img } = props
return (
<div
className="product"
onClick={() => handleClick(name)}
>
<p>{name}</p>
<p>{clickCount > 0 ? <p>Selected: {clickCount}</p> : <p>Not Selected</p>}</p>
<img src={img} alt="" />
</div>
);
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
On my follow up question from here : How to pass data from child to parent component using react hooks
I have another issue.
Below is the component structure
export const Parent: React.FC<Props> = (props) => {
const [disabled, setDisabled] = React.useState(false);
const createContent = (): JSX.Element => {
return (
<Authorization>
{<ErrorPanel message={errorMessage} setDisabled={setDisabled}/>}
<MyChildComponent/>
</<Authorization>
);
}
return (
<Button onClick={onSubmit} disabled={disabled}>My Button</Button>
{createContent()}
);
};
const Authorization: React.FC<Props> = (props) => {
const [disabled, setDisabled] = React.useState(false);
const render = (errorMessage : JSX.Element): JSX.Element => {
return (
<>
{<ErrorPanel message={errorMessage} setDisabled={setDisabled}/>}
</>
);
};
return (
<>
<PageLoader
queryResult={apiQuery}
renderPage={render}
/>
{props.children}
</>
);
};
How do I pass the disabled state value from Authorization component to my child which is invoked by
{props.children}
I tried React.cloneElement & React.createContext but I'm not able to get the value disabled to the MyChildComponent. I could see the value for disabled as true once the errorMessage is set through the ErrorPanel in the Authorization component.
Do I need to have React.useEffect in the Authorization Component?
What am I missing here?
You need to use React.Children API with React.cloneElement:
const Authorization = ({ children }) => {
const [disabled, setDisabled] = React.useState(false);
const render = (errorMessage) => {
return (
<>{<ErrorPanel message={errorMessage} setDisabled={setDisabled} />}</>
);
};
return (
<>
<PageLoader queryResult={apiQuery} renderPage={render} />
{React.Children.map(children, (child) =>
React.cloneElement(child, { disabled })
)}
</>
);
};
// |
// v
// It will inject `disabled` prop to every component's child:
<>
<ErrorPanel
disabled={disabled}
message={errorMessage}
setDisabled={setDisabled}
/>
<MyChildComponent disabled={disabled} />
</>
You can make use of React.cloneElement to React.Children.map to pass on the disabled prop to the immediate children components
const Authorization: React.FC<Props> = (props) => {
const [disabled, setDisabled] = React.useState(false);
const render = (errorMessage : JSX.Element): JSX.Element => {
return (
<>
{<ErrorPanel message={errorMessage} setDisabled={setDisabled}/>}
</>
);
};
return (
<>
<PageLoader
queryResult={apiQuery}
renderPage={render}
/>
{React.Children.map(props.children, child => {
return React.cloneElement(child, { disabled })
})}
</>
);
};
UPDATE:
Since you wish to update the parent state to, you should store the state and parent and update it there itself, instead of storing the state in child component too.
export const Parent: React.FC<Props> = (props) => {
const [disabled, setDisabled] = React.useState(false);
const createContent = (): JSX.Element => {
return (
<Authorization setDisabled={setDisabled}>
{<ErrorPanel message={errorMessage} disabled={disabled} setDisabled={setDisabled}/>}
<MyChildComponent disabled={disabled}/>
</<Authorization>
);
}
return (
<Button onClick={onSubmit} disabled={disabled}>My Button</Button>
{createContent()}
);
};
const Authorization: React.FC<Props> = (props) => {
const render = (errorMessage : JSX.Element): JSX.Element => {
return (
<>
{<ErrorPanel message={errorMessage} disabled={props.disabled} setDisabled={props.setDisabled}/>}
</>
);
};
return (
<>
<PageLoader
queryResult={apiQuery}
renderPage={render}
/>
{props.children}
</>
);
};
I have a problem with my application. The component ExpansionPanel of Material-ui is opening more than one Panel. I need to open just one.
father.js
<div>
{props.listContratos.map((item, index) => {
if (item.ativo) {
return (
<PartesCardCollapse
listPartes={props.listPartes}
item={item}
key={index}
thunks={props.thunks}
hideSnackbar={props.hideSnackbar}
/>
);
}
})}
</div>
son.js
export default function PartesCardCollapse(props) {
const classes = useStyles();
const [expanded, setExpanded] = React.useState(false);
const handleChange = (expanded, panel) => (event, isExpanded) => {
setExpanded(isExpanded ? panel : false);
};
return (
<div className={classes.root} onClick={getListPartes}>
<ExpansionPanel
expanded={expanded === props.item.id}
onChange={handleChange(expanded, props.item.id)}
>