How do I pass a state attribute from parent to child? In the following implementation, the Dropdown component has a state "isActive" and I want to access it in the Button component to attach propper styling to it. The Dropdown has to generic as it is supposed to take different sorts of buttons.
<Dropdown items="...">
<Button active ="false" />
</Dropdown>
Dropdwon.js
...
constructor(props){
super(props)
this.state = {
isActive: true,
}
}
render (){
return (
<div className={styles.toggle} onClick={(event) => this.showMenu(event)}>
{this.props.children} /* want to set active prop for the child button here */
</div>
);
}
...
You have two possibilities:
Lift your Dropdown state and keep it in its parent component;
Use useContext hook;
The first approach would be better, but it may not be good for your application (I cannot know that). Let me make an example for both cases.
This is an example where I've lifted the isActive state to the parent component.
const ParentComponent = () => {
const [isActive, setIsActive] = useState(false);
handleIsActiveChange = (newValue) => {
setIsActive(newValue);
}
<Dropdown isActive={isActive} setIsActive={handleIsActiveChange}>
<Button isActive={isActive} />
</Dropdown>
}
const Dropdown = props => {
// You can use `props.isActive` to know whether the dropdown is active or not.
// You can use `props.handleIsActiveChange` to update the `isActive` state.
}
const Button = props => {
// You can use `props.isActive` to know whether the dropdown is active or not.
}
Instead, this exploits the useContext API:
const dropdownContext = React.createContext(null);
const Dropdown = props => {
const [isActive, setIsActive] = useState(false);
return (
<dropdownContext.Provider value={{ isActive }}>
{props.children}
</dropdownContext.Provider>
);
}
const Button = props => {
const dropdownCtx = React.useContext(dropdownContext);
// You can use `dropdownCtx.isActive` to know whether the dropdown is active or not.
}
Aside from the answer I linked, there might be another way of achieving this that I didn't see mentioned.
You can send a function as a children element of your dropdown which will take isActive as a variable :
<Dropdown items="...">
{isActive => <Button active={isActive} />}
</Dropdown>
Then, is the render function, simply call the function and send your state value as a parameter :
render(){
return (
<div className={styles.toggle} onClick={(event) => this.showMenu(event)}>
{this.props.children(this.state.isActive)}
</div>
);
}
<Dropdown >
<Button isActive={this.state.isActive} />
</Dropdown>
In your button get it with this.props.isActive
Related
I am working on the project in React Typescript.
I have created hierarchy of components as per requirement.
In one scenario I have to pass data from child component to parent component and I am passing function as props and it works.
Issue :
When passing data to parent component child component gets re-render it looks like. Mean to say Dropdown selection is get reset and tree control expanded nodes get collapsed and set to the position as first time rendered.
I have used useState,useEffects hooks.
I have also tried React.memo as a part of my search on internet.
What I need :
I want to pass data to parent component from child without re-render the child component as there is no change in the props of child component.
Try this approach:
Add useCallback hook to memoize your function which lift data to <Parent />.
Then use React.memo for <Child /> to control prop changes and avoid unwanted re-renders.
I prepare an example for you here.
UPD. I have uploaded an example, you can copy it and see how it works!
Here is Child component:
const Child = ({ onChange }) => {
console.log("Child re-render");
return (
<div className="App">
<h1>Child</h1>
<button onClick={() => onChange(Math.random())}>
Lift value to Parant
</button>
</div>
);
};
const areEqual = ({ onChange: prevOnChange }, { onChange }) => {
return prevOnChange === onChange; // if true => this will avoid render
}
export default React.memo(Child, areEqual);
And the Parent:
consn App = () => {
const [value, setValue] = useState("");
const onChange = useCallback((value) => setValue(String(value)), []);
console.log("Parant re-render");
return (
<div className="App">
<h1>Parent</h1>
<div>Value is: {value}</div>
<Child onChange={onChange} />
</div>
);
}
Best regards 🚀
Currently I am facing the problem that I want to change a state of a child component in React as soon as a prop is initialized or changed with a certain value. If I solve this with a simple if-query, then of course I get an infinite loop, since the components are then rendered over and over again.
Component (parent):
function App() {
const [activeSlide, setActiveSlide] = useState(0);
function changeSlide(index) {
setActiveSlide(index);
}
return (
<div className="app">
<div className="app__nav">
<Button icon="FiSun" handler={changeSlide} active={activeSlide} index="0" />
<Button icon="FiSettings" handler={changeSlide} active={activeSlide} index="1" />
</div>
</div>
);
}
Component (child):
function Button(props) {
const Icon = Icons[props.icon];
const [activeClass, setActiveClass] = useState("");
// This attempts an endless loop
if(props.active == props.index) {
setActiveClass("active");
}
function toggleView(e) {
e.preventDefault();
props.handler(props.index);
}
return(
<button className={activeClass} data-index={props.index} onClick={toggleView}>
<Icon />
</button>
)
}
Is there a sensible and simple approach here? My idea would be to write the if-query into the return() and thus generate two different outputs, even though I would actually like to avoid this
The React docs have a nice checklist here used to determine if something does or does not belong in state. Here is the list:
Is it passed in from a parent via props? If so, it probably isn’t state.
Does it remain unchanged over time? If so, it probably isn’t state.
Can you compute it based on any other state or props in your component? If so, it isn’t state.
The active class does not meet that criteria and should instead be computed when needed instead of put in state.
return(
<button className={props.active == props.index ? 'active' : ''} data-index={props.index} onClick={toggleView}>
<Icon />
</button>
)
This is a great use of useEffect.
instead of the if statement you can replace that with;
const {active, index} = props
useEffect(_ => {
if(active == index) {
setActiveClass("active");
}
}, [active])
The last item in the function is a dependency, so useEffect will only run if the active prop has changed.
React automatically re-renders a component when there is a change in the state or props. If you're just using activeClass to manage the className, you can move the condition in the className as like this and get rid of the state.
<button className={props.active === props.index ? 'active' : ''} data-index={props.index} onClick={toggleView}>
<Icon />
</button>
however, if you still want to use state in the child component, you can use the useEffect hook to to update the state in the child component.
Try to use the hook useEffect to prevent the infinite loop. (https://fr.reactjs.org/docs/hooks-effect.html)
Or useCallback hook. (https://fr.reactjs.org/docs/hooks-reference.html#usecallback)
Try this and tell me if it's right for you :
function App() {
const [activeSlide, setActiveSlide] = useState(0);
const changeSlide = useCallback(() => {
setActiveSlide(index);
}, [index]);
return (
<div className="app">
<div className="app__nav">
<Button icon="FiSun" handler={changeSlide} active={activeSlide} index="0" />
<Button icon="FiSettings" handler={changeSlide} active={activeSlide} index="1" />
</div>
</div>
);
}
How to call a Material UI Dialog during onClick on delete icon ie onClick={deletePlayer(id)} ?
I have added the Dialog.js under modal/Dialog and imported to Home component.
I have added a demo here
Short answer: Forked CodeSandbox with working dialog
Long answer:
First of all, you need to move the display/dismiss logic out of the AlertDialog component and into the component that actually triggers the display of the modal (in your case, the Home component). This means that you'll receive the open state and onClose handler as props (along with the playerId which will hold the ID of the player being targeted for deletion). So the signature of your dialog component becomes:
export default function AlertDialog({ open, onClose, playerId }) {
return (
<Dialog open={open} onClose={onClose} ...> ... </Dialog>
);
}
In Home, we add the logic to track and set the state of both the dialog open/closed status, and the ID of the player targeted for deletion. We do this through useState:
const [deleteDialog, setDeleteDialog] = useState(false);
const [playerId, setPlayerId] = useState("");
While you could have as many AlertDialog components as you have players by adding <AlertDialog /> inside your player map loop, it is redundant as you'll only ever have one modal active (by definition). So all you have to do is place a single instance of <AlertDialog /> in your Home component. A good convention is to place it before the closing encompassing tag:
return (
<div className="App">
.
.
.
<AlertDialog
open={deleteDialog}
onClose={() => setDeleteDialog(false)}
playerId={playerId}
/>
</div>
);
Finally, we deal with the handler responsible for displaying the modal, in your case deletePlayer. We have two things to do there: set the player ID targeted for deletion through the playerId state variable, and display the modal through the deleteDialog state variable:
const deletePlayer = id => e => {
setPlayerId(id);
setDeleteDialog(true);
};
Create a state in the Home component to handle the Dialog visibility, set the state on click and render the AlertDialog conditionally:
const [openDialog, setOpenDialog] = useState(false);
...
const deletePlayer = id => e => {
setOpenDialog(true);
};
...
return(
...
{openDialog && (
<AlertDialog isOpen={openDialog} setIsOpen={setOpenDialog} />
)}
Then in the AlertDialog component:
export default function AlertDialog(props) {
const { isOpen, setIsOpen } = props;
const handleClose = () => {
setIsOpen(false);
};
return (
<div>
<Dialog
open={isOpen}
onClose={handleClose}
...
Working Example:
trying to update state variable('visible') via internal function(setVisible) in component. I checked the tutorıal and did same but its not updating after initialization of state.
Sandobx link here.
props.visible is true when user click ShowModal button. but value of visible in function component is still false. (I have checked the content on debugger)
code:
import Modal from '../Helpers/AppModal'
class Streams extends Component {
constructor(props) {
super(props)
this.state = { showModal: false }
}
componentDidMount() {
this.props.getStreams()
}
showDeleteModal = (isShow) =>
{
this.setState({ showModal: isShow });
}
onClickBackdrop = () => {this.setState({ showModal: false });}
render() {
return (
<div>
<button onClick={()=> this.showDeleteModal(true)} className="btn btn-danger">Delete</button>
<Modal visible={this.state.showModal} onClickBackdrop={this.onClickBackdrop} />
</div>
)
}
}
AppModal.js:
const AppModal = (props) => {
const [visible, setVisible] = useState(props.visible)
useEffect(() =>{
setVisible(props.visible)
},[props.visible])
debugger
return (
<Modal visible={visible} fade={true} onClickBackdrop={props.onClickBackdrop}>
<div className="modal-header">
<h5 className="modal-title">{props.title}</h5>
</div>
<div className="modal-body">
{props.body}
</div>
<div className="modal-footer">
<React.Fragment>
<button type="button" className="btn btn-default" onClick={()=>setVisible(false)}>
Close
</button>
</React.Fragment>
</div>
</Modal>
)
}
The argument passed to useStateis just the initial state. Pass a prop to it doesn't mean that the state will be synchronized with props. You can setup an effect to mirror those changes into your local state.
Currently your Modal only see visible from the local state, changing the props value won't cause Modal to change
//Inside child
useEffect(() =>{
setVisible(props.visible)
},[props])
Why should I use props instead of props.visible there?
The dependencies array exists to keep synchronicity, you're telling react:
"Hey, everytime one of those values changes re run this effect."
The problem is that React performs a shallow comparison (Object.is) between old and new props, uppon each render a new props object is generated which is what is triggering your effect in the first place.
React doesn't know how to "react" to nested changes. What is really changing here is props, react doesn't know (and doesn't care) about props.visible, passing it as a dependency is the same as passing []
Actually passing props as dependency is useless, since props changes every render you can omit the dependencies array, which will trigger the effect on each render
useEffect(() => {
setVisible(props.visible)
})
visible is a boolean.
Try changing the way you call setVisible like so:
setVisible(false)
instead of
setVisible({visible:false})
If this is a toggle switch then you should do this:
onClick={() => setVisible(!visible)}
Then it'll toggle on/off correctly.
You might want to set the initial value more explicitly though:
const [visible, setVisible] = useState(false);
I am trying to implement a collapsible component. I have designed it such as, on click of a button, a block of dynamic text will appear. I made a functional component and using the tags in a class. The name of the component is, CustomAccordion.jsx and using this component in Container.jsx
I have tried to create a button and a function for onClick event.
Part of the CustonAccordion.jsx
const handleToggle = () : string =>{
let content = this.nextElementSibling;
if (content.style.maxHeight){
content.style.maxHeight = null;
}else{
content.style.maxHeight = content.scrollHeight +'px';
}
}
export default function CustomAccordion(props: PropType): React.Component<*> {
const { title, children } = props
return(
<div>
<AccordionButton onClick={() => this.handleToggle()}>{title}</AccordionButton>
<AccordionContent>
<p>{children}
</p>
</AccordionContent>
</div>
)
}
Part of calling Container.jsx
<CustomAccordion title = {this.props.name}>
<p>This is the text passed to component.</p>
</CustomAccordion>
<br />
This does not show the expanded text and it seems that the click event does not work properly. I am very new in react, guessing the syntax might be incorrect.
In react you should generally try to avoid touching DOM directly unless you really have to.
Also you are accessing the handleToggle function wrongly. It should be onClick={() => handleToggle()} because this in your case is window/null and so it has no handleToggle method.
Instead you can use a stateful class component to achieve the same thing.
export default class CustomAccordion extends React.Component {
state = {show: false};
toggle = () => this.setState({show: !this.state.show});
render() {
const {title, children} = this.props;
const {show} = this.state;
return (
<div>
<AccordionButton onClick={this.toggle}>{title}</AccordionButton>
{show && (
<AccordionContent>
<p>{children}</p>
</AccordionContent>
)}
</div>
)
}
}
If you want to have some kind of animation, you can set different className based on the show state instead of adding/removing the elements.