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.

Leff
  • 1,968
  • 24
  • 97
  • 201

2 Answers2

12

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)}
    />
}
Jöcker
  • 5,281
  • 2
  • 38
  • 44
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>
Nik
  • 2,170
  • 1
  • 14
  • 20
  • I can't compile this, I get Typescript error: ```Expected 0 arguments, but got 1.``` for this line: ```React.createRef(null) ``` – Leff Sep 12 '19 at 09:34
  • Sorry, yes, just remove null argument :) I've updated the answer – Nik Sep 12 '19 at 09:37
  • I have done that as well, but then I get: ```Type 'RefObject<{}>' is not assignable to type '((instance: HTMLButtonElement | null) => void) | RefObject | null | undefined'. Type 'RefObject<{}>' is not assignable to type 'RefObject'.``` – Leff Sep 12 '19 at 09:47
  • It seems typescript doesn't allow to use such way. So I've made an example of this case with TypeScript https://codesandbox.io/s/wizardly-goldstine-33nw5 – Nik Sep 12 '19 at 10:43
  • 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 | null>>([]);```, then I get error: ```Type 'HTMLButtonElement | null' is not assignable to type 'RefObject | null'. Property 'current' is missing in type 'HTMLButtonElement' but required in type 'RefObject'``` – Leff Sep 12 '19 at 12:59
  • yes, I have added it to the question as well, here it is: https://codesandbox.io/embed/cranky-hawking-qy64c – Leff Sep 12 '19 at 14:23
  • I've fixed types of useRef in your example: https://codesandbox.io/s/friendly-meadow-fdpfe `const refs = React.useRef>([]);` – Nik Sep 12 '19 at 19:49
  • 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. – Leff Sep 12 '19 at 20:39
  • It wasn't the problem with `useRef`. I've just removed `keepMounted` prop from popper component: check it here https://codesandbox.io/s/friendly-meadow-fdpfe – Nik Sep 12 '19 at 21:12