0

Let's say I have a component CounterChanger. It is responsible for changing a counter when clicked based on a function which accepts the previous value.

interface CounterChangerProps {
   valueChangingFunc: (value: number) => number
   value: number
   // Same as a hook state setter
   onChange: (newValue: number | ((value: number) => number)) => void)
}

Inside the implementation, CounterChangerProps simply passes valueChangingFunc into onChange to take advantage of the signature.

const CounterChanger = ({ valueChangingFunc, value, onChange }) => {
  return <Button 
           onClick={() => onChange(previous => valueChangingFunc(previous))}
         >
           Click me {value}
         </Button>
}

The following patterns works fine if this component is isolated, and the setValue is passed directly into onChange.


const CounterWrapper = () => {
  const [value, setValue] = useState(1)
 // Double it
 return <CounterChanger 
           value={value} 
           onChange={setValue} 
           valueChangingFunc={(previous) => previous*2} 
        />

}

But what if I want the outside world to know of the value inside CounterWrapper? I want CounterWrapper to implement an onChange which doesn't need to worry about the callback variation.

interface CounterWrapperProps {
   onChange: (value: number) => void
   value: number
}

How can I make this happen? I can't replace setValue for onChange because it doesn't implement the necessary callback variation.

What I have come up with

The best implementation I could come up with was this"

const handleOnChange: React.Dispatch<React.SetStateAction<number>> = (
    newValue
  ) => {
    setCurrentValue((previous) => {
      if (typeof newValue === 'number') {
        const result = newValue
        onChange?.(newValue)
        return result
      } else {
        const result = newValue(previous)
        onChange?.(result)
        return result
      }
    })
  }

Is this okay to do? It feels like setter abuse to me.

Update: It is abuse

What I don't want to do

A tempting solution is to use effects for this.

useEffect(() => {
 onChange(value)
}, [value])

I don't want to do this because it makes me go down a rabbit hole of syncing state with effects which is Not The React Way.

Slava Knyazev
  • 5,377
  • 1
  • 22
  • 43

0 Answers0