Home > OS >  How to properly constrain a React state variable based on another one?
How to properly constrain a React state variable based on another one?

Time:01-03

const nums: string[] = [...];

const [left, setLeft] = useState(0);
const [right, setRight] = useState(1);
useEffect(() => { if (right < left) setRight(left); }, [left]);
useEffect(() => { if (left > right) setLeft(right); }, [right]);

// this throws if right < left:
const slice = nums.slice(left, right);
const sum = slice.reduce((s, c) => s   c);
return <p>Sum: {sum}</p>;

If right is changed to something smaller than left, this code will crash on the next render, despite the constraint applied in the effect, because:

  • the effect runs after the faulty render
  • even if it ran immediately, setRight only changes the value of right in the next render

I can think of various ways to avoid or work around this, but what would you say is the proper/idiomatic way to do something like this?

CodePudding user response:

Setting state in an effect is usually a code smell. You need to either:

  • Constraint it in the handler that sets this to a "bad" value in the first place, before it even gets set.
  • Or allow is to be set to a "bad" value and just clamp them in render, and only ever use that value to read from.
const [left, setLeft] = useState(0);
const [right, setRight] = useState(1);

// ...

const clampedLeft = Math.min(left, right)
const clampedRight = Math.max(0, left)

CodePudding user response:

Since both states are dependent on each other, I would use a reducer to contain the logic for both an constrain the actual state values:

const reducer = ({ left, right }, { l = left, r = right}) => ({
  left: Math.min(l, r)
  right: Math.max(l, r)
})

// init
const [{ left, right }, dispatch] = useReducer(reducer, { left: 0, right: 1 });

// update
const updateLeft = left => dispatch({ left })
const updateRight = right => dispatch({ right })

In addition, the problem is not with the Array.slice() method, which would return an empty array, if left is greater than right, but with the Array.reduce(), which throws an error when the array is empty, and there's no initial value. A simple solution is to add an initial value:

const sum = slice.reduce((s, c) => s   c, 0); // <- 0 initial value

CodePudding user response:

You don't need to use the effect hook at all for this.

const slice = nums.slice(left, right < left ? left : right);

Your edit doesn't make any sense. Rather, it will raise XY problem. Why are you so over complicating the scenario?

Just slice the nums based on the left and right value defining a condition. And then, the handler for updating left and right will do the work. Whenever the left and right value changes the code will work for you on the next render (after updating the left/right values). So, just keep it simple.

const slice = nums.slice(left, Math.min(left, right))
  • Related