Skip to content
Advertisement

React: multiple sliders sharing a common state

I have two sliders, each with an event listener. Using your mouse to adjust one slider should update a shared state, which should then cause a change in the second slider. However, I only want the event listener for the first slider to fire. In the code I have below, the second slider’s event listener is also firing, as can be seen by console prints of the form “Slider y was dragged…” when dragging the first slider. Is there some way to filter out the second slider’s event listener firing?

function MySlider(props) {
  const sliderRef = useRef();

  useEffect(() => {
    const slider = sliderRef.current;
    const onChange = (evt) => {
      const v = evt.detail.value;
      console.log("Slider " + props.name + " was dragged to: " + v);
      props.update(v);
    }
    slider?.addEventListener('change', onChange);
    return () => {
      slider?.removeEventListener('change', onChange);
    }
  }, []);

  return (
    <toolcool-range-slider
      id={props.name}
      min={props.min}
      max={props.max}
      value={props.value}
      step="1"
      ref={sliderRef}
    />
  );
}

class App extends Component {
  constructor(props) {
    super(props);
    this.state = {
      x: 10,
      y: 5,
    };
  }

  updateX(x) {
    this.setState({
      x: x,
      y: Math.ceil(x/2),
    });
  }

  updateY(y) {
    this.setState({y: y});
  }

  render() {
    const x = this.state.x;
    const y = this.state.y;
    const minY = Math.ceil(x/2);
    const maxY = x;
    return (
      <div>
        <table><tbody>
          <tr>
            <td>Choose x:</td>
            <td style={{padding:10}}>
              <MySlider name="x" min={10} max={50}
                value={this.state.x}
                update={(x) => this.updateX(x) }
              />
            </td>
            <td>{x}</td>
          </tr>
          <tr>
            <td>Choose a y in the range [x/2, x]:</td>
            <td style={{padding:10}}>
              <MySlider name="y" min={minY} max={maxY}
                value={this.state.y}
                update={(y) => this.updateY(y) }
              />
            </td>
            <td>{y}</td>
          </tr>
        </tbody></table>
      </div>
    );
  }
}

export default App;

The above example uses the toolcool-range-slider library, but I am fine using any slider implementation.

Advertisement

Answer

The problem:


The function updateX in react component named App is updating the state of y as well, which is triggering the change event of slider-y.

I believe you are already aware of this issue as per your comment in response to Wazeed, and want a way to identify the event only when the user is interacting with the slider.

First reference: @Wazeed

How to solve:

You can detect if the document with focus is the slider itself.
By doing this, it would only enter the onChange function when the slider element is focused, that is, changes not coming from the user’s legitimate action of changing the slide would not continue in the method, that said, value changes by code and etc would be ignored.

Code alternatives:


The way to solve it is checking if the element is in focus or not, that said, there are two ways I know to achieve such a result.

  1. Checking if the current focus element is the referenced element
    Bad points: If the child element is being referenced, the comparison will return false (document.activeElement == slider)

  2. Checking if the referenced element or any of its children have active focus in the DOM (slider.hasFocus())

The code:


const { useState, useRef, useEffect, Component } = React;

function MySlider(props) {
  const sliderRef = useRef();

  useEffect(() => {
    const slider = sliderRef.current;
    const onChange = (evt) => {
      if(document.activeElement !== slider) return;
      const v = evt.detail.value;
      console.log("Slider " + props.name + " was dragged to: " + v);
      props.update(v);
    }
    slider.addEventListener('change', onChange);
    return () => {
      slider.removeEventListener('change', onChange);
    }
  }, []);

  return (
    <toolcool-range-slider
      id={props.name}
      min={props.min}
      max={props.max}
      value={props.value}
      step="1"
      ref={sliderRef}
    />
  );
}

class App extends Component {
  constructor(props) {
    super(props);
    this.state = {
      x: 10,
      y: 5,
    };
  }

  updateX(x) {
    this.setState({
      x: x,
      y: Math.ceil(x/2),
    });
  }

  updateY(y) {
    this.setState({y: y});
  }

  render() {
    const x = this.state.x;
    const y = this.state.y;
    const minY = Math.ceil(x/2);
    const maxY = x;

    return (
      <div>
        <table><tbody>
          <tr>
            <td>Choose x:</td>
            <td style={{padding:10}}>
              <MySlider name="x" min={10} max={50}
                value={this.state.x}
                update={(x) => this.updateX(x) }
              />
            </td>
            <td>{x}</td>
          </tr>
          <tr>
            <td>Choose a y in the range [x/2, x]:</td>
            <td style={{padding:10}}>
              <MySlider name="y" min={minY} max={maxY}
                value={this.state.y}
                update={(y) => this.updateY(y) }
              />
            </td>
            <td>{y}</td>
          </tr>
        </tbody></table>
      </div>
    );
  }
}

ReactDOM.createRoot(
    document.getElementById("root")
).render(
    <App/>
);
<script src="https://incapdns.github.io/toolcool-range-slider.min.js/index.js"></script>
<script src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>

<div id="root"></div>

Remarks:

If this answer was not satisfactory, or confusing, or did not answer what was asked, please comment so I can edit it.

It’s worth mentioning that I’m using google translator to answer, I apologize for any inconvenience
User contributions licensed under: CC BY-SA
9 People found this is helpful
Advertisement