Davtho1983
Davtho1983

Reputation: 3954

React Hooks onChange not accepting input

I have a weird bug that only happens some of the time - onChange fires but does not change the value. Then if I click outside of the input with the onChange function, then click back inside the input box, the onChange function starts working.

The onChange function is like so:

const handleBarAmountChange = (event) => {
    let newWidthAmount = event.target.value / 10;
    setNewWidth(newWidthAmount);
    setNewBarAmount(event.target.value);
};

A parent div is using a ref with useRef that is passed to this function:

import { useEffect, useState } from 'react';
const useMousePosition = (barRef, barInputRef, barContainerRef) => {
    const [ mouseIsDown, setMouseIsDown ] = useState(null);

    useEffect(() => {
        const setMouseDownEvent = (e) => {
            if (e.which == 1) {
                if (barContainerRef.current.contains(e.target) && !barInputRef.current.contains(e.target)) {
                    setMouseIsDown(e.clientX);
                } else if (!barInputRef.current.contains(e.target)) {
                    setMouseIsDown(null);
                }
            }
        };

        window.addEventListener('mousemove', setMouseDownEvent);
        return () => {
            window.removeEventListener('mousemove', setMouseDownEvent);
        };
    }, []);
    return { mouseIsDown };
};

Is the onChange conflicting somehow with the eventListener?

How do I get round this?

Upvotes: 0

Views: 1265

Answers (2)

Matt Carlotta
Matt Carlotta

Reputation: 19772

There were a few syntax errors and missing hook dependencies that were the cause of your bugs. However, you can simplify your code quite a bit with a few tweaks.

When using state that relies upon other state, I recommend lumping it into an object and using a callback function to synchronously update it: setState(prevState => ({ ...prevState, example: "newValue" }). This is similar to how this.setState(); works in a class based component. By using a single object and spreading out it properties ({ ...prevState }), we can then overwrite one of its properties by redefining one of them ({ ...prevState, newWidth: 0 }). This way ensures that the values are in sync with each other.

The example below follows the single object pattern mentioned above, where newWidth, newBarAmount and an isDragging are properties of a single object (state). Then, the example uses setState to update/override the values synchronously. In addition, the refs have been removed and allow the bar to be dragged past the window (if you don't want this, then you'll want to confine it within the barContainerRef as you've done previously). The example also checks for a state.isDragging boolean when the user left mouse clicks and holds on the bar. Once the left click is released, the dragging is disabled.

Here's a working example:

Edit Bar & Input Updates


components/Bar/index.js

import React, { useEffect, useState, useCallback } from "react";
import PropTypes from "prop-types";
import "./Bar.css";

function Bar({ barName, barAmount, colour, maxWidth }) {
  const [state, setState] = useState({
    newWidth: barAmount / 2,
    newBarAmount: barAmount,
    isDragging: false
  });

  // manual input changes
  const handleBarAmountChange = useCallback(
    ({ target: { value } }) => {
      setState(prevState => ({
        ...prevState,
        newWidth: value / 2,
        newBarAmount: value
      }));
    },
    []
  );

  // mouse move
  const handleMouseMove = useCallback(
    ({ clientX }) => {
      if (state.isDragging) {
        setState(prevState => ({
          ...prevState,
          newWidth: clientX > 0 ? clientX / 2 : 0,
          newBarAmount: clientX > 0 ? clientX : 0
        }));
      }
    },
    [state.isDragging]
  );

  // mouse left click hold
  const handleMouseDown = useCallback(
    () => setState(prevState => ({ ...prevState, isDragging: true })),
    []
  );

  // mouse left click release
  const handleMouseUp = useCallback(() => {
    if (state.isDragging) {
      setState(prevState => ({
        ...prevState,
        isDragging: false
      }));
    }
  }, [state.isDragging]);

  useEffect(() => {
    window.addEventListener("mousemove", handleMouseMove);
    window.addEventListener("mouseup", handleMouseUp);

    return () => {
      window.removeEventListener("mousemove", handleMouseMove);
      window.removeEventListener("mouseup", handleMouseUp);
    };
  }, [handleMouseMove, handleMouseUp]);

  return (
    <div className="barContainer">
      <div className="barName">{barName}</div>
      <div
        style={{ cursor: state.isDragging ? "grabbing" : "pointer" }}
        onMouseDown={handleMouseDown}
        className="bar"
      >
        <svg
          width={state.newWidth > maxWidth ? maxWidth : state.newWidth}
          height="40"
          fill="none"
          xmlns="http://www.w3.org/2000/svg"
          colour={colour}
        >
          <rect width={state.newWidth} height="40" fill={colour} />
        </svg>
      </div>
      <div className="barAmountUnit">£</div>
      <input
        className="barAmount"
        type="number"
        value={state.newBarAmount}
        onChange={handleBarAmountChange}
      />
    </div>
  );
}

// default props (will be overridden if defined)
Bar.defaultProps = {
  barAmount: 300,
  maxWidth: 600
};

// check that passed in props match patterns below
Bar.propTypes = {
  barName: PropTypes.string,
  barAmount: PropTypes.number,
  colour: PropTypes.string,
  maxWidth: PropTypes.number
};

export default Bar;

Upvotes: 1

Fraction
Fraction

Reputation: 12993

React uses SyntheticEvent and Event Pooling, from the doc:

Event Pooling

The SyntheticEvent is pooled. This means that the SyntheticEvent object will be reused and all properties will be nullified after the event callback has been invoked. This is for performance reasons. As such, you cannot access the event in an asynchronous way.

You could call event.persist() on the event or store the value in a new variable and use it as follows:

const handleBarAmountChange = (event) => {
  // event.persist(); 
  // Or
  const { value } = event.target;

  let newWidthAmount = value / 10;
  setNewWidth(newWidthAmount);
  setNewBarAmount(value);
};

Upvotes: 1

Related Questions