Get Started: Tutorials, Examples > Tutorials / React > Tutorial - Creating a SciChart React Component from the Ground Up
Tutorial - Creating a SciChart React Component from the Ground Up

NEW! We have now published an open source scichart-react library which neatly wraps up this entire tutorial and more. Handling component lifecycle, proper disposal of the SciChartSurface and with a nice API too :)

Head over to the following page to learn more:

React Charts with SciChart.js: Introducing the scichart-react library

Also see:

 

The idea of this tutorial is to create a reusable React component which could be used as a core setup for instantiating a SciChart.js chart and properly disposing of memory once the chart is no longer needed. 

Source code for this tutorial can be found at SciChart.Js.Examples Github Repository

Main criteria and points to consider:

  • The component should be reusable for different chart configurations
  • It should be possible to safely create several instances of the component
  • It should be easy to add custom functionality to the component
  • SciChart instantiation is an async function, thus it should be properly handled
  • SciChart requires a root node element where it would reside to exist before the instantiation
  • The chart should be properly disposed and memory deleted after the component is unmounted.

While creating the example we will try using tools provided in SciChart's MemoryUsageHelper to discover potential issues with memory leaking and bugs.

Worked Example - a Basic React Component

Let's start with showing an example of how to create a re-usable React Component to work with SciChart.js. We'll start off with a simple React component which instantiates a SciChartSurface in useEffect but fails to delete it on unmount.

Simple React Component
Copy Code
function SciChart() {
    const rootElementId = 'chart';
    useEffect(() => {
       createChart(rootElementId); // Note, does not delete on unmount (todo later)
    }, []);
    return <div id={rootElementId} style={{width: 800, height: 600}} />;
}

where createChart could be defined as below:

createChart
Copy Code
const createChart = async (divElementId: string ) => {
    const { sciChartSurface, wasmContext } = await SciChartSurface.create(divElementId);
    const xAxis = new NumericAxis(wasmContext);
    const yAxis = new NumericAxis(wasmContext);
    sciChartSurface.xAxes.add(xAxis);
    sciChartSurface.yAxes.add(yAxis);
    return { sciChartSurface };
};

So by placing a createChart into the useEffect hook with an empty list of dependencies we ensure that:

  • initialization will happen only once.
  • and it will happen after the component render, so that it could create a chart on the rendered root element.

Reusing the component

Now let's make it reusable by allowing to provide arbitrary initialization function and styles. For this we could pass these via the component props.

Also, to allow using the result of the initialization we want to expose some interface to manipulate a chart. A common return result would contain a surface reference as createChart has. To make it more generic we can defined the return type with ISciChartSurfaceBase which is common for 2D, 3D and Pie surfaces in SciChart.

Also, to allow placing multiple charts on the page we should provide a unique rootElementId per component instance.

Updated useState
Copy Code
const [rootElementId] = useState(`chart-root-${generateGuid()}`);

So the interface and usage of the props would look like this:

React Component
Copy Code
interface IChartComponentProps {
    initChart: (rootElementId: string) => Promise<{ sciChartSurface: ISciChartSurfaceBase }>;
    className?: string;
    style?: CSSProperties;
}
function SciChart(props: IChartComponentProps) {
    const [rootElementId] = useState(`chart-root-${generateGuid()}`);
    useEffect(() => {
        props.initChart(rootElementId);
    }, []);
    return <div id={rootElementId} className={props.className} style={props.style} />;
}

 

The usage example of such component:

usage
Copy Code
function App() {
  return (
     <div className='App'>
        <SciChart initChart={createChart} style={{ width: 800, height: 600 }} />
     </div>
  );
}

Testing the component with SciChart Memory Debug tools

Now, before going to implementing the further requirements, let's try to test this component with SciCharts Memory Debugging tools.

We will setup an example in which we could force the unmounting of the SciChart component to see if it has been properly cleaned up.
For this we will:

  • enable the debug mode with MemoryUsageHelper.isMemoryUsageDebugEnabled = true,
  • hint the SciChart to automatically destroy the WebAssembly Context (if there are no surface instances using it) with SciChartSurface.autoDisposeWasmContext = true
    (to make a full cleanup for testing purposes).
