Home > front end >  Dynamically create React.Dispatch instances in FunctionComponents
Dynamically create React.Dispatch instances in FunctionComponents

Time:09-22

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)

CodePudding user response:

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>
    )}
  )
}

CodePudding user response:

React hooks cannot be called in callbacks as this breaks the Rules of Hooks.

From what I've gathered you want to click the button and dynamically add inputs, and then be able to update each input. You can add a new element to the allVals array in the addVal callback, simply use a functional state update to append a new element to the end of the allVals array and return a new array reference. Similarly, in the updateVal callback use a functional state update to map the previous state array to a new array reference, using the index to match the element you want to update.

interface Foo {
  val: string;
}

function MyReactFunction() {
  const [allVals, setAllVals] = useState<Foo[]>([]);
  
  const addVal = () => {
    setAllVals((allVals) => allVals.concat({ val: "" }));
  };

  const updateVal = (index: number) => (e: any) => {
    setAllVals((allVals) =>
      allVals.map((el, i) =>
        i === index
          ? {
              ...el,
              val: e.target.value
            }
          : el
      )
    );
  };

  return (
    <>
      <input type="button" value="Add input" onClick={addVal} />
      {allVals.map((v, i) => (
        <li key={i}>
          <input value={v.val} onChange={updateVal(i)} />
        </li>
      ))}
    </>
  );
}

Edit dynamically-create-react-dispatch-instances-in-functioncomponents

  • Related