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.

2

2 Answers 2

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)}
    />
}
Sign up to request clarification or add additional context in comments.

1 Comment

I believe there's a typo, probably it should be textInputRefs and not textInputs
10

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>

9 Comments

I can't compile this, I get Typescript error: Expected 0 arguments, but got 1. for this line: React.createRef(null)
Sorry, yes, just remove null argument :) I've updated the answer
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>'.
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-33nw5
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>'
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.