React creating dynamically refs with Typescript
Asked Answered
H

2

6

I have a table list where each row has a menu button, for which I need a ref. I am using react mui in my project and it's menu. I have tried creating the refs like this:

const {rows} = props;
const refs = Array.from({length: rows.length}, a => React.useRef<HTMLButtonElement>(null));

And then tried to use the inside the map function like this on each button:

<Button
   ref={refs[index]}
   aria-controls="menu-list-grow"
   aria-haspopup="true"
   onClick={() => handleToggle(row.id)}
>Velg
</Button>
  <Popper open={!!checkIfOpen(row.id)} anchorEl={refs[index].current} keepMounted transition disablePortal>
    {({TransitionProps, placement}) => (
       <Grow
        {...TransitionProps}
        style={{transformOrigin: placement === 'bottom' ? 'center top' : 'center bottom'}}>
       <Paper id="menu-list-grow">
         <ClickAwayListener onClickAway={(e) => handleClose(e, refs[index].current)}>
          <MenuList>                                                        
            <MenuItem
             onClick={(e) => handleClose(e, refs[index].current)}>Profile</MenuItem>
           <MenuItem onClick={(e) => handleClose(e, refs[index].current)}>My account</MenuItem>
           <MenuItem onClick={(e) => handleClose(e, refs[index].current)}>Logout</MenuItem>
        </MenuList>
      </ClickAwayListener>
    </Paper>
  </Grow>
 )}
</Popper>

But, then I get an error:

React Hook "React.useRef" cannot be called inside a callback. React Hooks must be called in a React function component or a custom React Hook function react-hooks/rules-of-hooks

How can I do this dynamically, so that I can use refs inside the map function. I have tried with the suggestion in the answers, but I couldn't get it to work. Here is the codesandbox of the example.

Hagio answered 12/9, 2019 at 7:27 Comment(2)
Possible duplicate of How target DOM with react useRef in mapJury
@Jury this one is more Typescript orientedAvellaneda
C
9

useRef is not exactly the same as React.createRef. it's better to call it useInstanceField :)

So, your code could be a bit another.

First step: we use useRef to save the array of refs:

const {rows} = props;
const refs = useRef(Array.from({length: rows.length}, a => React.createRef()));

Then, in your map function we save each ref to its index in the refs array:

<Button
   ref={refs.current[index]}
   aria-controls="menu-list-grow"
   aria-haspopup="true"
   onClick={() => handleToggle(row.id)}
>Velg
</Button>
  <Popper open={!!checkIfOpen(row.id)} anchorEl={refs.current[index].current} keepMounted transition disablePortal>
    {({TransitionProps, placement}) => (
       <Grow
        {...TransitionProps}
        style={{transformOrigin: placement === 'bottom' ? 'center top' : 'center bottom'}}>
       <Paper id="menu-list-grow">
         <ClickAwayListener onClickAway={(e) => handleClose(e, refs.current[index].current)}>
          <MenuList>                                                        
            <MenuItem
             onClick={(e) => handleClose(e, refs.current[index].current)}>Profile</MenuItem>
           <MenuItem onClick={(e) => handleClose(e, refs.current[index].current)}>My account</MenuItem>
           <MenuItem onClick={(e) => handleClose(e, refs.current[index].current)}>Logout</MenuItem>
        </MenuList>
      </ClickAwayListener>
    </Paper>
  </Grow>
 )}
</Popper>

if your length is changed, you should process it in useEffect to change the length of refs

You also can use another way:

1) Create an array of refs, but without React.createRef:

const {rows} = props;
const refs = useRef(new Array(rows.length));

In map we use ref={el => refs.current[index] = el} to store ref

<Button
   ref={el => refs.current[index] = el}
   aria-controls="menu-list-grow"
   aria-haspopup="true"
   onClick={() => handleToggle(row.id)}
