State Management with Svelte - Props (Part 1)

Each component of a client-side application contains internal state that encapsulates its own data, which determines the component's behavior and stores business-related data for rendering. When composing many components into a multi-level hierarchy, components must communicate data and coordinate amongst each other to perform more complex tasks. For example, a <SearchBar /> component might pass a list of search results to a <TypeaheadDropdown /> component to display suggestions based on the currently typed query. There are three common approaches to facilitate communication amongst the various components within a Svelte application:

  • Props

  • Context API

  • Stores

Having full control over state within an application allows developers to know how data flows throughout the application, better reason about the data and optimize accordingly. Modeling data flow with props, contexts and/or stores improves the organization of state, normalizes data to reduce instances of duplicated data and curtails the amount of time spent on debugging (quickly identify the component/s or portions of state responsible for a bug).

Below, and in two additional blog posts, I'm going to show you:

  • How to manage application/component state with props, context and stores.

  • The differences between props, contexts and stores.

  • Several code snippets and demos demonstrating the capabilities and limitations of props, contexts and stores.

  • How to evaluate the trade-offs between Svelte stores and Redux as a state management solution in large Svelte applications.

Props#

The simplest form of communication involves a parent component passing data to its direct child component/s via props. This concept is widely adopted in other popular component-based libraries/frameworks, such as React and Vue. In Svelte, the export keyword defines a prop that a component can accept from its consumers.

(Parent.svelte)

(Child.svelte)

With props, data flows unidirectionally downwards from a higher-level component to a lower-level component in the component hierarchy. This means changes made to a prop in a child component will not propagate back to its parent component and yield any unwanted side effects. Unlike two-way data-binding in AngularJS, which caused performance issues and modal updates to cascade, one-way data binding with props reduces the number of possible points of failure due to the predictable nature of component/application state.

Spreading Props#

If multiple props need to passed to a component, then consider packing all the props into an object and spreading them onto the component, which serves as a convenient shortcut for passing many props rather than individually specifying them.

(Parent.svelte)

(Child.svelte)

Default Props#

If the parent component does not specify the prop on the child component, then its value will be set to undefined by default. However, instead of setting its value to undefined, the prop can be set to a default initial value.

(Parent.svelte)

(Child.svelte)

const, class and function Exports vs. let Exports#

Specifying props in a component with export let allows those props to be modified within the component itself.

(Parent.svelte)

(Child.svelte)

When clicking the button and incrementing the value of the prop count, the newly incremented value is logged to the developers tool console, which indicates this prop being modified in the <Child /> component. However, this change is not propagated to the <Parent /> component. The count displayed within its <p /> tag remains zero no matter how many times you click on the "Increment" button.

Try it out here.

Swapping let for const changes the behavior of export. Unlike variables declared with export let, variables declared with export const, export function or export class are not props, but instead, are readonly constants that can be imported into other components as named imports.

(Parent.svelte)

(Child.svelte)

Try it out here.

To export these variables for consumption in other components via an import statement, declare the <script /> block with the attribute context="module", which will execute the code within the <script /> block once upon the evaluation of the module, not the instantiation of the component.

Uncommenting out any of the count++ statements and then clicking on the button bound to the event handler containing this statement will result in an error being logged to the developers tool console and count not being incremented. Variables declared with export const cannot be reassigned to a different value. This also applies to variables declared with export function and export class.

Note: Properties of an object declared with export const can be modified, but the reference to the object cannot be changed.

Suppose you decide to export a function that changes a value within the component it is exported from.

(Parent.svelte)

(Child.svelte)

Clicking the "Increment" button will call the imported incrementCount function. When incrementCount is executed, the value of count is incremented. Because count is declared within a <script context="module" /> block, count is not reactive, and therefore, the count value displayed within the <p /> tag remains zero.

A function can be exported from an instance directly and accessed as a property on the component's reference, which can be obtained via the bind:this directive. These properties are accessible after the component has been mounted to the DOM.

(Parent.svelte)

(Child.svelte)

When clicking the "Increment" button, not only is count incremented, but its new value is displayed in the <p />.

Caveat/Limitation of Props#

For grandchild components to receive data from grandparent components, that data must first be passed from the grandparent component to its child component (the grandchild's parent component), and then, that data must be passed from this parent component to its child component (the grandchild component).

(Grandparent.svelte)

(Parent.svelte)

(Child.svelte)

This process of explicitly and redundantly passing data from higher-level ancestor components to lower-level descendant components via props is known as prop drilling. As the number of component layers increases, it becomes more difficult to track these props, especially when refactoring components and having to manually rename or remove props. In the above example, if we decide to rename the message prop to greeting, then we would have to start renaming the prop at the <Grandparent /> component and traverse down its subtree, renaming the prop at each subsequent component (the <Parent /> and <Child /> components). Also, the intermediate <Parent /> component does not make use of the message prop whatsoever. It simply receives this prop and passes it directly to the <Child /> component. Having these types of props in our components can add unnecessary clutter to them and exacerbate our ability to understand and maintain these components.

Alternatively, to make data available to a component and its descendants without prop drilling, Svelte provides the Context API.

Next Steps#

Proceed to the next blog post to learn more about Svelte's Context API.

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

Sources#