Loading Data with loadData
The loadData
function, called inside your .emb.ts
files, is a powerful part of the Embeddable SDK. It lets you load data for your components (charts, controls, etc.) while giving no-code users the flexibility to choose which data they want displayed. This is key to building reusable and configurable components.
A Basic Example
Below is an .emb
file for a bar chart component that calls loadData
inside the defineComponent
function:
// src/components/BarChart/BarChart.emb.ts
import { defineComponent } from '@embeddable.com/react';
import { loadData } from '@embeddable.com/core';
import Component from './index';
export const meta = {
name: 'BarChart',
label: 'Bar Chart',
inputs: [
{
name: 'ds',
type: 'dataset', // Renders a dropdown with available datasets in the builder
label: 'Dataset',
},
{
name: 'myDimension',
type: 'dimension', // Renders a dropdown with dimensions
array: false, // Enables selection of a single dimension
label: 'Dimension',
config: { dataset: 'ds' } // Limit dimensions to the chosen dataset
},
{
name: 'myMeasures',
type: 'measure', // Renders a dropdown with measures
label: 'Measure',
array: true, // Enables selection of multiple measures
config: { dataset: 'ds' } // Limit measures to the chosen dataset
}
]
} as const;
export default defineComponent(Component, meta, {
props: (inputs) => {
return {
...inputs,
results: loadData({
from: inputs.ds, // Receives the chosen dataset
dimensions: [inputs.myDimension], // Receives the chosen dimension
measures: inputs.myMeasures // Receives the chosen measures
})
};
}
});
When users add this component in the no-code dashboard builder, they will:
- Pick a dataset (e.g.
orders
orcustomers
). - Select a dimension (e.g.
country
). - Choose one or more measures (e.g.
count
,revenue
).
Then loadData
retrieves information from your database or the cache, and your component receives props like:
props: {
myDimension: {
name: "customers.country",
title: "Country",
...
},
myMeasures: [
{
name: "customers.count",
title: "# of customers",
...
}
],
results: {
isLoading: false, // false when data has loaded
error: undefined, // error message if there's a loading issue
data: [ // each row of data
{
"customers.country": "United States",
"customers.count": "9"
},
{
"customers.country": "Spain",
"customers.count": "67"
},
...
]
}
}
Your React component can:
- Check
results.isLoading
orresults.error
to show spinners or errors. - Read
results.data
to render your chart or UI. - Use
myDimension
andmyMeasures
to label series or axes.
This approach lets no-code users decide which data to load, while your code handles how to display it.
Parameters
Below are the main parameters relevant to loadData(...)
, with examples. For each type, you can prompt the user to select it by defining the appropriate inputs in your .emb.ts
file.
Param | Type | Required | Default Value |
---|---|---|---|
from | Dataset | Yes | — |
dimensions | Dimension[] | At least one dimension or measure is required | [] |
measures | Measure[] | At least one dimension or measure is required | [] |
timeDimensions | TimeDimension[] | No | [] |
orderBy | OrderBy[] | No | none |
limit | number | No | 100 |
offset | number | No | 0 |
filters | QueryFilter[] | No | [] |
loadData
effectively builds an SQL statement under the hood, using the parameters above:
SELECT <timeDimensions>, <dimensions>, <measures>
FROM <dataset>
WHERE <filters on dimensions>
GROUP BY <timeDimensions>, <dimensions>
HAVING <filters on measures>
ORDER BY <orderBy>
LIMIT <limit>
OFFSET <offset>
Types
-
Dataset
is a defined dataset in the no-code builder that's based on a data model, possibly with pre-defined filters. -
Dimension
is an array of one or more dimensions. -
Measure
is an array of one or more measures -
TimeDimensions
are time-based dimensions, often paired with a granularity (e.g. day, week, month).timeDimensions: [ { dimension: inputs.xAxis?.name, //pass just the dimension name granularity: inputs.granularity } ]
-
OrderBy
is an array of sorting objects containing aproperty
(dimension
ormeasure
) and adirection
(asc
ordesc
). This is also available as a native input in the no-code builder, but you can define it if your component needs custom sorting.orderBy: [ { property: inputs.yAxis, direction: 'desc' } ]
-
limit
is a number restricting how many rows are returned, e.g. for pagination. It defaults to 100 if not provided. This is also available as a native input in the no-code builder. -
offset
is a number specifying how many rows to skip, e.g. for pagination. Defaults to 0. -
filters
is an array of additional filters, applied on top of any filters in the dataset. Dimensions go intoWHERE
, measures go intoHAVING
.filters: [ { property: inputs.yAxis, // Dimension | Measure | DimensionOrMeasure operator: 'gt', // see below for available operators value: 0 // string | boolean | number | Date | Time | TimeRange | Array<string | boolean | number | Date>; } ],
Available Filter Operators
Embeddable currently supports the following operators for dimension and measure filters:
equals
: Property equals the specified value.notEquals
: Property does not equal the specified value.contains
: String property contains the specified substring.notContains
: String property does not contain the specified substring.startsWith
: String property starts with the specified substring.endsWith
: String property ends with the specified substring.gt
: Property is greater than the specified value.gte
: Property is greater than or equal to the specified value.lt
: Property is less than the specified value.lte
: Property is less than or equal to the specified value.set
: Property is not null (i.e., it has a value).notSet
: Property is null (no value).measureFilter
: Used for advanced measure-based filtering logic (e.g., when combining multiple measures).
Depending on whether a filter applies to a dimension or measure, Embeddable translates these operators into a WHERE
or HAVING
clause in the generated SQL.
Date/Time-specific operators (for fields of type time
):
-
inDateRange
: Property falls within a specified start and end date/time. -
notInDateRange
: Property is outside the specified date range.{ property: inputs.timeDimension, operator: 'inDateRange', value: { // TimeRange value from: inputs.timeFilter.from, // string | Date relativeTimeString: '', // string to: inputs.timeFilter.to, // string | Date }, }
💡You can also specify a wide range of relative time strings e.g. yesterday, last week etc. Behind the scenes, Cube uses Chrono (opens in a new tab) to parse these.
-
beforeDate
: Property is strictly before a specified date/time. -
afterDate
: Property is strictly after a specified date/time.
{
property: inputs.timeDimension,
operator: 'beforeData',
value: ["2025-01-01"] // an array of one element in YYYY-MM-DD format
}
A Full Example
Below is a comprehensive example that uses all the parameters of loadData
:
import { loadData } from '@embeddable.com/core';
import { defineComponent } from '@embeddable.com/react';
import Component from './index';
export const meta = {
name: 'GroupedLineChart',
label: 'Grouped Line chart',
inputs: [
{
name: 'ds',
type: 'dataset',
label: 'Dataset',
},
{
name: 'xAxis',
type: 'dimension',
label: 'X-Axis',
config: {
dataset: 'ds',
supportedTypes: ['time'] // Only let user pick time dimensions
}
},
{
name: 'granularity',
type: 'granularity',
label: 'Granularity'
},
{
name: 'groupBy',
type: 'dimension',
label: 'Group by',
config: { dataset: 'ds' }
},
{
name: 'yAxis',
type: 'measure',
label: 'Y-Axis',
config: { dataset: 'ds' }
},
],
};
export default defineComponent(Component, meta, {
props: (inputs) => {
return {
...inputs,
results: loadData({
from: inputs.ds,
measures: [inputs.yAxis],
dimensions: [inputs.groupBy],
timeDimensions: [
{
dimension: inputs.xAxis?.name,
granularity: inputs.granularity
}
],
filters: [
{
property: inputs.yAxis,
operator: 'gt',
value: 0
}
],
orderBy: [{ property: inputs.yAxis, direction: 'desc' }],
limit: 500,
offset: 0
})
};
}
});
Working With The results
Object
After loadData
fetches data, your React component receives a results
object (along with any other inputs the user selected). Using the example above, your component might get props like this:
{
"ds": { "datasetId": "...", "embeddableId": "..." },
"xAxis": {
"name": "orders.created_at",
"title": "Created at"
},
"granularity": "month",
"groupBy": {
"name": "customers.country",
"title": "Country of origin"
},
"yAxis": {
"name": "customers.count",
"title": "# of customers"
},
"results": {
"isLoading": false, // true while data is loading
"error": undefined, // an error message, returned as a string
"data": [
{
"customers.country": "United States",
"orders.created_at": "2023-05-01T00:00:00.000",
"customers.count": "9"
},
{
"customers.country": "United States",
"orders.created_at": "2023-06-01T00:00:00.000",
"customers.count": "57"
},
...
]
}
}
results.isLoading
andresults.error
let you handle loading states or errors.results.data
is an array of rows, indexed by each dimension/measurename
.xAxis
,yAxis
, andgroupBy
objects show which dimension/measure the user chose in the builder.
Example Usage in Your Component
export default function MyLineChart(props: Props) {
const { xAxis, groupBy, yAxis, results } = props;
const { isLoading, error, data } = results;
if (isLoading) return <Loading />;
if (error) return <Error msg={error} />;
const xValues = data.map((row) => new Date(row[xAxis.name]));
const groups = data.map((row) => row[groupBy.name]);
const yValues = data.map((row) => Number(row[yAxis.name]));
// Render your chart (e.g., axis labels, legends, grouping)
return <LineChart x={xValues} y={yValues} groupBy={groups} />;
}
This pattern lets no-code users pick what data to load, while you focus on how to display it.
Making Multiple loadData
Requests
Sometimes your component needs more than one data set. For example, you might display a main chart and a secondary chart, each requiring different dimensions or measures. Simply call loadData
multiple times under different prop names:
export default defineComponent(Component, meta, {
props: (inputs) => {
return {
...inputs,
// First request
mainResults: loadData({
from: inputs.mainDataset,
dimensions: [inputs.mainDimension],
measures: [inputs.mainMeasure]
}),
// Second request
secondaryResults: loadData({
from: inputs.secondaryDataset,
measures: [inputs.secondaryMeasure]
})
};
}
});
Here, mainResults
and secondaryResults
each return their own isLoading
, error
, and data
properties—letting you manage multiple data sets without extra complexity.
Loading Data Dynamically
Sometimes you need to fetch new data triggered by an action or event fired from within your React component. You can achieve using Embeddable State.
1. Pagination Example
A common scenario is incrementing offset
or changing limit
when a user clicks “Next Page” on a table component. You can hold these values in Embeddable State, then pass them to loadData
each time they change:
// MyPaginatedTable.emb.ts
import { defineComponent } from '@embeddable.com/react';
import { loadData } from '@embeddable.com/core';
import TableComponent from './index';
export const meta = {
name: 'MyPaginatedTable',
label: 'My Paginated Table',
inputs: [
{
name: 'ds',
type: 'dataset',
label: 'Dataset'
},
{
name: 'columns',
type: 'dimension',
label: 'Columns',
array: true,
config: {
dataset: 'ds'
}
},
]
} as const;
export default defineComponent(TableComponent, meta, {
props: (inputs, [state, setState]) => {
// Default pagination to page=0, limit=10
const page = state?.page ?? 0;
const limit = 10;
return {
...inputs,
page,
setPage: (newPage) => setState({ page: newPage }), // So the React component can update page
results: loadData({
from: inputs.ds,
dimensions: inputs.columns,
limit,
offset: page * limit
})
};
}
});
Inside your React component:
import { useEmbeddableState } from '@embeddable.com/react';
export default function TableComponent(props) {
const { page, setPage, results } = props;
const { isLoading, error, data } = results;
if (isLoading) return <Loading />;
if (error) return <Error msg={error} />;
const handleNext = () => setPage(page + 1);
const handlePrev = () => setPage(Math.max(0, page - 1));
return (
<div>
<Table data={data} />
<button onClick={handlePrev} disabled={page === 0}>Prev</button>
<button onClick={handleNext}>Next</button>
<p>Current Page: {page}</p>
</div>
);
}
Every time you click Next or Prev, Embeddable State updates page
, triggers a re-render, and runs loadData
with the new offset.
2. Filtering Example
For more advanced scenarios, such as applying filters dynamically:
// DynamicLoader.emb.ts
import { defineComponent } from '@embeddable.com/react';
import { loadData } from '@embeddable.com/core';
import Component from './index';
export const meta = {
name: 'DynamicLoader',
label: 'Dynamic Loader',
inputs: [
{
name: 'ds',
type: 'dataset',
label: 'Dataset'
},
{
name: 'columns',
type: 'dimensionOrMeasure',
label: 'Columns',
array: true,
config: {
dataset: 'ds',
},
category: 'Chart data',
}
]
} as const;
export default defineComponent(Component, meta, {
props: (inputs, [state, setState]) => {
const currentFilter = state?.filter;
return {
...inputs,
currentFilter,
results: loadData({
from: inputs.ds,
dimensions: inputs.columns,
filters: currentFilter ? [currentFilter] : undefined
})
};
}
});
Then in your React component:
import { useEmbeddableState } from '@embeddable.com/react';
export default function DynamicLoader(props) {
const [localState, setLocalState] = useEmbeddableState({ filter: {} });
const applyFilter = (value) => {
setLocalState((prev) => ({
...prev,
filter: {
property: { name: customers.country }, // some dimension, hard-coded or dynamically passed in via props
operator: 'equals',
value
}
}));
};
// props.results automatically updates when Embeddable State changes.
return (
<div>
<Table data={data} />
<button onClick={() => applyFilter('United States')}>
Filter to US
</button>
</div>
);
}
How it works:
- User Interaction: Clicking the button modifies Embeddable State (
filter
). - Rerun
loadData
: When state changes, the.emb.ts
file sees the new filter and callsloadData
again. - Refresh Component: Your component receives fresh results reflecting the updated filter.
This pattern lets you dynamically reload data based on user-driven state changes while still leveraging the no-code builder for broader configuration.
Importantly, these code-driven techniques are useful for component-specific actions, such as pagination or custom filters controlled entirely inside the component. For broader dashboard-level filters, the no-code builder already provides a visual interface—using variables and filters to reload component data without writing extra code. This gives you the best of both worlds: a powerful no-code approach for general filtering, and full control for specialized component logic.