简体   繁体   中英

Unexpected behavior of useCallback hook in my React component

I am fairly new to React and Javascript space. I am trying to build a component which uses the HighCharts library and one of the things I am implementing is a formatter callback. This callback needs to access the props passed to this component from the parent component. The property I am using is called metricUnit . The behavior I am noticing right now is that even though the metricUnit updates in the component, it doesn't relay down to this callback function. What am I doing incorrectly here?

const BoxChart = ({ panelId, group, metric, metricUnit, min, max,
  data}) => {
  
  console.log(panelId, metric, metricUnit) // this shows the updated value
  const pointFormatterCallback = useCallback(function() {
      return function() {
        console.log(metric);
        var value = this.y;
        if (value % 1) {
          value = Highcharts.numberFormat(value, 2);
        } else {
          value = Highcharts.numberFormat(value, 0);
        }
        const unitStr = metricUnit ? " (" + metricUnit + ")" : ""; //this still shows the older value
        return this.name + ': ' + value + unitStr +  '<br/>';
      }
  }, [metric, metricUnit]);

  const [defaultChartOptions, setDefaultChartOptions] = useState({
    chart: {
      style: {
        fontFamily: '"Roboto", -apple-system, BlinkMacSystemFont, "Segoe UI", "Helvetica Neue", Arial, sans-serif;',
        width: '100%'
      },
      type: 'boxplot',
      zoomType: 'x'
    },
    xAxis: {
      categories: [],
      title: {
        text: group,
        style: {
          fontWeight: 'bold'
        }
      }
    },
    yAxis: {
      title: {
        text: metric,
        style: {
          fontWeight: 'bold'
        }
      },
      labels: {
        formatter: function() {
          var outputVal = this.value;
          if(outputVal % 1) {
            outputVal = Highcharts.numberFormat(this.value, 2);
          } else {
            outputVal = Highcharts.numberFormat(this.value, 0);
          }
          return outputVal;
        }
      },
      startOnTick: false,
      endOnTick: false
    },
    title: {
      text: null
    },
    credits: {
      enabled: false
    },
    legend: {
      maxHeight:100,
      margin: 6
    },
    plotOptions: {
      boxplot: {
        tooltip: {
          valueDecimals: 2
        }
      },
      scatter: {
        marker: {
          radius: 5
        },
        tooltip: {
          headerFormat: "<b>{series.name}</b><br>",
          pointFormatter: pointFormatterCallback(),
          valueDecimals: 2
        }
      }
    }
  });

  const chartWrapper = useRef(null);
  const { width } = useElementSize(chartWrapper)

  const chartComponent = useRef(null);
  

  useEffect(() => {
    console.log("Current width", width);
    if(chartComponent.current) {
      const chart = chartComponent.current.chart;
      chart.update({
        chart: {
          width: width
        }
      });
    }
  }, [width]);

  if(data === undefined || Object.keys(data).length === 0 || data.categories.length === 0) {
    return(<div style={{position: 'absolute', top: '50%', left: '50%', transform: 'translateX(-50%) translateY(-50%)'}}>No data available</div>);
  } else {
    const series = data['series'];
    const categories = data['categories'];
    const chartOptions = {...defaultChartOptions};
    const unitStr = metricUnit ? " (" + metricUnit + ")" : "";
    chartOptions['yAxis']['title']['text'] = metric + unitStr;
    chartOptions['xAxis']['title']['text'] = group;
    chartOptions['series'] = series;
    chartOptions['xAxis']['categories'] = categories;
    if(min !== null && !isNaN(min)) {
      chartOptions['yAxis']['min'] = min;
    }
    if(max !== null && !isNaN(max)) {
      chartOptions['yAxis']['max'] = max;
    }
    return (
      <div ref={chartWrapper} style={{overflow: "hidden", width: "100%"}}>
        <HighchartsReact highcharts={Highcharts} options={chartOptions}
        ref={chartComponent}
        />
      </div>
    );
  }
}

Here is a sample reproduction: https://codesandbox.io/s/determined-tesla-26z00?file=/demo.jsx

Right now your callback passed into useCallback returns a function which does the work. There will be some unexpected closure behavior because of this. My best attempt at explaining it is thus:

When you set pointFormatterCallback to pointFormatter you then invoke it, and the value of metricUnit in the function returned will be whatever it is set to at invocation time . In order to read metric when it is updated, you'd have to re-invoke your pointFormatterCallback in order to access that new updated metric value.

Try removing the returned function like so:

    const pointFormatterCallback = useCallback(function() {
        console.log(metric);
        var value = this.y;
        if (value % 1) {
          value = Highcharts.numberFormat(value, 2);
        } else {
          value = Highcharts.numberFormat(value, 0);
        }
        const unitStr = metricUnit ? " (" + metricUnit + ")" : ""; //this still shows the older value
        return this.name + ': ' + value + unitStr +  '<br/>';
      }, [metric, metricUnit]);

Notice that changing parent state doesn't re-render the tooltipCallback function and it still has references to the previous unit.

Demo: https://codesandbox.io/s/patient-fog-mxdjr?file=/demo.jsx:708-723 - check the console to see what I am talking about.

I think that you should use the useEffect hook to set this parameter if has been changed.

You can even make it easier by just update the tooltip formatter - it makes y9ou sure that the chart component will be re-rendered.

Demo: https://stackblitz.com/edit/react-ruwaxu?file=index.js

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM