how to pass props to children with React.cloneElement? - javascript

I'm trying to pass props to my component children but I have this error : Unknown prop 'user' on tag. Remove this prop from the element.
When looking at documentation and questions, I think I understood that props given to React.cloneElement (second argument) must be DOM recognized properties.
So my question is how to pass props to the component children and make them accessible in this.props ?
Here is my code :
render() {
const { children } = this.props
const { user } = this.state
const childrenWithProps = React.Children.map(children, child =>
React.cloneElement(child, { user })
)
return (
<div>
{ childrenWithProps }
</div>
)
}
edit : the children component's propTypes
ChildrenPage.propTypes = {
user: PropTypes.object
}
export default ChildrenPage

Your code looks fine for me. Usually, React give this warning when you are trying to render DOM element(Not a React Component) with invalid/non-standard DOM attribute.
In your case, this might happen if your children collection has a DOM element. Since user is not a standard DOM attribute, it might fire this warning when you are trying to clone the element with user prop.
You can read more about this error here.
Hope this helps!

Make sure that you are not passing down the children key as props. Below code removes children key from props before passing it down to the children.
let key = 'children';
let {[key]: _, ...newProps} = state;
{React.Children.map(this.props.children, child =>
React.cloneElement(child, {...newProps}))}
This is one of the possible reasons. and, let me know if this solution works. For more, https://facebook.github.io/react/warnings/unknown-prop.html

Related

React clone element to modify a child component and keep ref in a functional component

I used to have refs in my component when rendering, and it worked:
// props.children is ReactElement<HTMLDivElement>[]
const [childRefs] = useState<RefObject<any>[]>(props.children.map(() => createRef()));
// working code, all the variables (props, childRefs etc) are defined earlier in scope
return <div {...props}>
{
props.children.map((c, i) => <div key={i} ref={childRefs[i]}>{c}</div >)
}
</div>
Basically I'm using the refs to imperatively directly set some transforms to style as mouse moves on JS mousemove event.
However, I now need to inject some CSS class into the passed component automatically. I've created a component (named Layer) that takes the child element, clones it, sets the CSS class, and returns it:
function Layer(props:LayerProps){
const elem = cloneElement(props.children, {...props.children.props,
className: styles['layer']
});
return elem;
}
I've updated the main component like this too:
return <div {...props}>
{
props.children.map((c, i) => <Layer key={i} ref={childRefs[i]}>{c}</Layer>)
}
</div>
However now my refs aren't passed now, understandibly, as the Layer functional component can't have a ref (as it's a function). When I try to set the ref to Layer it can't, and have this error (understandably):
(property) ref: React.RefObject<any>
Type '{ children: ReactElement<HTMLDivElement, string | JSXElementConstructor<any>>; key: number; ref: RefObject<any>; }' is not assignable to type 'IntrinsicAttributes & LayerProps'.
Property 'ref' does not exist on type 'IntrinsicAttributes & LayerProps'.ts(2322)
If I try to forward the ref using forwardRef it doesn't have anything to set that ref to as I'm just modifying the passed child element and returning it, not returning a new element like <div>...</div> that I could forward ref to like <div ref={forwardedRef}>...</div>.
How can I modify the CSS class and keep a ref to the object? I know how to do each one (if I just need to add class I cloneElement, if I just need to ref it I use forwardRef and pass it to the child component in JSX) yet I couldn't figure out being able to do both at the same time.
How can I do it?
Okay, after a bit digging and experimenting I've realized I can give ref "prop" (which isn't technically a real prop, but anyway) in cloneElement just like any prop.
I've ended up forwarding ref to the functional component, and provided ref as a prop to the newly cloned element, and it worked.
Yet, the TypeScript definitions are incorrectly flagging ref property as non-existent while it works perfectly. I needed it to cast the props to any to silence the linter error though:
const Layer = forwardRef((props:LayerProps, ref:any) => {
const elem = cloneElement(props.children, {...props.children.props,
className: `${props.children.props.className ?? ''} ${styles['layer']}`,
ref: ref
} as any); // as any fixes ref complaining
return elem;
});
And in many component:
return <div {...props}>
{
props.children.map((c, i) => <Layer key={i} ref={childRefs[i]}>{c}</Layer>)
}
</div>

