Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Dynamically create React.Dispatch instances in FunctionComponents

Tags:

reactjs

How can I create an array of input elements in react which are being "watched" without triggering the error for using useState outside the body of the FunctionComponent?

if I have the following (untested, simplified example):

interface Foo {
  val: string;
  setVal: React.Dispatch<React.SetStateAction<string>>;
}
function MyReactFunction() {
  const [allVals, setAllVals] = useState<Foo[]>([])
  const addVal = () => {
    const [val, setVal] = useState('')
    setAllVals(allVals.concat({val, setVal}))
  }
  return (
    <input type="button" value="Add input" onClick={addVal}>
    allVals.map(v => <li><input value={v.val} onChange={(_e,newVal) => v.setVal(newVal)}></li>)
  )
}

I will get the error Hooks can only be called inside of the body of a function component.

How might I dynamically add "watched" elements in the above code, using FunctionComponents?

Edit

I realise a separate component for each <li> above would be able to solve this problem, but I am attempting to integrate with Microsoft Fluent UI, and so I only have the onRenderItemColumn hook to use, rather than being able to create a separate Component for each list item or row.

Edit 2

in response to Drew Reese's comment: apologies I am new to react and more familiar with Vue and so I am clearly using the wrong terminology (watch, ref, reactive etc). How would I rewrite the code example I provided so that there is:

  1. An add button
  2. Each time the button is pressed, another input element is added.
  3. Each time a new value is entered into the input element, the input element shows the value
  4. There are not excessive or unnecessary re-rendering of the DOM when input elements have their value updated or new input element is added
  5. I have access to all the values in all the input elements. For example, if a separate submit button is pressed I could get an array of all the string values in each input element. In the code I provided, this would be with allVals.map(v => v.val)
like image 283
Brent Avatar asked Sep 03 '25 15:09

Brent


1 Answers

const [val, setVal] = useState('') is not allowed. The equivalent effect would be just setting value to a specific index of allVals.

Assuming you're only adding new items to (not removing from) allVals, the following solution would work. This simple snippet just shows you the basic idea, you'll need to adapt to your use case.

function MyReactFunction() {
  const [allVals, setAllVals] = useState<Foo[]>([])
  const addVal = () => {
    setAllVals(allVals => {
      // `i` would be the fixed index of newly added item
      // it's captured in closure and would never change
      const i = allVals.length
      const setVal = (v) => setAllVals(allVals => {
        const head = allVals.slice(0, i)
        const target = allVals[i]
        const tail = allVals.slice(i+1)
        const nextTarget = { ...target, val: v }
        return head.concat(nextTarget).concat(tail)
      })

      return allVals.concat({
        val: '',
        setVal,
      })
    })
  }
  return (
    <input type="button" value="Add input" onClick={addVal} />
    {allVals.map(v => 
      <li><input value={v.val} onChange={(_e,newVal) => v.setVal(newVal)}></li>
    )}
  )
}
like image 163
hackape Avatar answered Sep 05 '25 07:09

hackape