How to call multiple instances of the same child component's function from parent - javascript

I know that using ref's I can call a function in a child component from the parent component. But my problem is I'm having multiple instances of the child component and I need to invoke the same function in all the children components.
// Get a hook function
const {useState, useRef, useImperativeHandle, useEffect} = React;
const Parent = ({title}) => {
const childRef1 = useRef();
const childRef2 = useRef();
const childRef3 = useRef();
const childRef4 = useRef();
const childRef5 = useRef();
const handleClick = () => {
childRef1.current.increment()
childRef2.current.increment()
childRef3.current.increment()
childRef4.current.increment()
childRef5.current.increment()
}
return (
<div>
<p>{title}</p>
<Child num={1} childRef={childRef1} />
<Child num={2} childRef={childRef2} />
<Child num={3} childRef={childRef3} />
<Child num={4} childRef={childRef4} />
<Child num={5} childRef={childRef5} />
<button onClick={() => handleClick()}>
Click me
</button>
</div>
);
};
const Child = ({num, childRef}) => {
const [count, setCount] = useState(0)
useImperativeHandle(childRef, () => ({
increment() {
// if(textEditable)
setCount(count+1)
},
}));
useEffect(() => {
setCount(num)
}, [num])
return (
<div>
{
count
}
</div>
)
}
// Render it
ReactDOM.render(
<Parent title="Example using Hooks:" />,
document.getElementById("react")
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="react"></div>
My question is how should I do the same if I'm rendering the child components inside a map function ?

I know that using ref's I can call a function...
Perfect! In that case, we will use the same procedure, but simply expand it a bit further.
Set either the component or the state to hold the child components...
this.state.children = [];
Then map() them like so...
var children = [1,2,3,4,5,6];
children.map((child) => {
return (
<Child
num={child}
ref={(instance) => {this.state.children[child] = instance}}
/>
);
});
Then call the functions on with a forEach() on them...
Object.keys(this.state.children).forEach((child) => {
var childinstance = this.state.children[child];
childinstance.specialFunction();
});

I did it with hooks like below.
// Get a hook function
const {useState, useRef, useImperativeHandle, useEffect} = React;
const Parent = ({title}) => {
const [count, setCount] = useState(0);
const [children, setChildren] = useState(5);
const [childRefs, setChildRefs] = useState([])
const handleClick = () => childRefs.forEach(c => c.current.increment())
useEffect(() => {
setChildRefs([...Array(children).keys()].map(e => React.createRef()))
}, [title])
return (
<div>
<p>{title}</p>
{
[...Array(children).keys()].map(e => <Child
num={e}
childRef={ childRefs[e] }
/>)
}
<button onClick={() => handleClick()}>
Click me
</button>
</div>
);
};
const Child = ({num, childRef}) => {
const [count, setCount] = useState(0)
useImperativeHandle(childRef, () => ({
increment() {
// if(textEditable)
setCount(count+1)
},
}));
useEffect(() => {
setCount(num)
}, [num])
return (
<div>
{
count
}
</div>
)
}
// Render it
ReactDOM.render(
<Parent title="Example using Hooks:" />,
document.getElementById("react")
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="react"></div>

Track the state in the parent and pass it to the child as a prop:
const Parent = ({title}) => {
const [count, setCount] = useState(0);
return (
<div>
<p>{title}</p>
<Child count={count} />
<Child count={count} />
<Child count={count} />
</div>
)
}
If you need them to be independent, use an array for the state:
const Parent = ({title}) => {
const [count, setCount] = useState([0,1,42,96]);
const onIncrement = index => {
// probably a better way to do this. just here for illustrative purposes
const newCount = [...count];
newCount.splice(index, 1, count[index] + 1);
setCount(newCount);
}
const incrementAll = () => setCount(count.map(x => x + 1));
return (
<div>
<p>{title}</p>
{ count.map((entry, index) => (
<Child
num={entry}
key={index}
onIncrement={() => onIncrement(index))} {/* if you want the child to be able to do it */}
/>
)) }
<button onClick={incrementAll}>Click Me</button>
</div>
)
}

Related

i want to increment and decrement the number when the button from other component is clicked

counterScreen.js
import React, { useState } from "react";
const CounterScreen = () => {
const [count, setCount] = useState(0);
return (
<div>
<h2>This the number: {count}</h2>
</div>
) }
export default CounterScreen
addButton.js
import React from 'react'
const AddButton = () => {
return (
<div>
<button>+</button>
</div>
) }
export default AddButton
subtractButton.js
import React from 'react'
const SubtractButton = () => {
return (
<div>
<button>-</button>
</div>
) }
export default SubtractButton
i want when i click the button in addbutton.js the counter should add 1 and when i click the button in subtractbutton.js the counter should subtract 1
what will be the best way to share the state here please help
One simple way to solve this is to put the state in the containing component, and pass in values or callbacks to the relevant components:
const Counter = () => {
const [count, setCount] = useState(0);
return (
<div>
<CounterScreen count={count}/>
<AddButton onClick={() => setCount(count+1)}/>
<SubtractButton onClick={() => setCount(count-1)}/>
</div>
);
};
const CounterScreen = ({count}) => {
return (
<div>
<h2>This the number: {count}</h2>
</div>
)
};
const AddButton = ({onClick}) => {
return (
<div>
<button onClick={onClick}>+</button>
</div>
)
};
const SubtractButton = ({onClick}) => {
return (
<div>
<button onClick={onClick}>-</button>
</div>
)
};

React useImperativeHandle and forwardRef being set, the reference doesn't seem to be updated

I need to access the location of a child component. For what I understand, to access the child properties, I need to use useImperativeHandle to add the child API to its ref. Moreover, I need to use forwardRef to transmit the reference from the parent to the child. So I did this:
const Text = React.forwardRef(({ onClick }, ref) => {
const componentAPI = {};
componentAPI.getLocation = () => {
return ref.current.getBoundingClientRect ? ref.current.getBoundingClientRect() : 'nope'
};
React.useImperativeHandle(ref, () => componentAPI);
return (<button onClick={onClick} ref={ref}>Press Me</button>);
});
Text.displayName = "Text";
const App = () => {
const ref = React.createRef();
const [value, setValue] = React.useState(null)
return (<div>
<Text onClick={() => setValue(ref.current.getLocation())} ref={ref} />
<div>Value: {JSON.stringify(value)}</div>
</div>);
};
ReactDOM.render(<App />, document.querySelector("#app"))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="app"></div>
As you can see, the ref doesn't have the getBoundingClientRect property, but if I do this it will work as expected:
const App = () => {
const ref = React.createRef();
const [value, setValue] = React.useState(null)
return (<div>
<button ref={ref} onClick={() => setValue(ref.current.getBoundingClientRect()) } ref={ref}>Press Me</button>
<div>Value: {JSON.stringify(value)}</div>
</div>);
};
ReactDOM.render(<App />, document.querySelector("#app"))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="app"></div>
So what is wrong with my understanding of useImperativeHanedle and forwardRef?
To use useImperativeHandle you need to work with another ref instance like so:
const Text = React.forwardRef(({ onClick }, ref) => {
const buttonRef = React.useRef();
React.useImperativeHandle(
ref,
() => ({
getLocation: () => buttonRef.current.getBoundingClientRect()
}),
[buttonRef]
);
return (
<button onClick={onClick} ref={buttonRef}>
Press Me
</button>
);
});
If you want your logic to be valid (using the same forwarded ref), this will work:
const Text = React.forwardRef(({ onClick }, ref) => {
React.useEffect(() => {
ref.current.getLocation = ref.current.getBoundingClientRect;
}, [ref]);
return (
<button onClick={onClick} ref={ref}>
Press Me
</button>
);
});
Why your example doesn't work?
Because ref.current.getBoundingClientRect not available in a moment of assigning it in useImperativeHandle (try logging it) because you actually overridden the button's ref with useImperativeHandle (Check Text3 in sandbox, the ref.current value has getLocation assigned after the mount).
As shown in docs(maybe not understandable enough), the child component itself should have a different ref, and by useImperativeHandle you can define a function mapping forwardedRef to child ref:
import React from 'react'
import ReactDOM from 'react-dom'
const Text = React.forwardRef(({ onClick }, ref) => {
const buttonRef = React.useRef() // create a new ref for button
const componentAPI = {};
componentAPI.getLocation = () => {
return buttonRef.current.getBoundingClientRect ? buttonRef.current.getBoundingClientRect() : 'nope' // use buttonRef here
};
React.useImperativeHandle(ref, () => componentAPI); // this maps ref to buttonRef now
return (<button onClick={onClick} ref={buttonRef}>Press Me</button>); // set buttonRef
});
Text.displayName = "Text";
const App = () => {
const ref = React.useRef();
const [value, setValue] = React.useState(null)
return (<div>
<Text onClick={() => setValue(ref.current.getLocation())} ref={ref} />
<div>Value: {JSON.stringify(value)}</div>
</div>);
};
ReactDOM.render(<App />, document.querySelector("#app"))
I just wanted to add this answer to show how things can become easier when removing useless overcontrol...
const Text = React.forwardRef(({ onClick }, ref) => {
ref.getLocation = () => ref.current && ref.current.getBoundingClientRect()
return (<button onClick={onClick} ref={ref}>Press Me</button>);
});
Text.displayName = "Text";
function App() {
const ref = { current: null };
const [value, setValue] = React.useState(null)
return (<div>
<Text onClick={() => setValue(ref.getLocation())} ref={ref} />
<div>Value: {JSON.stringify(value)}</div>
</div>);
}
ReactDOM.render(<App />, document.querySelector("#app"))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="app"></div>
In the code above, we just use forwardRef and attach the child API to it's ref, which seems very natural in the end, and very userfriendly.
The only thing that would prevent you using this is that React.createRef makes a call to Object.preventExtension() (thanks for making my life harder...), so the hack is to use { current: null } instead of Object.createRef() (which is basically the same).

Can i set state in parent from child using useEffect hook in react

I have a set of buttons in a child component where when clicked set a corresponding state value true or false. I have a useEffect hook in this child component also with dependencies on all these state values so if a button is clicked, this hook then calls setFilter which is passed down as a prop from the parent...
const Filter = ({ setFilter }) => {
const [cycling, setCycling] = useState(true);
const [diy, setDiy] = useState(true);
useEffect(() => {
setFilter({
cycling: cycling,
diy: diy
});
}, [cycling, diy]);
return (
<Fragment>
<Row>
<Col>
<Button block onClick={() => setCycling(!cycling)}>cycling</Button>
</Col>
<Col>
<Button block onClick={() => setdIY(!DIY)}>DIY</Button>
</Col>
</Row>
</Fragment>
);
};
In the parent component I display a list of items. I have two effects in the parent, one which does an initial load of items and then one which fires whenever the filter is changed. I have removed most of the code for brevity but I think the ussue I am having boils down to the fact that on render of my ItemDashboard the filter is being called twice. How can I stop this happening or is there another way I should be looking at this.
const ItemDashboard = () => {
const [filter, setFilter] = useState(null);
useEffect(() => {
console.log('on mount');
}, []);
useEffect(() => {
console.log('filter');
}, [filter]);
return (
<Container>..
<Filter setFilter={setFilter} />
</Container>
);
}
I'm guessing, you're looking for the way to lift state up to common parent.
In order to do that, you may bind event handlers of child components (passed as props) to desired callbacks within their common parent.
The following live-demo demonstrates the concept:
const { render } = ReactDOM,
{ useState } = React
const hobbies = ['cycling', 'DIY', 'hiking']
const ChildList = ({list}) => (
<ul>
{list.map((li,key) => <li {...{key}}>{li}</li>)}
</ul>
)
const ChildFilter = ({onFilter, visibleLabels}) => (
<div>
{
hobbies.map((hobby,key) => (
<label {...{key}}>{hobby}
<input
type="checkbox"
value={hobby}
checked={visibleLabels.includes(hobby)}
onChange={({target:{value,checked}}) => onFilter(value, checked)}
/>
</label>))
}
</div>
)
const Parent = () => {
const [visibleHobbies, setVisibleHobbies] = useState(hobbies),
onChangeVisibility = (hobby,visible) => {
!visible ?
setVisibleHobbies(visibleHobbies.filter(h => h != hobby)) :
setVisibleHobbies([...visibleHobbies, hobby])
}
return (
<div>
<ChildList list={visibleHobbies} />
<ChildFilter onFilter={onChangeVisibility} visibleLabels={visibleHobbies} />
</div>
)
}
render (
<Parent />,
document.getElementById('root')
)
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.12.0/umd/react.production.min.js"></script><script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.11.0/umd/react-dom.production.min.js"></script><div id="root"></div>
Yes, you can, useEffect in child component which depends on the state is also how you typically implement a component which is controlled & uncontrolled:
const NOOP = () => {};
// Filter
const Child = ({ onChange = NOOP }) => {
const [counter, setCounter] = useState(0);
useEffect(() => {
onChange(counter);
}, [counter, onChange]);
const onClick = () => setCounter(c => c + 1);
return (
<div>
<div>{counter}</div>
<button onClick={onClick}>Increase</button>
</div>
);
};
// ItemDashboard
const Parent = () => {
const [value, setState] = useState(null);
useEffect(() => {
console.log(value);
}, [value]);
return <Child onChange={setState} />;
};

Pass multiple elements using forwardRef

How to pass multiple elements from a component using forwardRef
I have this component and I want to pass separately loader--holder and loader--holder__logo to parent function component
const Loader = forwardRef((props, ref) => {
const ref1 = useRef(null);
const ref2 = useRef(null);
return (
<div className="loader--holder" ref={ref1}>
<div className="loader--holder__box">
<div className="loader--holder__logo"ref={ref2}>
<img src={logo} alt="logo" />
</div>
</div>
</div>
);
});
Parent component
export default function HomePage() {
const ref = React.createRef();
return (
<div>
<Loader ref={ref} />
</div>
)
}
Just assign to the forwarded ref what you need:
const Loader = forwardRef((props, ref) => {
const ref1 = useRef(null);
const ref2 = useRef(null);
useEffect(() => {
if (ref) {
ref.current = {
ref1,
ref2,
};
}
}, [ref]);
return (
<div className="loader--holder" ref={ref1}>
<div className="loader--holder__box">
<div className="loader--holder__logo" ref={ref2}>
<div>Hello</div>
</div>
</div>
</div>
);
});
// Usage
export default function HomePage() {
const ref = useRef();
useEffect(() => {
console.log('in parent', ref);
}, []);
return <Loader ref={ref} />;
}

React Change inner function of parent component using functional component

I have two components: one parent and one in child component as shown below:
import React, { Component, useState, useEffect } from 'react';
const useDocumentTitle = (title) => {
useEffect(() => {
document.title = title;
}, [title])
}
function App(){
const [count,setCount] = useState(0);
const incrementCount = () => setCount(count + 1);
const decrementCount = () => setCount(count - 1);
const newDivElem = () => { return ( <>Hello World </>)}
useDocumentTitle(`You clicked ${count} times`);
return (
<>
Count of this value {count}
<br />
<button onClick={incrementCount}>+</button>
<button onClick={decrementCount}>-</button>
{newDivElem()}
</>
);
}
export default App;
function InternalApp(){
return(
<App />
);
}
export default InternalApp;
How can I overwrite the App component inner function newDivElem() inside the InternalApp component?
Please suggest some idea.
You can extract the function to a prop, and set the original function as the default value:
const newDivElem = () => { return ( <>Hello World </>)}
function App({ newDivElem = newDivElem }){
const [count,setCount] = useState(0);
const incrementCount = () => setCount(count + 1);
const decrementCount = () => setCount(count - 1);
useDocumentTitle(`You clicked ${count} times`);
return (
<>
Count of this value {count}
<br />
<button onClick={incrementCount}>+</button>
<button onClick={decrementCount}>-</button>
{newDivElem()}
</>
);
}
If you want to override it, pass another function as the prop:
function InternalApp(){
return(
<App newDivElem={() => <div>Something Else</div>} />
);
}

Categories