Pass data from grandchild to parent in React

Hello i have an array called info[] in a grandchild component and i want my parent component when a button is clicked to access the array. I also want a sibling component to have access to it. How is this possible .. i am a bit confused.
Should I use use-context ?
Thank you!
If I have understand what you are asking it could be something like this.
const GrandChild = ({ setParentInfo }) => {
const info = [1, 2, 3];
const handleClick = () => {
setParentInfo(info);
};
return <button onClick={handleClick}>Set parent info</button>;
};
const Sibling = ({ parentInfo }) => {
return <div>{parentInfo.length}</div>; // Do whatever you need with parentInfo
};
const Parent = () => {
const [parentInfo, setParentInfo] = useState([]);
return (
<div>
<GrandChild setParentInfo={setParentInfo} />
<Sibling parentInfo={parentInfo} />
</div>
);
};
Here you don't need context because you don't have that much layers but if you need to drill down the props than use a context.
If you want to share state among many different components in your application, and you believe that passing State as a prop is "a very long journey" to move around I'm probably you should consider something like use context hook.
Either way what you just described seems like a simple use case witch will not need context.
What you should do is:
On the parent you have [state, setState]
On the current component pass setStat as a prop to child component and then from child component pass setState as a prop to grandchild component.
Then on grandchild component you can do something like:
props.setState(array).
So now on the parent component the variable state will have been updated with the value array from the grandchild component.
If you want to pass state to siblings component, and by sibling I assume you mean sibling of the parent,
Then you should move the state from parent one level up let's say the parent of the parent.. and do what I've described above.
So create useState high in your component tree,
And pass State and setState as well to children as props, setState will be passed as function, so you can call it on the grandchild component or any other component

Access list of siblings in React

Parent element (Board) creates list of children and passes them method to access this list, like this:
export default class Board extends React.Component {
constructor(props) {
super(props);
this.getList = this.getList.bind(this);
const nodes = this.props.data.map(item => (
<BoardItem key={item.id} next={item.next} accessSiblings={this.getList} />
));
this.state = {data: this.props.data, nodes: nodes}
}
getList() {
return this.state.nodes;
}
render() {
return (
<div>
{this.state.nodes}
</div>
);
} }
Then I call update() method and receive this list, filter it and correctly get the required object:
update() {
console.log(this.props.accessSiblings().find(x => x.key == this.props.next));
}
However it returns Symbol(react.element), and I am trying to get such properties as "offsetTop", "offsetHeight" of already rendered element.
Basically, I want to call an event in one element that gets some DOM properties of sibling element(e.g. offsetTop) and changes the state of this sibling.
Is this the correct approach? It feels very hacky and doesn't work at the moment.
You need to use "refs" (see also: how to access a dom element in react).
However, you should avoid working with DOM objects, if possible.
You do need a ref for accessing offsetTop etc., but apart from that you should not pass DOM or ReactElements, but you should only work with state (like "plain javascript" objects) as far as possible, and then render ReactElements (JSX, like <BoardItem ...) as the last step, and never care about DOM elements (React does it for you).
It is also usually not necessary to store ReactElements in variables or state, I suggest to try if you can focus a little bit more on state, and understand JSX more as a way to view the state.

Set a child array state object from props value

