Clients for Web Frameworks
Clients are responsible for publishing their data needs and performing data processing tasks—such as rendering a visualization. The MosaicClient
class provides a base class for client implementation.
Many applications are written in web frameworks such as React, Svelte, and Vue. These frameworks have their own state management and lifecycle management systems which make it difficult to use the MosaicClient
class directly. Mosaic has a makeClient
API that makes it easier to integrate with such frameworks.
Svelte Example
Here is an example Count
component written in Svelte, with $effect
:
svelte
<script>
// Show the number of rows in the table.
// If a `selection` is provided, show the filtered number of rows as well.
import { makeClient } from "@uwdata/mosaic-core";
import { count, Query } from "@uwdata/mosaic-sql";
const { coordinator, table, selection } = $props();
let totalCount = $state(null);
let filteredCount = $state(null);
let isError = $state(false);
let isPending = $state(false);
$effect(() => {
// Capture tableName so Svelte keeps track of it in the effect.
// When `table` changes, Svelte will re-run the effect and cause the old client
// be destroyed and a new client be created.
let tableName = table;
// Note that the identity of `selection` is also captured below.
// If it is replaced with a new instance of Selection, the client will get recreated as well.
let client = makeClient({
coordinator,
selection,
prepare: async () => {
// Preparation work before the client starts.
// Here we get the total number of rows in the table.
let result = await coordinator.query(
Query.from(tableName).select({ count: count() })
);
totalCount = result.get(0).count;
},
query: (predicate) => {
// Returns a query to retrieve the data.
// The `predicate` is the selection's predicate for this client.
// Here we use it to get the filtered count.
return Query.from(tableName)
.select({ count: count() })
.where(predicate);
},
queryResult: (data) => {
// The query result is available.
filteredCount = data.get(0).count;
isError = false;
isPending = false;
},
queryPending: () => {
// The query is pending.
isPending = true;
isError = false;
},
queryError: () => {
// There is an error running the query.
isPending = false;
isError = true;
},
});
return () => {
// Destroy the client on effect cleanup.
client.destroy();
};
});
</script>
{filteredCount} / {totalCount}
{isPending ? "(pending)" : ""}
{isError ? "(error)" : ""}
The makeClient
API combined with $effect
allows the component to update its client when the props changes. This includes:
- Replacement of the
coordinator
instance. - Changes to the
table
name. - Replacement of the
selection
instance (i.e., when theselection
is replaced with a new instance ofSelection
).
React Example
Here is the same example in React:
jsx
import { makeClient } from "@uwdata/mosaic-core";
import { count, Query } from "@uwdata/mosaic-sql";
import { useState, useEffect } from "react";
/** Show the number of rows in the table.
* If a `selection` is provided, show the filtered number of rows as well. */
export function Count(props) {
const { coordinator, table, selection } = props;
const [totalCount, setTotalCount] = useState(null);
const [filteredCount, setFilteredCount] = useState(null);
const [isError, setIsError] = useState(false);
const [isPending, setIsPending] = useState(false);
useEffect(() => {
// Note that the identity of `table` and `selection` is captured below.
// If they are replaced with a new instances, the client will get recreated as well.
const client = makeClient({
coordinator,
selection,
prepare: async () => {
// Preparation work before the client starts.
// Here we get the total number of rows in the table.
const result = await coordinator.query(
Query.from(table).select({ count: count() })
);
setTotalCount(result.get(0).count);
},
query: (predicate) => {
// Returns a query to retrieve the data.
// The `predicate` is the selection's predicate for this client.
// Here we use it to get the filtered count.
return Query.from(table)
.select({ count: count() })
.where(predicate);
},
queryResult: (data) => {
// The query result is available.
setFilteredCount(data.get(0).count);
setIsError(false);
setIsPending(false);
},
queryPending: () => {
// The query is pending.
setIsPending(true);
setIsError(false);
},
queryError: () => {
// There is an error running the query.
setIsPending(false);
setIsError(true);
},
});
return () => {
// Destroy the client on effect cleanup.
client.destroy();
};
}, [coordinator, table, selection]);
return (
<div>
{filteredCount} / {totalCount}
{isPending ? "(pending)" : ""}
{isError ? "(error)" : ""}
</div>
);
}