Forward multiple ref to react children - reactjs

I have seen some articles about forwardRef, however couldn't be able to find an example that have a specific approach to it, basically when we are dealing with the children props of React and we need to forward multiple ref to this children props.
I will give an example to clarify any doubt.
Let image we have a parent component List, which will look like it:
const List = ({ children }) => (
<div>
{children}
</div>
);
And we have its children component Tab:
const Tab = ({ children }) => (
<div>
{children}
</div>
);
They are being used like that:
<List>
<Tab />
<Tab />
<Tab />
</List>
Therefore my question is, how I would be able to create multiple refs at List, be able to forward them to Tab, properly set them at each Tab, and them finally get its reference at List to work with.
If there still any doubts I'm happy to clarify.

I guess you can use something like React.cloneElement and pass the references as props to the children.
{React.Children.map(children, childElement =>
React.cloneElement(childElement)
)}

This is the current answer I came with:
const tabReferences = React.useRef<HTMLDivElement>([]);
...
{React.Children.map(children, (child, index) => {
return React.cloneElement(child, {
ref: tabReferences.current[index],
});
})}
Thanks Lucas!

Related

Two instances of the same element seem to share state

In my React application I have a Collapsible component that I use more than once, like so:
const [openFAQ, setOpenFAQ] = React.useState("");
const handleFAQClick = (question: string) => {
if (question === openFAQ) {
setOpenFAQ("");
} else {
setOpenFAQ(question);
}
};
return ({
FAQS.map(({ question, answer }, index) => (
<Collapsible
key={index}
title={question}
open={openFAQ === question}
onClick={() => handleFAQClick(question)}
>
{answer}
</Collapsible>
))
})
And the Collapsible element accepts open as a prop and does not have own state:
export const Collapsible = ({
title,
open,
children,
onClick,
...props
}: Props) => {
return (
<Container {...props}>
<Toggle open={open} />
<Title onClick={onClick}>{title}</Title>
<Content>
<InnerContent>{children}</InnerContent>
</Content>
</Container>
);
};
However, when I click on the second Collapsible, the first one opens... I can't figure out why.
A working example is available in a sandbox here.
You will need to ensure that the label and id for each checkbox is the same. Here's a working example
But if you're trying to implement an accordion style, you may need another approach.
on Collapsible.tsx line 36, the input id is set the same for both the Collapsibles. you need to make them different from each other and the problem would be solved.
One thing is that you have the same id which is wrong BUT it can still work. Just change the checkbox input from 'defaultChecked={...}' to 'checked={...}'.
The reason is that, if you use defaultSomething - it tells react that even if this value will be changed - do not change this value in the DOM - https://reactjs.org/docs/uncontrolled-components.html#default-values
Changing the value of defaultValue attribute after a component has mounted will not cause any update of the value in the DOM

I can't figure out a way to set the key on list elements from the container even though the container can very well set any other properties

Ok so here is a simplification of my real-world use-case. Basically I have a complicated layout and I pass the layout component to the child so that it renders itself within it and then the parent layout displays it as its children. Works great, except I use this in a loop and I cannot figure out how to set the key on the children from the parent.
It's strange because I can set other properties without any issues. In the example below, the "id" property is correctly set from the list, but the console emits the warning:
Warning: Each child in a list should have a unique "key" prop.
const Child = Parent => (
<Parent>
The child is reponsible
<br />
for writing the contents.
</Parent>
);
const present = (title) => {
const Tag = ({ children }) => (
<article id={title} key={title}>
<h1>{title}</h1>
{children}
</article>
);
return Child(Tag);
};
const app = (
<div>
{[
'hello',
'this',
'is',
'a list'
].map(present)}
</div>
);
ReactDOM.render(
app,
document.getElementById('root')
);
here's a demo of that code on codepen
Is there any way to accomplish what I'm trying to do?
This may seem like an over-complicated way to render a template, I know, but I have - I think - a good real-world use-case.
Actually I figured it out.
I need to use a React.Fragment around the return value of "present",
like so:
const present = (title) => {
const Tag = ({ children }) => (
<article id={title}>
<h1>{title}</h1>
{children}
</article>
);
return (
<React.Fragment key={title}>
{Child(Tag)}
</React.Fragment>
);
};
the problem is you are providing the key to the child & not the parent of what is being rendered from the map items.
you can set key to the parent component (instead of <article>) as well as sending the Tag parameter:
const Child = (Parent, key) => (
<Parent key={key}>
The child is reponsible
<br />
for writing the contents.
</Parent>
);
//...
const present = (title) => {
const Tag = ({ children }) => (
<article id={title}>
<h1>{title}</h1>
{children}
</article>
);
return Child(Tag, title);
};
your solution by using a React.Fragment also does a similar job (setting the key for the outer wrapper (here being fragment) in rendering the list using the map method). the difference being you are creating a fragment for each iteration in the map method, but by passing the key you can avoid that as well.