>Velg
</Button>
  <Popper open={!!checkIfOpen(row.id)} anchorEl={refs.current[index].current} keepMounted transition disablePortal>
    {({TransitionProps, placement}) => (
       <Grow
        {...TransitionProps}
        style={{transformOrigin: placement === 'bottom' ? 'center top' : 'center bottom'}}>
       <Paper id="menu-list-grow">
         <ClickAwayListener onClickAway={(e) => handleClose(e, refs.current[index])}>
          <MenuList>                                                        
            <MenuItem
             onClick={(e) => handleClose(e, refs.current[index])}>Profile</MenuItem>
           <MenuItem onClick={(e) => handleClose(e, refs.current[index])}>My account</MenuItem>
           <MenuItem onClick={(e) => handleClose(e, refs.current[index])}>Logout</MenuItem>
        </MenuList>
      </ClickAwayListener>
    </Paper>
  </Grow>
 )}
</Popper>
Chromite answered 12/9, 2019 at 7:40 Comment(9)
I can't compile this, I get Typescript error: Expected 0 arguments, but got 1. for this line: React.createRef(null) Hagio
Sorry, yes, just remove null argument :) I've updated the answerChromite
I have done that as well, but then I get: Type 'RefObject<{}>' is not assignable to type '((instance: HTMLButtonElement | null) => void) | RefObject<HTMLButtonElement> | null | undefined'. Type 'RefObject<{}>' is not assignable to type 'RefObject<HTMLButtonElement>'.Hagio
It seems typescript doesn't allow to use such way. So I've made an example of this case with TypeScript codesandbox.io/s/wizardly-goldstine-33nw5Chromite
I tried to implement your solution into my example, but then I get the error: Type 'HTMLButtonElement | null' is not assignable to type 'never'. Type 'null' is not assignable to type 'never'. And if I change refs to this: const refs = React.useRef<Array<React.RefObject<HTMLButtonElement> | null>>([]);, then I get error: Type 'HTMLButtonElement | null' is not assignable to type 'RefObject<HTMLButtonElement> | null'. Property 'current' is missing in type 'HTMLButtonElement' but required in type 'RefObject<HTMLButtonElement>'Hagio
yes, I have added it to the question as well, here it is: codesandbox.io/embed/cranky-hawking-qy64cHagio
I've fixed types of useRef in your example: codesandbox.io/s/friendly-meadow-fdpfe const refs = React.useRef<Array<HTMLButtonElement | null>>([]);Chromite
I have done the same, but the popper with the menu is still not working, it seems as it can't recognize to which button the ref refers to.Hagio
It wasn't the problem with useRef. I've just removed keepMounted prop from popper component: check it here codesandbox.io/s/friendly-meadow-fdpfeChromite
N
15

Here another option:

const textInputRefs = useRef<(HTMLDivElement | null)[]>([])

...

const onClickFocus = (event: React.BaseSyntheticEvent, index: number) => {
    textInputRefs.current[index]?.focus()
};

