Skip to main content
Version: 5.2.0

React

React is one of the most popular web frontend frameworks at the moment. As such, it is extensively used by LightningChart JS users. Here you will find resources related to using LightningChart JS with React.

npm i @arction/lcjs

Basic usage boilerplate

Here's the React boilerplate code for creating perhaps the most common LightningChart JS use case - a Line Chart component.

/**
* props.data: {x: number, y: number}[]
*/
const Chart = (props) => {
const { data } = props;
const id = useId()
const [chart, setChart] = useState(undefined);

// Create chart just once during lifecycle of component.
useEffect(() => {
const container = document.getElementById(id);
if (!container) return
const lc = lightningChart()
const chart = lc.ChartXY({
theme: Themes.darkGold,
container,
});
const lineSeries = chart.addLineSeries({
dataPattern: { pattern: "ProgressiveX" },
});
setChart({ chart, lineSeries });
return () => {
// Destroy chart when component lifecycle ends.
lc.dispose();
};
}, [id]);

// Update line series data whenever data prop changes.
useEffect(() => {
if (!chart || !data) return
chart.lineSeries.clear().add(data);
}, [chart, data]);

return <div id={id} style={{ width: "100%", height: "100%" }}></div>;
};

We have prepared a minimal React application (created using create-react-app), which you can run to inspect and experiment with code like above. You can find it in GitHub.

Plug-and-play components for React

We have also created a simple NPM package for a React Time Series Chart.

If this fits your needs, you can get a kickstart by installing it directly from NPM and plugging it in your app. For more information, see react-time-series-chart.

Best practices for LightningChart JS based React components

Here you can find the most important best practices when developing React components using LightningChart JS.

Avoiding heavy side effects on component re-rendering

If your React component has props like chart title, x axis title, etc. you should apply them in a separate useEffect so that charts and/or series don't have to be recreated when the prop changes.

❌ bad:

useEffect(() => {
const container = document.getElementById(id);
if (!container) return
const lc = lightningChart()
const chart = lc.ChartXY({ container })
.setTitle(props.chartTitle)
return () => {
// NOTE: Chart is disposed whenever props.chartTitle changes!
lc.dispose();
};
}, [id, props.chartTitle]);

❎ good:

const [chart, setChart] = useState(undefined);
useEffect(() => {
const container = document.getElementById(id);
if (!container) return
const lc = lightningChart()
const chart = lc.ChartXY({ container })
setChart({ chart })
return () => {
lc.dispose();
};
}, [id]);
useEffect(() => {
if (!chart) return
// Whenever props.chartTitle changes, it is reapplied to existing chart without recreating it
chart.setTitle(props.chartTitle)
}, [chart, props.chartTitle])

Streaming data applications

If your application involves streaming in new data points frequently (several times per second), then the simple use example above will not perform very well. This is because, whenever a new data point is received (and data prop changes), the entire line series is cleared and then the entire data set is pushed in again.

While this works, it is hardly efficient, because LightningChart has extremely powerful APIs for appending new data points while keeping old data points around.

For best results, new data points arriving should only result in LineSeries.add() or equivalent method being called, without re-rendering the React component, or clearing the previous data.

This is generally achieved by handling historical data (points that exist when component is created) and streaming data (points that spawn after component is created) separately. Here is one example implementation of this:

const MyChartComponent = (props: {
// Historical data - new data points arriving MUST NOT result in changing this prop!
data?: { x: number, y: number }[],
// Component is injected with an event subscription method, so it can listen to new data points arriving.
onNewDataPoints: (clbk: (newDataPoints: { x: number, y: number }[]) => void) => () => void,
}) => {
const { data, onNewDataPoints }
const id = useId();
useEffect(() => {
const container = document.getElementById(id);
if (!container) return
const lc = lightningChart()
const chart = lc.ChartXY({ container });
const lineSeries = chart.addLineSeries({
dataPattern: { pattern: "ProgressiveX" },
})
// When component is created, add historical data in
if (data) lineSeries.add(data)
// When new data points arrive, push them directly to series with `add` without re-specifying entire data set.
const offNewDataPoints = onNewDataPoints((newDataPoints) => {
lineSeries.add(newDataPoints)
})
return () => {
lc.dispose();
offNewDataPoints()
};
},
[id, data, onNewDataPoints]);
return <div id={id} style={{ width: "100%", height: "100%" }}></div>;
}