NOTE The debug mode (MemoryUsageHelper.isMemoryUsageDebugEnabled = true) is only supposed to be used development mode. It will not work in the production build.
Debugging memory leaks
Copy Code
// ...
SciChartSurface.autoDisposeWasmContext = true;
MemoryUsageHelper.isMemoryUsageDebugEnabled = true;
function App() {
  const [drawChart, setDrawChart] = useState(true);
  const handleCheckbox: ChangeEventHandler<HTMLInputElement> = (e) => {
     setDrawChart(e.target.checked);
  };
  const handleClick: MouseEventHandler<HTMLInputElement> = () => {
     const state = MemoryUsageHelper.objectRegistry.getState();
     console.log('state', state);
  };
  return (
     <div className='App'>
        <header className='App-header'>
           <h1>SciChart.js with React</h1>
              <p>In this example we setup webpack, scichart, react and create a simple chart with one X and Y axis</p>
        </header>
        <input type='checkbox' checked={drawChart} onChange={handleCheckbox} /> Show Chart
        <br />
        <input type='button' onClick={handleClick} value="Log Object Registry State"></input>
        {drawChart ? <SciChart initChart={createChart} style={{ width: 800, height: 600 }} /> : null}
     </div>
  );
}

Now let's compare how the state of MemoryUsageHelper.objectRegistry will change after we unmount the component.

We will use this debugging steps for testingL

  1. Output the current state by pressing "Log Object Registry State" button. The output will be something like:
    Console output
    Copy Code
    state {undeletedObjectsIds: Array(61), uncollectedObjectsIds: Array(62)}
    
    which is the list of SciChart related objest that could be disposed using delete method.

    NOTE SciChart manages the lifecycle of most of them internally. But some of those, that were not attached to or were detached from a chart, are supposed to be managed explicitly.

  2. To unmount the component we could click the "Show Chart" checkbox. Immediately we will get a console warning

        
     
    It suggest that we may have forgotten to clean up the created chart when the component was unmounted (which we did).
  3. Now, additionally, we may force the garbage collection using browser tools if available. This may help to better understand if there are leaks of JS objects caused by unreleased references based on uncollectedObjectsIds. But more important is to focus on making sure that SciChart related entities were properly disposed and there should be undeletedObjectsIds.

  4. Pressing the "Log Object Registry State" button again will show an output similar to the previous, confirming that the chart was not cleaned up properly.

Adding a cleanup callback

Let's try to fix an obvious issue and return a component destructor in the useEffect hook. Here we updated the component, but there is still an issue with it.

React component with cleanup (1)
Copy Code
function SciChart(props: IChartComponentProps) {
    const [sciChartSurface, setSciChartSurface] = useState<ISciChartSurfaceBase>();
    const [rootElementId] = useState(`chart-root-${generateGuid()}`);
    useEffect(() => {
       (async () => {
           const res = await props.initChart(rootElementId);
           setSciChartSurface(res.sciChartSurface);
       })();
      return () => sciChartSurface?.delete();
    }, []);
    return <div id={rootElementId} className={props.className} style={props.style} />;
}

After going through the debug steps again we will see that nothing has really changed. The reason for this is a wrong reference to the surface used in the destructor () => sciChartSurface?.delete();. As you may guess it will use the nullish value assigned in the initial component render.

So deal with the fact that chart initialization is an async operation we will consider handling the unresolved promise.
So our next iteration of the component will be:

React component with cleanup (2)
Copy Code
function SciChart(props: IChartComponentProps) {
    const sciChartSurfaceRef = useRef<ISciChartSurfaceBase>();
    const [rootElementId] = useState(`chart-root-${generateGuid()}`);
    useEffect(() => {
        const chartInitializationPromise = props.initChart(rootElementId).then((initResult) => {
            sciChartSurfaceRef.current = initResult.sciChartSurface;
            return initResult.sciChartSurface;
        });
        const performCleanup = () => {
            sciChartSurfaceRef.current.delete();
            sciChartSurfaceRef.current = undefined;
        };
       
        return () => {
            // check if chart is already initialized or wait init to finish before deleting it
            sciChartSurfaceRef.current ? performCleanup() : chartInitializationPromise.then(performCleanup);
        };
    }, []);
    return <div id={rootElementId} className={props.className} style={props.style} />;
}

After running the debug steps again we should see an empty state returned to the output, which means the chart was properly garbage collected.

And with this we've achieved our goal of creating a reusable chart component. This component could be a boilerplate for further improvement and customization, so consider testing corner cases with the SciChart debugging tools, as well as tools provided by browsers.

 


Additional Tips

React:

  • To allow adding custom functionality upon a chart component (e.g. binding UI controls for chart manipulations), consider exposing a reference to the surface via useImperativeHandle hook.
  • Consider using Suspense for awaiting the async data/component load

Debugging:

  • MemoryUsageHelper exposes a number of methods for performing a cleanup. They may be useful for testing purposes but use them with caution.

See Also