...
{items.map((item, index) => (
    <textInput
        inputRef={(ref) => textInputRefs.current[index] = ref}
    />
    <Button
        onClick={event => onClickFocus(event, index)}
    />
}
Newsome answered 3/5, 2020 at 20:4 Comment(1)
I believe there's a typo, probably it should be textInputRefs and not textInputsBute
C
9

useRef is not exactly the same as React.createRef. it's better to call it useInstanceField :)

So, your code could be a bit another.

First step: we use useRef to save the array of refs:

const {rows} = props;
const refs = useRef(Array.from({length: rows.length}, a => React.createRef()));

Then, in your map function we save each ref to its index in the refs array:

<Button
   ref={refs.current[index]}
   aria-controls="menu-list-grow"
   aria-haspopup="true"
   onClick={() => handleToggle(row.id)}
>Velg
</Button>
  <Popper open={!!checkIfOpen(row.id)} anchorEl={refs.current[index].current} keepMounted transition disablePortal>
    {({TransitionProps, placement}) => (
       <Grow
        {...TransitionProps}
        style={{transformOrigin: placement === 'bottom' ? 'center top' : 'center bottom'}}>
       <Paper id="menu-list-grow">
         <ClickAwayListener onClickAway={(e) => handleClose(e, refs.current[index].current)}>
          <MenuList>                                                        
            <MenuItem
             onClick={(e) => handleClose(e, refs.current[index].current)}>Profile</MenuItem>
           <MenuItem onClick={(e) => handleClose(e, refs.current[index].current)}>My account</MenuItem>
           <MenuItem onClick={(e) => handleClose(e, refs.current[index].current)}>Logout</MenuItem>
        </MenuList>
      </ClickAwayListener>
    </Paper>
  </Grow>
 )}
</Popper>

if your length is changed, you should process it in useEffect to change the length of refs

You also can use another way:

1) Create an array of refs, but without React.createRef:

const {rows} = props;
const refs = useRef(new Array(rows.length));

In map we use ref={el => refs.current[index] = el} to store ref

<Button
   ref={el => refs.current[index] = el}
   aria-controls="menu-list-grow"
   aria-haspopup="true"
   onClick={() => handleToggle(row.id)}
>Velg
</Button>
  <Popper open={!!checkIfOpen(row.id)} anchorEl={refs.current[index].current} keepMounted transition disablePortal>
    {({TransitionProps, placement}) => (
       <Grow
        {...TransitionProps}
        style={{transformOrigin: placement === 'bottom' ? 'center top' : 'center bottom'}}>
       <Paper id="menu-list-grow">
         <ClickAwayListener onClickAway={(e) => handleClose(e, refs.current[index])}>
          <MenuList>                                                        
            <MenuItem
             onClick={(e) => handleClose(e, refs.current[index])}>Profile</MenuItem>
           <MenuItem onClick={(e) => handleClose(e, refs.current[index])}>My account</MenuItem>
           <MenuItem onClick={(e) => handleClose(e, refs.current[index])}>Logout</MenuItem>
        </MenuList>
      </ClickAwayListener>
    </Paper>
  </Grow>
 )}
</Popper>
Chromite answered 12/9, 2019 at 7:40 Comment(9)
I can't compile this, I get Typescript error: Expected 0 arguments, but got 1. for this line: React.createRef(null) Hagio
Sorry, yes, just remove null argument :) I've updated the answerChromite
I have done that as well, but then I get: Type 'RefObject<{}>' is not assignable to type '((instance: HTMLButtonElement | null) => void) | RefObject<HTMLButtonElement> | null | undefined'. Type 'RefObject<{}>' is not assignable to type 'RefObject<HTMLButtonElement>'.Hagio
It seems typescript doesn't allow to use such way. So I've made an example of this case with TypeScript codesandbox.io/s/wizardly-goldstine-33nw5Chromite
I tried to implement your solution into my example, but then I get the error: Type 'HTMLButtonElement | null' is not assignable to type 'never'. Type 'null' is not assignable to type 'never'. And if I change refs to this: const refs = React.useRef<Array<React.RefObject<HTMLButtonElement> | null>>([]);, then I get error: Type 'HTMLButtonElement | null' is not assignable to type 'RefObject<HTMLButtonElement> | null'. Property 'current' is missing in type 'HTMLButtonElement' but required in type 'RefObject<HTMLButtonElement>'Hagio
yes, I have added it to the question as well, here it is: codesandbox.io/embed/cranky-hawking-qy64cHagio
I've fixed types of useRef in your example: codesandbox.io/s/friendly-meadow-fdpfe const refs = React.useRef<Array<HTMLButtonElement | null>>([]);Chromite
I have done the same, but the popper with the menu is still not working, it seems as it can't recognize to which button the ref refers to.Hagio
It wasn't the problem with useRef. I've just removed keepMounted prop from popper component: check it here codesandbox.io/s/friendly-meadow-fdpfeChromite

© 2022 - 2024 — McMap. All rights reserved.