I have a dashboard with different components. Everything is working with a separate start-button on each component, now I need to have a common start-button, and for accessing the children's subfunctions from a parent, I understand that in React you should use the useRef.(but its perhaps not correct, but I'm struggling to see another way). I would like to have the flexibility to choose which component to start from this "overall start-button"
I have a component list that i map through shown below.
return(
{ComponentsList.map((item) => {
return (
<Showcomponents
{...item}
key={item.name}
/>
)
This works fine, but I would like, as mentioned, to access a function called something like "buttonclick" in each of the children, so I tested this with a pressure-gauge component
The function "exposed" via the forwardRef and the useImparativeHandle
const ShowRadialGauge = forwardRef((props, ref) => {
useImperativeHandle(ref, () => ({
buttonclick() {
setStart(!start);
},
}));
)
then in my dashboard I changed to :
const gaugepressure = useRef();
return(
<div>
<Button onClick={() => gaugepressure.current.buttonclick()}>
Start processing
</Button>
<ShowRadialGauge ref={gaugepressure} />
<div>
)
This works fine if I use the useRef from the dashboard and instead of mapping over the components, I add them manually.
I understand the useRef is not a props, but its almost what I want. I want to do something like this:
return(
{ComponentsList.map((item) => {
return (
<Showcomponents
{...item}
key={item.name}
**ref={item.ref}**
/>
)
where the ref could be a part of my component array (as below) or a separate array.
export const ComponentsList = [
{
name: "Radial gauge",
text: "showradialgauge",
component: ShowRadialGauge,
ref: "gaugepressure",
},
{
name: "Heatmap",
text: "heatmap",
component: Heatmap,
ref: "heatmapstart",
},
]
Anyone have any suggestions, or perhaps do it another way?
You are on the right track with a React ref in the parent to attach to a single child component. If you are mapping to multiple children though you'll need an array of React refs, one for each mapped child, and in the button handler in the parent you will iterate the array of refs to call the exposed imperative handle from each.
Example:
Parent
// Ref to hold all the component refs
const gaugesRef = React.useRef([]);
// set the ref's current value to be an array of mapped refs
// new refs to be created as needed
gaugesRef.current = componentsList.map(
(_, i) => gaugesRef.current[i] ?? React.createRef()
);
const toggleAll = () => {
// Iterate the array of refs and invoke the exposed handle
gaugesRef.current.forEach((gauge) => gauge.current.toggleStart());
};
return (
<div className="App">
<button type="button" onClick={toggleAll}>
Toggle All Gauges
</button>
{componentsList.map(({ name, component: Component, ...props }, i) => (
<Component
key={name}
ref={gaugesRef.current[i]}
name={name}
{...props}
/>
))}
</div>
);
Child
const ShowRadialGauge = React.forwardRef(({ name }, ref) => {
const [start, setStart] = React.useState(false);
const toggleStart = () => setStart((start) => !start);
React.useImperativeHandle(ref, () => ({
toggleStart
}));
return (....);
});
The more correct/React way to accomplish this however is to lift the state up to the parent component and pass the state and handlers down to these components.
Parent
const [gaugeStarts, setGaugeStarts] = React.useState(
componentsList.map(() => false)
);
const toggleAll = () => {
setGaugeStarts((gaugeStarts) => gaugeStarts.map((start) => !start));
};
const toggleStart = (index) => {
setGaugeStarts((gaugeStarts) =>
gaugeStarts.map((start, i) => (i === index ? !start : start))
);
};
return (
<div className="App">
<button type="button" onClick={toggleAll}>
Toggle All Guages
</button>
{componentsList.map(({ name, component: Component, ...props },, i) => (
<Component
key={name}
start={gaugeStarts[i]}
toggleStart={() => toggleStart(i)}
name={name}
{...props}
/>
))}
</div>
);
Child
const ShowRadialGauge = ({ name, start, toggleStart }) => {
return (
<>
...
<button type="button" onClick={toggleStart}>
Toggle Start
</button>
</>
);
};
#Drew Reese
Thx Drew,
you are off course correct. I'm new to React, and I'm trying to wrap my head around this "state handling".
I tested your suggestion, but as you say, its not very "React'ish", so I lifted the state from the children up to the parent.
In the parent:
const [componentstate, setComponentstate] = useState([
{ id:1, name: "pressuregauge", start: false},
{ id:2, name: "motormap", start: false },
{ id:3, name: "heatmapstart", start: false},
]);
then in the component ShowRadialGauge, I did like this:
const ShowRadialGauge = ({ props, componentstate })
and if we need to keep the button in each component, I have the id in the componentstate object that is desctructured, so I can send that back.
.
First of all, why do you need refs to handle click when you can access it via onClick. The most common use case for refs in React is to reference a DOM element or store value that is persist between renders
My suggestion are these
First, try to make it simple by passing a function and then trigger it via onClick
Second if you really want to learn how to use imperativeHandle you can reference this video https://www.youtube.com/watch?v=zpEyAOkytkU.
I have a dynamic component in which I pass in children as prop.
So the props look something like:
interface Props {
...some props
children: React.ReactNode
}
export default Layout({...some props, children}: Props) {...}
I need to access the size of the children elements (height and width), in the Layout component. Note that the children are from completely different components and are non-related.
I can use the Layout component as follow:
<Layout ...some props>
<Child1 /> // I need to know the height and width of this child
<Child2 /> // as well as this child
<Child3 /> // and this child.
</Layout>
How can I do so dynamically? Do I somehow have to convert ReactNode to HTMLDivElement? Note that there is no way I can pass in an array of refs as a prop into Layout. Because that the pages which use Layout are dynamically generated.
Since many doesn't really understand what I meant by dynamically generated. It means that the pages which are using the Layout component can pass in x amount of children. The amount of children is unknown but never 0.
You can achieve this by using React.Children to dynamically build up a list of references before rendering the children. If you have access to the children element references, you can follow the below approach. If you don't then you can follow the bit at the bottom.
You have access to the children element references
If the children components pass up their element reference, you can use React.Children to loop through each child and get each element reference. Then use this to perform calculations before the children components are rendered.
i.e. This is a very simple example on how to retrieve the references and use them.
interface LayoutWrapperProps {
onMount: () => void;
}
const LayoutWrapper: React.FC<LayoutWrapperProps> = ({ onMount, children }) => {
React.useEffect(() => {
onMount();
}, [onMount]);
return <>{children}</>;
};
const Layout: React.FC = ({ children }) => {
const references = React.useRef<HTMLElement[]>([]);
React.useEffect(() => {
references.current = [];
});
function getReference(ref: HTMLElement) {
references.current = references.current.filter(Boolean).concat(ref);
}
function getHeights() {
const heights = references.current.map((ref) =>
ref?.getBoundingClientRect()
);
console.log(heights);
}
const clonedChildren = React.Children.map(children, (child) => {
return React.cloneElement(child as any, {
ref: getReference
});
});
return <LayoutWrapper onMount={getHeights}>{clonedChildren}</LayoutWrapper>;
};
If you don't have access to the children element references
If the children components aren't passing up an element as the reference, you'll have to wrap the dynamic children components in a component so we can get an element reference. i.e.
const WrappedComponent = React.forwardRef((props, ref) => {
return (
<div ref={ref}>
{props.children}
</div>
)
});
When rendering the children components, then the code above that gets the references will work:
<Layout>
<WrappedComponent>
<Child1 />
</WrappedComponent>
</Layout>
Since we don't know how your children is built, here is what I can propose you :
import React from 'react';
import { render } from 'react-dom';
const App = () => {
const el1Ref = React.useRef();
const el2Ref = React.useRef();
const [childrenValues, setChildrenValues] = React.useState([]);
React.useEffect(() => {
setChildrenValues([
el1Ref.current.getBoundingClientRect(),
el2Ref.current.getBoundingClientRect()
]);
}, []);
return (
<Parent childrenVals={childrenValues}>
<span ref={el1Ref}>
<Child value="Hello" />
</span>
<span ref={el2Ref}>
<Child value="<div>Hello<br />World</div>" />
</span>
</Parent>
);
};
const Parent = ({ children, childrenVals }) => {
React.useEffect(() => {
console.log('children values from parent = ', childrenVals);
});
return <>{children}</>;
};
const Child = ({ value }) => {
return <div dangerouslySetInnerHTML={{ __html: value }} />;
};
render(<App />, document.getElementById('root'));
And here is the repro on Stackblitz.
The idea is to manipulate how your children is built.
I use React + styled and my question is about the following code
<MobileButton
onClick={props.handleMobileDropdownElementClicked}
padding={isSmallDevice ? 1 : 0}
>
Name
</MobileButton>
Is there a way to generalise this component to write like this without having props and onclick()? like the following code.
<MobileButton>
Name
</MobileButton>
I was just curious if there is things in react or styled component that could do that if we are repeating the same component with the same props/onclick function.
Thank you
Sure, you can make a new component that doesn't require said props. But you'll still have to pass the props when creating that component initially.
For example:
const YourComponent = (props) => {
const NewMobileButton = (newProps) => {
return (
<MobileButton
onClick={props.handleMobileDropdownElementClicked}
padding={isSmallDevice ? 1 : 0}
>
{newProps.children}
</MobileButton>
)
}
return (
<>
<NewMobileButton>1</NewMobileButton>
<NewMobileButton>2</NewMobileButton>
</>
)
}
The functional components in react are better to use if there aren't any internal state to be tracked within the component.
But what I want is to access the children of the stateless components without having to extend React.Component using which i can use props.children. Is this possible ?
If so , how to do it ?
We can use props.children in functional component. There is no need to use class based component for it.
const FunctionalComponent = props => {
return (
<div>
<div>I am inside functional component.</div>
{props.children}
</div>
);
};
When you will call the functional component, you can do as follows -
const NewComponent = props => {
return (
<FunctionalComponent>
<div>this is from new component.</div>
</FunctionalComponent>
);
};
Hope that answers your question.
Alternatively to the answer by Ashish, you can destructure the "children" property in the child component using:
const FunctionalComponent = ({ children }) => {
return (
<div>
<div>I am inside functional component.</div>
{ children }
</div>
);
};
This will allow you to pass along other props that you would like to destructure as well.
const FunctionalComponent = ({ title, content, children }) => {
return (
<div>
<h1>{ title }</h1>
<div>{ content }</div>
{ children }
</div>
);
};
You can still use "props.title" etc to access those other props but its less clean and doesn't define what your component accepts.
In the simple example below, I'm looking for a way to get for example CardBody child.
const Card = ({children}) => {
const cardHeadChild = ...;
const cardBodyChild = ...;
return (
<div>
{cardHeadChild}
{cardBodyChild}
</div>
)
}
const CardHead = () => <div>Head</div>
const CardBody = () => <div>Body</div>
// Usage:
<Card>
<CardHead>
<CardBody>
</Card>
I cannot get by index (eg: React.Children.toArray(children)[1]) because children are optionals.
I tried something like this:
React.Children.forEach(children, child => {
if(child.type.name === 'CardBody') cardBodyChild = child
// or
if(child.type.displayName === 'CardBody') cardBodyChild = child
..
})
but it doesn't work when component are wrapped in HOC.
Any solution ?
Function name shouldn't be used in production client-side code because function names are mangled when the application is minified. The same applies to displayName - unless it was set explicitly. Also notice that primary use of displayName and name is debugging.
Children can be identified by React element type. If the purpose is to output optional children in specified order, this can be done similarly to this answer:
Optional head:
{props.children.find(({ type }) => type === CardHead)}
Optional body:
{props.children.find(({ type }) => type === CardBody)}
It's expected that children should be exactly CardHead and CardBody stateless components. If there's a need to enhance their functionality with other components, CardHead and CardBody should wrap these components:
const CardHead = props => <div>
Head
{props.children}
</div>
...
<Card>
<CardHead><SomeComponent/></CardHead>
</Card>