The key points here are:

  • data prop should not change during the life cycle of the component. It is used for supplying historical data that is already present when the component is created!
  • When new data points arrive, they are directly pushed to LineSeries.add - very efficient!

Here's how this could look on the other side, where this chart component is used:

function App() {
const refDataListener = useRef(undefined);

// Connect to a websocket server and listen for data.
useEffect(() => {
const listener = refDataListener.current;
if (!listener) return;
const socket = new WebSocket('ws://my-data-server');
socket.onmessage = (e) => {
const newDataPoints = JSON.parse(e.data)
listener(newDataPoints)
}
return () => {
socket.close()
}
}, [refDataListener]);

// NOTE: Important to useMemo here, because otherwise `MyChartComponent` is re-rendered everytime `App` is.
const handleNewDataPoints = useMemo(() => {
return (clbk) => {
refDataListener.current = clbk;
return () => {
refDataListener.current = undefined;
};
};
}, []);

return (
<MyChartComponent onNewDataPoints={handleNewDataPoints} />
);
}

For more information about handling real-time data in general (not with React specifically), see Real-Time Data Documentation

Optimizing apps with several charts visible at once / charts being initialized often

If your React application involves having more than 1 chart visible at same time, or charts are frequently destroyed and recreated, then you should use LCContext for massive performance gains. LCContext is a React context that you can use for sharing LightningChart resources between all charts that are created during the application lifetime.

Integrating LCContext to your React application is a 3-step process:

  1. Add LCHost and LCContext to your source code. You can find the latest version of this logic in our React template repository.

  2. Wrap your React components into a shared LC context by using LCHost near the top of your React render tree:

import { LCHost } from './LC'
function App() {
return (
// NOTE: LCHost should be defined at the top of component tree, before any and all LCJS based components
// This let's them share the same LC context for performance benefits.
<LCHost>
<MyChartComponent />
</LCHost>
);
}
  1. Use the LCContext instead of directly calling lightningChart in your LightningChart JS components:

❌ bad:

export function MyChartComponent(props) {
const id = useId()
useEffect(() => {
const container = document.getElementById(id);
if (!container) return
const chart = lightningChart().ChartXY({ container })
return () => {
chart.dispose();
};
}, [id]);
}

❎ good:

export function MyChartComponent(props) {
const id = useId()
const lc = useContext(LCContext); // !
useEffect(() => {
const container = document.getElementById(id);
if (!container) return
const chart = lc.ChartXY({ container }) // !
return () => {
chart.dispose();
};
}, [id, lc]);
}

Using LCContext can increase chart loading time by up to 10x faster and it also completely removes the limit of max 16 charts being visible at 1 time.

Additional troubleshooting

React specific crashes

Often, LightningChart JS based React components involve several useEffect hooks, which relate to each other. A basic example:

  • 1st useEffect creates the LC context, a chart and a series.
  • 2nd useEffect updates the data within the series. If data changes, the whole chart is not destroyed.

Sometimes, complicated effect setups can result in React triggering effect cleanup functions and effects themselves in seemingly strange order. In some cases, this has resulted in a React component first disposing a chart component, and then triggering an effect that tries to utilize that destroyed chart component.

In most cases, this is not a problem and happens silently, but sometimes it has been found to crash the application. One easy workaround to this kind of issues is to add a sanity check before using chart components that confirms that they are not disposed already.

useEffect(() => {
const { chart, series } = chartState
// Sanity check
if (!chart || chart.isDisposed()) return // Exit silently
// Safe to use chart, series, etc...
}, [chartState])