Filtering array of child components

In the parent component, I have a state array of child components:
const [myList, setMyList] = useState([]);
setMyList([
<MyComponent color='blue' />
<MyComponent color='blue1' />
<MyComponent color='blue2' />
<MyComponent color='red' />
])
I display these later on with
myList.map(item => item)
I sometimes might have a large number of items in the list and would like to have a search box that does myList.filter() on the name in the input box.
I have something like this
const itemsToHide = myList.filter(item => !item.props.color.includes(e.target.value));
This correctly gives me the items that do not match the search.
Is there a way to now set display to none to these items? Or hide them in another way that will not dismount them since when i clear the search box i want them to still be there.
Is there a reason to have them in another array? Just checking the condition inside the renderer might be easier. Either wrap your Component in another div
myList.map((item) => {
const isVisible = item.props.name.includes(searchTerm);
return (
<div key={item.props.id} style={{display: `${isVisible ? 'block' : 'none'}`>
{item}
</div>
)
}
or pass a prop and render your component in the same manner. Don't forget to add a unique key to all the divs though.
Just render the items that satisfy the condition:
myList
.filter(item => item.name.includes(searchText))
.map((item) =>
(<div key={item.props.id}>
{item}
</div>
))

Jest: functional component array list not mapping after adding element

I am trying to list person by clicking onClick function to add empty object so that map can loop
over and show div. I see prop onclick function calling works but i map is not looping over it.
a little help would be appreciated.
// functional component
const listing = ({list=[]}) => (
<>
{
<div id='addPerson' onClick={() => list.push({})}>Add person</div>}
{list.map((element, index) => (
<div key={index} className='listItems'>
<p>list name</p>
</div>
))}
</>
);
export default listing;
// test case
it('renders list-items', () => {
const wrapper = shallow(<listing />);
wrapper.find('#addPerson').prop('onClick')();
console.log(wrapper.find('.listItems').length); // showing zero, expected 1
});
Updating List is not going to cause a re-render in your component. I am not sure where List is coming from, but I am assuming it is coming from a local state in the parent, or probably redux store. From inside your component, you will have to call a function in your parent to update the list array there.
Below is a version of your component, assuming the list in your local state. You will have to adjust the code to update the state in the parent or redux store, but the below should be enough to give you an idea of what to do. Also, don't push the element to array, as it mutates the object and will not contact re-render.
const Listing = ({list = [], addToList}) => {
return <>
{
<div id='addPerson' onClick={() => addToList({})}>Add person</div>}
{list.map((element, index) => (
<div key={index} className='listItems'>
<p>list name</p>
</div>
))}
</>
};

lazy render children

Take a look at this simple example:
const List = function({ loading, entity }) {
return (
<Layout loading={loading}>
<span>Name: {entity.name}</span>
</Layout>
);
};
Layout component is rendering its children only when loading is false. But the problem here is that React is resolving Layout children immediatelly. Since entity is null (while loading=true) I get error that it can't read name of null. Is there a simple way to avoid this error since this span will always be rendered when entity is not null?
Currently I know about 3 options:
Move this span to stateless function which receives entity as prop
Wrap whole children of Layout in function and then support function children in Layout
Just use {entity && <span>Name: {entity.name}</span>}
Why do I have to use one of these options and can I make React to consider those children as function and resolve block inside later on before the render?
I just stumbled upon the same problem.
What worked for me was passing children as a function:
ready: boolean;
children?: () => ReactNode,
}> = ({ ready, children }) => {
return (
<div>{
ready ?
children() :
(
<div>not ready</div>
)
}</div>
);
};
<Layout loading={loading}>
{() =>
<span>Name: {entity.name}</span>
}
</Layout>
Though it's still not perfect.

Resources