In React-Native + hooks: updating parent's value multiple times in a row from a child component

Viewed 241

The objective is to create an input component where the user can click a button, which if held, would fire an action multiple times (e.g. think here of a + button, which would increment continuously the parent's value). The firing stops when the button is released.

The below code is the minimum code to highlight both the needed infrastructure (parent/child) and the issue I am facing.

The issue: when pressing, the count value is incremented only once, although the incrementor is fired multiple times. The parent owns both the count value and the onChange callback (along the <TextInput> pattern)

I believe it comes from that the value in incr() is set to the value at MyInput render time; As long as the button is not released, MyInput is not re-rendered, hence the incrementor is still bound to that original value.

Parent Component:

const Screen = ({}) => {
  const [ count, setCount ] = useState(0);
  
  const onChange = (value) => {
    setCount(value);
  }
  
  return (
    <View style={{ flex:1, justifyContent:'center', alignItems:'center' }}>
      <Text>Parent: [{count}]</Text>
      <MyInput value={count} onChange={onChange}/>
    </View>
  )
}

Child Component: MyInput follows <TextInput> and similar patterns where the value is owned by the parent and updated via an onChange callback

export const MyInput = ({ value, onChange }) => {
  const [ timer, setTimer ] = useState(null);

  const incr = () => {
    const nv = value + 1;
    console.warn(new Date(), `nv: ${nv}`);
    onChange(value + 1);
  }

  const onPressIn = () => {
    setTimer(setInterval(incr, 10));
    // or calling incr() twice here, would have the same effect
  };

  const onPressOut = () => {
    clearInterval(timer);
  };

  return (
    <View>
      <Text>Child: {value}</Text>
      <TouchableOpacity
        onPressIn={onPressIn}
        onPressOut={onPressOut}>
        <Text>+</Text>
      </TouchableOpacity>
    </View>
  )
};

I could partially solve the situation if I use a local state copy of the value (say [ localValue, setLocalValue ] = useState(value) in MyInput, and use setLocalValue(xxx => xxx+1) ... however it would only update the local value and not the parent's one.

Maybe force refreshing would be a (bad) option too?

I tried around with useCallback/... however, as it seems, I would need to release the button before the parent gets re-rendered (or something like that).

The constraints are: functional-components & hooks and the value/onChange parent owned props

Any idea how I could solve the above situation? thanks in advance for any hints

1 Answers

import {useEffect}from 'react'

export const MyInput = ({ value, onChange }) => {
  const [timer, setTimer] = useState(null);

const incr = () => {
    const nv = value + 1;
    // console.warn(new Date(), `nv: ${nv}`);
    onChange(value + 1);
}
useEffect(() => {
    incr()
    setTimer(setInterval(incr, 10));
}, [value])
const onPressIn = () => {
    // setTimer(setInterval(incr, 10));
    // or calling incr() twice here, would have the same effect
    onChange(value + 1);
};

const onPressOut = () => {
    clearInterval(timer);
};
  return (
    <View>
      <Text>Child: {value}</Text>
      <TouchableOpacity
        onPressIn={onPressIn}
        onPressOut={onPressOut}>
        <Text>+</Text>
      </TouchableOpacity>
    </View>
  )
};

Related