React: how to forward refs to multiple children? - reactjs

I'm trying to forward multiple refs to multiple children DOM nodes:
I need references to the 7 buttons, so I can manage focus between them.
I tried by using an array of React.createRef() and then attach each element of this array to a child using index, but all refs refers to the last button.
Why and is there another solution?
class RestaurantsList extends Component {
references = Array(7).fill(React.createRef());
render() {
return (
<ul
id="restaurants-list"
role="menu"
>
{
this.props.restaurants.map((restaurant, index) => {
return (
<Restaurant
ref={this.references[index]}
name={restaurant.name}
/>
);
})
}
</ul>
);
}
}
const Restaurant = React.forwardRef((props, ref) => {
return (
<li>
<button
ref={ref}
>
{name}
</button>
</li>
);
})

As it was discussed in comments the best way is to keep list of references as an array or object property inside parent component.
As for Array.prototype.fill() its arguments is calculated just once. In other words fill(React.createRef()) will generate list where each entry will refer to the same object - and you will get equal ref to last element. So you need to use .map() for getting unique reference objects.
references = Array(7).fill(0).map(() => React.createRef());
Anyway in real world project this will rather happen in constructor() or componentDidUpdate().
But I believe it's better to have hashmap:
references = {};
getOrCreateRef(id) {
if (!this.references.hasOwnProperty(id)) {
this.references[id] = React.createRef();
}
return this.references[id];
}
render() {
return (
....
{
this.props.restaurants.map((restaurant, index) => {
return (
<Restaurant
ref={this.getOrCreateRef(restaurant.id)}
key={restaurant.id}
name={restaurant.name}
/>
);
})
}
Also you will need some helper methods to avoid exposing this.references to outer world:
focusById(id) {
this.references[id].current && this.references[id].current.focus();
}
And take special attention to cleaning up references to unmounted elements. Otherwise you may got memory leak if list of restaurants is changed dynamically(if ref stays in this.references it keeps reference to HTML element even if it has been detached). Actual need depends on how is your component used. Also this memory leakage will be fixed once container(that has reference = {}) is unmounted itself due to navigating away.

Related

Conditionally assign ref in react

I'm working on something in react and have encountered a challenge I'm not being able to solve myself. I've searched here and others places and I found topics with similar titles but didn't have anything to do with the problem I'm having, so here we go:
So I have an array which will be mapped into React, components, normally like so:
export default ParentComponent = () => {
//bunch of stuff here and there is an array called arr
return (<>
{arr.map((item, id) => {<ChildComponent props={item} key={id}>})}
</>)
}
but the thing is, there's a state in the parent element which stores the id of one of the ChildComponents that is currently selected (I'm doing this by setting up a context and setting this state inside the ChildComponent), and then the problem is that I have to reference a node inside of the ChildComponent which is currently selected. I can forward a ref no problem, but I also want to assign the ref only on the currently selected ChildComponent, I would like to do this:
export default ParentComponent = () => {
//bunch of stuff here and there is an array called arr and there's a state which holds the id of a selected ChildComponent called selectedObjectId
const selectedRef = createRef();
return (<>
<someContextProvider>
{arr.map((item, id) => {
<ChildComponent
props={item}
key={id}
ref={selectedObjectId == id ? selectedRef : null}
>
})}
<someContextProvider />
</>)
}
But I have tried and we can't do that. So how can dynamically assign the ref to only one particular element of an array if a certain condition is true?
You can use the props spread operator {...props} to pass a conditional ref by building the props object first. E.g.
export default ParentComponent = () => {
const selectedRef = useRef(null);
return (
<SomeContextProvider>
{arr.map((item, id) => {
const itemProps = selectedObjectId == id ? { ref: selectedRef } : {};
return (
<ChildComponent
props={item}
key={id}
{...itemProps}
/>
);
})}
<SomeContextProvider />
)
}
You cannot dynamically assign ref, but you can store all of them, and access by id
export default ParentComponent = () => {
//bunch of stuff here and there is an array called arr and theres a state wich holds the id of a selected ChildComponent called selectedObjectId
let refs = {}
// example of accessing current selected ref
const handleClick = () => {
if (refs[selectedObjectId])
refs[selectedObjectId].current.click() // call some method
}
return (<>
<someContextProvider>
{arr.map((item, id) => {
<ChildComponent
props={item}
key={id}
ref={refs[id]}
>
})}
<someContextProvider />
</>)
}
Solution
Like Drew commented in Medets answer, the only solution is to create an array of refs and access the desired one by simply matching the index of the ChildElement with the index of the ref array, as we can see here. There's no way we found to actually move a ref between objects, but performance cost for doing this should not be relevant.

useRef in a dynamic context, where the amount of refs is not constant but based on a property

In my application I have a list of "chips" (per material-ui), and on clicking the delete button a delete action should be taken. The action needs to be given a reference to the chip not the button.
A naive (and wrong) implementation would look like:
function MemberList(props) {
const {userList} = this.props;
refs = {}
for (const usr.id of userList) {
refs[usr.id] = React.useRef();
}
return <>
<div >
{
userList.map(usr => {
return <UserThumbView
ref={refs[usr.id]}
key={usr.id}
user={usr}
handleDelete={(e) => {
onRemove(usr, refs[usr.id])
}}
/>
}) :
}
</div>
</>
}
However as said this is wrong, since react expects all hooks to always in the same order, and (hence) always be of the same amount. (above would actually work, until we add a state/any other hook below the for loop).
How would this be solved? Or is this the limit of functional components?
Refs are just a way to save a reference between renders. Just remember to check if it is defined before you use it. See the example code below.
function MemberList(props) {
const refs = React.useRef({});
return (
<div>
{props.userList.map(user => (
<UserThumbView
handleDelete={(e) => onRemove(user, refs[user.id])}
ref={el => refs.current[user.id] = el}
key={user.id}
user={user}
/>
})}
</div>
)
}

react dynamically add children to jsx/component

Situation
I'm attempting to render a component based on template jsx pieces. Reason being because I have a few of these situations in my app but with subtle customizations and I'd rather leave the business logic for the customizations in the respective component, not the factory.
Example
Parent Template
<div></div>
Child Template
<p></p>
In the render function I want to add n child components to the parent. So if n=4 then I would expect output like
<div>
<p></p>
<p></p>
<p></p>
<p></p>
</div>
I tried using parentTemplate.children.push with no avail because the group template is still JSX at this point and not yet a rendered component. How can I accomplish this task using jsx + react ?
Extra
Here is what my actual code looks like so far
render() {
let groupTemplate = <ListGroup></ListGroup>
let itemTemplate = (item) => {
return <ListGroup.Item>{item.name}</ListGroup.Item>;
}
if (this.props.itemTemplate) itemTemplate = this.props.itemTemplate;
let itemsJsx;
this.props.navArray.forEach((item) => {
groupTemplate.children.push(itemTemplate(item))
});
return (
[groupTemplate]
);
}
From the parent:
const groupTemplate = <ListGroup className='custom-container-classes'></ListGroup>
const itemTemplate = <ListGroup.Item className='customized-classes'></ListGroup.Item>
<NavigationFactory groupTemplate={groupTemplate} itemTemplate={itemTemplate}>
</NavigationFactory>
You could use render props, which is the best way to achieve something like that:
The parent:
<NavigationFactory
itemTemplate={itemTemplate}
render={children => <ListGroup className='custom-container-classes'>
{children}
</ListGroup>}
/>
The Child:
render() {
this.props.render(this.props.navArray.map(item => <ListGroup.Item key={item.id}>
{item.name}
</ListGroup.Item>))
}
This will let you define the container on your calling function and enables you to create the chidlren as you want.
Hope this helps.
Unclear on your question but I have attempted to figure out based on your code on what you are trying to do, see if it helps, else I'll delete my answer.
The mechanism of below code working is that it takes in groupTemplate which in your case you'll be passing in as a prop which then takes children within it and returns them composed all together. There's default children group template which you can use if no related prop is passed and you have a mapper function within the render which determines which template to use and renders n number of times based on navArray length.
componentDidMount() {
// setup default templates
if (!this.props.navButtonContainerTemplate) {
this.prop.navButtonContainerTemplate = (items) => {
return <ListGroup>{items}</ListGroup>
}
}
if (!this.props.navButtonTemplate) {
this.props.navButtonTemplate = (item) => {
return (
<ListGroup.Item>
{item.name}
</ListGroup.Item>
)
}
}
}
render() {
const itemsJsx = this.props.navArray.map(obj => {
const itemJsx = this.props.navButtonTemplate(obj)
return itemJsx;
});
return this.props.navButtonContainerTemplate(itemsJsx);
}
Pass the contents as regular children to the ListGroup via standard JSX nesting:
render() {
let itemTemplate = (item) => {
return <ListGroup.Item>{item.name}</ListGroup.Item>;
}
if (this.props.itemTemplate) itemTemplate = this.props.itemTemplate;
return (
<ListGroup>
{this.props.navArray.map(item => (
itemTemplate(item)
))}
</ListGroup>
);
}

functions vs class components to handle events and attach arguments

I am trying to choose which is the most optimized way to write a React component that has to handle an event and send some data.
Hi I am tinkering and trying to get comfortable writing react components.
This piece of code fetches some array and makes buttons out of it.
https://codepen.io/daifuco/pen/OdxWYZ
App is just the main component, nothing special about it.
My question: As you can see in the working code, Header + GenreButton:
class Header extends Component {
render() {
return (
<div clasName="footer">
{this.props.data.map((genre)=>
<GenreButton clicky={this.props.clickytoTop}genre={genre}/>
)}
</div>
)
}
}
class GenreButton extends Component {
handleClick = () => {
this.props.clicky(this.props.genre)
}
render() {
return (
<div className="genreButton" onClick={this.handleClick}>{this.props.genre}</div>
)
}
}
has the same result as Header2, which is just a functional component, but as far as I know , Header2 creates a callback every time it renders each div.
function Header2(props) {
return (
<div clasName="footer">
{props.data.map((genre)=>
<div className="genreButton" onClick={()=>props.clickytoTop(genre)}>
{genre}
</div>)}</div>
)
}
So, I understand that Header2 is not the optimal way to design it?
are Header + GenreButton more optimized? Is there any way to improve it?
If you ever have the choice, it is usually preferable to use a stateless component, as they do not have to manage a state which would slow down your application.
To choose between one or the other, if your component needs to re-render itself, use a class, if not, use a function.
Your GenreButton does not need to be a class component either in this case :
const Header = ({ data, clickytoTop }) => ( //Deconstructs your props
<div clasName="footer">
{data.map((genre) =>
<GenreButton clicky={clickytoTop} genre={genre} />
)}
</div>
)
To avoid creating a new function everytime in your render, you could use a decorated arrow function, that you will bind this way :
const GenreButton = ({ clicky, genre }) => <div className="genreButton" onClick={clicky(genre)}>{genre}</div>
The function passed in the header props will now have the following declaration, to handle both sets of parameters :
clickFunction = genre => ev => {
}
When calling clicky(genre) it will return another function capable of accepting your click event.
<Header clickytoTop={this.clickFunction}/>
Second version (same result) :
const Header2 = ({ data, clickytoTop }) => ( //Deconstructs your props
<div clasName="footer">
{data.map(genre =>
<div className="genreButton" onClick={clickytoTop(genre)}>{genre}</div>
)}
</div>
)

React - Decorating Children and Handling Keys

I'm writing a small component that decorates some children. Given the following sample:
const Child = props => <div>{props.name}</div>;
const Parent = props => {
let wrappedChildren = React.Children.map(props.children, c => {
return (<Decorator key={c.key}>
{c}
</Decorator>);
});
return (<div>
{wrappedChildren}
</div>);
}
const Consumer = props => {
let children = [0, 1, 2].map(num => {
return <Child key={num} name={num} />;
});
return <Parent>{children}</Parent>;
});
In this code, I'm wanting to take each child and decorate it with some wrapping container or some behaviour. Forgetting for the moment that there may only be one child, I need to give each instance a key.
Currently I'm assuming that each child does have a key which isn't fantastic, lifting it off the child and applying it to the Decorator directly.
Is this the "correct" way of doing this? Is there a better way?
I think your approach is fine. And you do need the key on the top level. Use the child's key, if it is there. If not, fall back to the index, as React recommends:
When you don't have stable IDs for rendered items, you may use the item index as a key as a last resort.
Be advised though:
We don't recommend using indexes for keys if the items can reorder, as that would be slow.
Source: React Docs about keys
const Child = props => <div>{props.name}</div>;
const Parent = props => {
let wrappedChildren = React.Children.map(props.children, (c, i) => {
const key = c.key ? `key-${c.key}` : `index-${i}`
return (
<Decorator key={key}>
{c}
</Decorator>
);
});
return (
<div>
{wrappedChildren}
</div>
);
};
const Consumer = () => {
let children = [ 0, 1, 2 ].map(num => {
return <Child key={num} name={num} />;
});
return <Parent>{children}</Parent>;
};
That would work with the current version of React, 15.6.1 (and probably with prior versions as well). However, there is a slightly better way to achieve your goal with a small tweak, which would be delegating the lifting on a prop, rather than using directly the key.
The reason is that the concept of a key is something that is controlled by React internals before your component gets created, so it falls into the implementation details category, with the consequence that future versions of React could break your code without you even noticing it.
Instead, you can use a prop on your Child component, for instance id, based on your assumption that each child does have some sort of unique value. Your code would result in:
const Child = props => <div>{props.name}</div>;
const Parent = props => {
return React.Children.map(props.children, c => {
return (<Decorator key={c.props.id}>
{c}
</Decorator>);
});
return (<div>
{wrappedChildren}
</div>);
}
const Consumer = props => {
let children = [0, 1, 2].map(num => {
return <Child id={num} name={num} />;
});
return <Parent>{children}</Parent>;
});
If you have a more complex structure and want to avoid passing props individually, the parent component should held the responsibility of generating unique ids for the children. You can follow the ideas explained in this answer https://stackoverflow.com/a/29466744/4642844.
Those are the steps I'd follow:
Implement your own utility function or use an existing browser library to generate unique ids.
Implement componentWillMount in your Parent component to create an array of unique ids. You can use an internal member class variable instead of local state. It would be something like:
componentWillMount() {
this.uuids = React.Children.map(() => uuid());
}
Inside your Parent render, assign each key to the Decorator component in the following way.
render() {
return React.Children.map(props.children, (c, i) => {
return (<Decorator key={this.uuids[i]}>
{c}
</Decorator>);
});
}
Some useful links:
http://mxstbr.blog/2017/02/react-children-deepdive/
https://medium.com/#dan_abramov/react-components-elements-and-instances-90800811f8ca
Javascript Array.map(fn) passes actually 3 arguments to fn second being an index. So, what you need to do is:
let wrappedChildren = props.children.map((c, i) => <Decorator key={i}>{c}</Decorator>);

Resources