I am creating a react application using hooks but I'm getting a weird behavior on my code, I'm not going to put all my code here but I will give you an example what it's going on.
const Child = props => {
const {data} = props;
const [myData, setMyData] = useState(data);
const items => myData.map( r => <li> r </li> );
return ( <ul> { items } </ul> );
}
const Parent = () => {
return (<div>
<Child data={ [1, 2, 3] }
</div> );
}
I am making changes to the array that the Parent component sends to the Child component.
When I add a new element to the array, the Child component re-render so MyData is equals to the array ( this makes sense because the Child components is re-render by the props change ).
If I delete an element from the array the Child component is re-render but myData doesn't change and the element I deleted from the array is still there in myData.
Why the useState sets the array to myData when I add elements to the array but when I delete elements it seems like this doesn't work,even though the Child component is re-render.
I know it seems a little dumb, you can ask me, why you don't just use the props value on the Child component instead of a state value, well the idea is on the Child component there is a search control so I can make some kind of searching over MyData and not over the props value ( but maybe there is another way ).
I think the issue is how you're using props.
When Child renders, it gets props.data from Parent. It then copies data to its component state as the variable myData.
From here on, changes to props can trigger the Child to re-render, but myData won't be redefined again via useState. That happens only once.
If you're writing code that does hot-reloading (you save the file and the application reloads in the browser), it might seem like changing the props sent to Child updates myData, but that's not happening in a production environment.
My suggestion: if you're going to fork props into the state of Child, think of props as an initial value, and not something that Parent can update.
The React docs explain why this is an anti-pattern:
The problem is that it’s both unnecessary (you can use this.props.color directly instead), and creates bugs (updates to the color prop won’t be reflected in the state).
Only use this pattern if you intentionally want to ignore prop updates.
React constructor docs
useEffect(() => {
setMyData(data)
},[data])
your data will be load to state only once. but you can handle changes on data by parent component with useEffect, useEffect will updates when data will updated
there is full code
const Child = props => {
const {data} = props;
const [myData, setMyData] = useState(data);
useEffect(() => {
setMyData(data)
},[data])
const items => myData.map( r => <li> r </li> );
return ( <ul> { items } </ul> );
}
const Parent = () => {
return (<div>
<Child data={ [1, 2, 3] }
</div> );
}
you can read about React use effect

React - Remove prop from child

I need to remove a prop from a child.
I have a container element which uses a property on it's children to perform some enhancements on the children. That property should be removed from the child before rendering.
<AsyncContainer>
<Button onClick={this.asyncStuff} asyncHandler="onClick"/>
</AsyncContainer>
The asyncHandler property should be removed from the button before rendering.
AsyncContainer uses React.cloneElement(child, properties).
I've tried nulling the asyncHandler property, setting it to undefined and deleting the property from the child.props. It seems that it is impossible to get rid of this property again.
I just ran into this issue. You can just create a new element and use the old element's type and props you want to pass through. I'm not sure if this an anti-pattern or not, I just stumbled on it and it seems to be working well so far.
It should look something like this:
function AsyncContainer(props) {
const child = React.Children.only(props.children)
const { asyncHandler, ...childProps } = child.props
// do asyncHandler stuff
return React.createElement(child.type, childProps)
}
function AsyncContainer(props) {
const child = React.Children.only(props.children);
return React.cloneElement(
child,
{ asyncHandler: undefined }
);
}
How it works
You clone element using React.cloneElement because element is immutable and only way to change its props is to create clone.
Use second React.cloneElement argument to add new props and remove old props. Unneeded props should be assigned with undefined. You need to do this because by default cloned element is cloned with all its props.
As per the comments you cannot modify the props directly as they are immutable.
However, I think I have a simple solution to this problem. I have no idea what library that is or how it works, so this may or may not work. However, this is a general answer to how you would remove a prop before a component gets mounted.
That being said, I would try to create my own component which renders a <Button />:
class MyButtonComponent extends React.Component {
...
render() {
return <Button onClick={this.props.onClickHandler} />;
}
}
Then in the component you want to do your enhancements:
render() {
<AsyncContainer>
<MyButtonComponent onClickHandler={this.asyncStuff} asyncHandler="onClick"/>
</AsyncContainer>
}
This way you maintain your onClick eventlistener on the <Button /> component but you don't pass the illegal asyncHandler prop.
Edit:
Alternatively, you could also do:
class MyButtonComponent extends React.Component {
...
componentWillMount() {
let newProps = this.props;
delete newProps.asyncHandler;
this.setState({properties: newProps}):
}
render() {
return <Button {...this.state.properties} />;
}
}
This will apply all the props (with the spread operator) to <Button /> except for asyncHandler which we delete prior to the component being mounted by creating a copy of the props in state but with asyncHandler removed.
Also check this answer I gave to a similar question.

Categories