Dashboard with drag-drop dynamic widgets in React.

Many times, when creating a web application, you get a requirement to create an interactive dashboard. This dashboard can have as many features a developer can imagine, out of which we are going to implement a few today here, like:

  • Have dynamic widgets which a user can play with
  • Allowing a user to have a default view with default widgets.
  • Users will be able to add new widgets by dragging and dropping from a separate view.
  • Users can also drag and drop existing widgets anywhere across the dashboard canvas.
  • Users can also resize these widgets according to their will.

To enable these features are for the users, a developer can play with the code and implement a solution that is dynamic in nature and does not have much impact on the web package size. 

But first, let us gather all the required ingredients:

  • 1 full cup of React 17 (or any version ≥ 16.6)
  • 1 full cup of a library which we will use to manage our widgets: React-Grid-Layout
  • Lastly, an optional ingredient for those who like things sweeter: TypeScript, as per taste.

Here is the CodeSandbox for the complete code:

Now let us hop into implementing it here step by step:

Step 1: Create a Dashboard component:

This component will be responsible for rendering and managing the grid of the widgets. This will also be responsible to save the layout of the current widgets.

  • We will use GridLayout from react-grid-layout to implement this. For responsive views, use GridLayout.Responsive.
  • We will provide values to some of its properties, such as preventCollision to customize the Grid behavior.
  • React Grid Layout provides a few callbacks, such as onDrop, which we will use to add new widgets to the grid dynamically, or to save the layout in component state and in some persistent storage such as localStorage.
  • We will also use a second component DashboardWidget, to dynamically load the widget component, which we will create in the next step.
import React, { useState, useCallback, useEffect } from 'react';
import GridLayout from 'react-grid-layout';

import { DashboardWidget } from './DashboardWidget';
import { WidgetSelector } from '../widget/WidgetSelector';

const defaultWidgets = [
  {
    id: 'IncrementWidget',
    layout: { i: 'IncrementWidget', x: 0, y: 0, w: 3, h: 1, isDraggable: false },
  },
  {
    id: 'DecrementWidget',
    layout: { i: 'DecrementWidget', x: 0, y: 1, w: 3, h: 1, isDraggable: false },
  }
];

export const Dashboard = () => {
  const [widgets, setWidgets] = useState(defaultWidgets);

  const onLayoutChange = useCallback(
		(_, oldItem, newItem) => {
			const newWidgetArr = [...widgets];
			newWidgetArr.forEach((x) => {
				if (x.id === oldItem.i) {
					x.layout = newItem;
				}
			});
			setWidgets(newWidgetArr);
		},
		[widgets]
	);

	const onDrop = useCallback(
		(_: Layout[], item: Layout, e: DragEvent) => {
			const raw = e.dataTransfer?.getData('droppableWidget');
			if (!raw) {
				return;
			}

			const droppableWidget = JSON.parse(raw) as IWidget<IWidgetDefaultProps>;

			const newWidgetArr = [...widgets];

			droppableWidget.layout.x = item.x;
			droppableWidget.layout.y = item.y;
			droppableWidget.layout.isDraggable = undefined;
			newWidgetArr.push(droppableWidget);

			setWidgets(newWidgetArr);
		},
		[widgets],
	);

	useEffect(() => {
		// Add any logic here to presist widgets and their layout to any presistent storage, like localStorage or any API
	}, [widgets]);

  return (
    <>
			<WidgetSelector />
      <GridLayout
				autoSize
				preventCollision
				useCSSTransforms
				isDroppable
				compactType={null}
        width={1000}
        onDrop={onDrop}
        onDragStop={onLayoutChange}
        onResizeStop={onLayoutChange}
      >
        {widgets.map(x => (
          <DashboardWidget key={x.id} widget={x} data-grid={x.layout} />
        ))}
      </GridLayout>
    </>
  );
};

Step 2: Create a DashboardWidget component:

This component will be responsible to dynamically load the component at runtime and save it in the state while re-rendering.

  • For this component, we will use a React feature that was introduced in React 16.6, Code-Splitting:

Code-splitting your app can help you “lazy-load” just the things that are currently needed by the user, which can dramatically improve the performance of your app. While you have not reduced the overall amount of code in your app, you have avoided loading code that the user may never need and reduced the amount of code needed during the initial load.

This React feature will help us split out widgets code into separate files at build time, thus reducing the main package size. Therefore, the browser will only load the widgets that the user chooses in his customized layout.

import React, { forwardRef, Suspense, useState } from 'react';
import { Layout } from 'react-grid-layout';

const loadWidget = widget => {
  return React.lazy(() => import(`../widget/${widget.id}.tsx`));
};

export const DashboardWidget = forwardRef((props, ref) => {
  const { widget } = props;
  const [WidgetComponent] = useState(React.lazy(loadWidget(widget)));

  return (
    <div ref={ref} {...props}>
      <Suspense fallback={<>Loading</>}>
        <WidgetComponent />
        {props.children}
      </Suspense>
    </div>
  );
});

Step 3: Creating a Widget Selector:

This component will be responsible for managing new widgets and allowing users to drag and drop widgets into the grid on Dashboard.

  • To allow users to drag and drop widgets, we are going to use HTML Drag and Drop API. This API allows interfaces to drag draggable elements with a mouse and drop them by releasing the mouse button.
  • For this, we’ll use onDragStart event and dataTransfer object to allow the grid to identify the element as a new widget
  • You may customize how you want to display available widgets here.
import React from 'react';

const availableWidgets = [
  {
    previewImg: 'https://via.placeholder.com/200x80',
    previewName: 'Placeholder Image',
    id: 'ImageWidget',
    layout: { i: 'ImageWidget', x: 0, y: 0, w: 3, h: 1 },
  }
];

export const WidgetSelector = () => {
  return (
    <div style={{ borderBottom: '1px solid' }}>
      <div>Drag and drop a widget from following list:</div>
      {availableWidgets.map(widget => (
        <div
          key={widget.id}
          unselectable="on"
          onDragStart={e => {
            // this is a hack for firefox
            // Firefox requires some kind of initialization
            // which we can do by adding this attribute
            // @see https://bugzilla.mozilla.org/show_bug.cgi?id=568313
            e.dataTransfer.setData('text/plain', '');
            e.dataTransfer.setData('droppableWidget', JSON.stringify(widget));
            return true;
          }}
        >
          <img src={widget.previewImg} />
          <div>{widget.previewName}</div>
        </div>
      ))}
    </div>
  );
};

Step 4: Create a widget:

This will a self-sufficient, independent component which can manage its own state and data. You may customize its interface as per your choice. 

The only thing to remember while creating a widget is that you have to export the component using Default Export (and not Named Export). This is due to a React.lazy function which currently only supports default exports.

import React from 'react';

const ImageWidget = () => {
  return <img src="https://via.placeholder.com/200x80" />;
};

export default ImageWidget;

That’s it.

You have now created a dynamic dashboard that will allow your users to customize the widget and its layout, also allowing you to dynamically load widgets at runtime.

You can also add a few improvements to this by adding props for widgets which can be changed at runtime by the grid (helpful for adding filters for the data being displayed) or make the grid responsive according to the width available.

Here is the link for the Github repo:

kgrvr/React-Dashboard-Widget-Example

Do you feel this will jazz up your app’s landing page?

If yes, then feel free to chat with our live agent at impledge.com

Thank you !!

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s