State Management with Svelte - Context API (Part 2)

Disclaimer - If you are unfamiliar with props in Svelte applications, then please read this blog post before proceeding on. You must understand the limitations of props to better understand the Context API and how it addresses those limitations.

For components that need to share data to lower-level descendant components (and slotted content) within the same subtree, the Context API offers two methods for these components to communicate data without prop drilling or dispatching events: getContext and setContext.

setContext#

When a component calls setContext, the component defines a context, which is a value (primitive or object) representing data that can only be accessed by the component's descendants via the getContext method. Defining a context with setContext requires the component to supply a context key and a value. The context key helps to retrieve the context value from the closest ancestor component with a defined context corresponding to this key.

getContext#

For a descendant component to consume context defined within one of its ancestors, call the getContext method, passing to it the context key.

Note: The context key does not have to be a string; it can be any value. Using a string as the context key has the downside of conflicting with other contexts with the same context key, especially if these contexts belong to third-party libraries that you have little to no control over. Using an object literal as the context key ensures that its context won't conflict with other contexts since object literals are compared via referential equality.

Both setContext and getContext must be called during component initialization at the top-level of <script />, similar to lifecycle methods.

Revisiting Previous Example#

Let's revisit the <Grandparent />, <Parent /> and <Child /> components' example and refactor the code by substituting the props with context.

(Grandparent.svelte)

(Parent.svelte)

(Child.svelte)

This approach is much cleaner compared to the props approach. The intermediate <Parent /> component no longer contains any reference to message, which it previously received from the <Grandparent /> component and forwarded it directly to the <Child /> component. Only the descendant components concerned with message (in this case, the <Child /> component) receive it.

Use Case - D3 Scatterplot Visualization#

At a minimum, a simple scatterplot consists of two labeled axes (an x-axis and y-axis) and some dots. Optionally, a scatterplot may display a legend mapping a dot's color to a specific category it belongs to (for classification purposes).

If you break down a <Scatterplot /> component into its constituent components, then you might arrive at this component hierarchy:

Note: <svg /> is commonly the root element of interactive data visualizations, especially those built using D3.

When you include props, then you may notice how reliant these components are on the same set of props.

  • The dimensions prop provides the height, width and margins (top, right, bottom and left) of the visualization. Every child component needs to know these dimensions to align labels and position elements properly relative to the canvas.

  • The <XAxis /> component must know the scale (in this case, a continuous, linear scale), xScale, with which to generate the ticks for the x-axis based on the extent of the domain (in this case, petal width) and dimensions of the canvas. The <Dots /> component also must know this xScale to calculate the cx value (x coordinate) of each <circle /> element, which represents a single data point. The reasoning for passing the xScale to both the <Dots /> and <XAxis /> components can likewise be applied to the passing of the yScale to both the <Dots /> and <YAxis /> components, but within the context of the y-axis.

  • The colorScale maps each category to a unique color. The <Legend /> component displays this mapping, representing each pairing with a colored square and an adjacent label for the category. The <Dots /> component uses this scale to color each data point based on its cateogry.

As the requirements of the visualization grow and more features are added, passing these props explicitly to each of these components becomes repetitive and unmaintainable.

Visit this simple Svelte REPL demo to see how the Context API allows these child components to receive all of this data from a single, common parent component (<Scatterplot />):

Context API Demo - D3 Scatterplot

The scatterplot shows the relationship between the petal length and petal width of flowers classified as species of Iris (Iris setosa, Iris virginica and Iris versicolor). If you have a background in a data science, statistics or machine learning background, then you are likely to have encountered and explored this multivariate dataset when learning introductory classification algorithms.

Context API Demo - D3 Scatterplot

(App.svelte)

The <App /> component fetches the Iris dataset from a GitHub Gist, and it establishes the petal length as the independent variable (x) and petal width as the dependent variable (y) to be plotted in the scatterplot. Each data point is categorized as one of three flower species: setosa, versicolor and virginica. Once the data is fetched, the <Scatterplot /> component is rendered using this data and custom configuration options.

Note: The <Scatterplot /> component can accept data from either a remote source (as shown above via d3.csv, fetch, etc.) or locally with this arrangement. The other props passed to the <Scatterplot /> component configure certain aspects of the scatterplot such as its dimensions and axes.

(context-keys.js)

To avoid any conflicts with other contexts, the context key for the scatterplot will be a literal object. This object will be referenced by all of the scatterplot's constituent components when they access the context set by the <Scatterplot /> component.

(Scatterplot.svelte)

The <Scatterplot /> component receives a set of props from a consuming component (in this case, <App />), and creates a context via setContext, which contains these values. Notice how no props are passed to the <Dots />, <XAxis />, <YAxis /> and <Legend /> components because each of these components will access those values directly from this context via getContext.

(Dots.svelte)

Since both axes are drawn dimensions.margins.left pixels away from the left-side of the visualization, we must automatically translate all of the dots horizontally by this same amount of pixels to ensure that they are drawn within the confines of these axes (translate(${dimensions.margins.left}, 0)). Then, each data point is drawn as a dot with a radius of three pixels, colored based on its flower species and positioned based on the values of xScale(item.x) and yScale(item.y).

(XAxis.svelte)

(YAxis.svelte)

To generate an axis' tick marks, call the ticks method on the scale function (xScale or yScale). The number of tick marks created is based on the number passed to the ticks method. To space the tick marks out evenly, translate each tick mark by a number of pixels determined by scale(tick) (translate(${xScale(tick)} 0) for each x-axis tick mark and translate(0, ${yScale(tick)}) for each y-axis' tick mark). To position the labels (<text /> element, last child of the outermost <g /> element), space them slightly away from the halfway point of its corresponding axis line (<path class="axis-line" /> element).

(Legend.svelte)

The legend is placed 25 pixels to the right of the y-axis (adding x pixels to dimensions.margins.left for the horizontal translation ensures the legend is shifted x pixels from the y-axis) and 25 pixels from the top-side of the canvas. Each category is displayed with its unique color representation and label.

React Context and Caveats of the Context API#

If you have built React applications, then up to this point, Svelte's Context API may appear similar to React's Context API: sharing values between components of the same subtree without having to explicitly specify props at every level of the component hierarchy.

In Svelte, the setContext creates the context, sets its value and automatically provides access to this context for all of the component's descendants. In React, you must first create the context with the createContext method.

Note: According to React's documentation, providing a default value to createContext is useful for testing components in isolation without having to wrap them within a provider component. This value serves as a fallback for when a component has no matching provider (for example, <ScatterplotCtx.Provider />) above it.

For components to access this context value, create a provider component and have it wrap all of these components:

In React, updates to the provider component will cause the useContext hook to trigger a rerender using the latest context value passed to this provider component.

However, in Svelte, descendant components cannot receive updates from a context whenever its set to a different value via a subsequent call to setContext. These components are only able to access the values made available to them from their ancestor component (the one setting the context) during component initialization. Since the components are not updated with the new context value and not re-rendered as a result of those changes, we cannot consider the Context API as reactive feature of Svelte. For reactivity, we must use props and/or stores, which allow components to access data and subscribe to updates from global data stores (not restricted to a subtree of components).

Next Steps#

Proceed to the next blog post to learn more about Svelte stores.

If you want to learn more about Svelte, then check out Fullstack Svelte:

Sources#