DEV Community

Cover image for [S4SRD]S01E02 - Hooked Evolved (Contextful Hooks in Svelte)
Tiago Nobrega
Tiago Nobrega

Posted on

[S4SRD]S01E02 - Hooked Evolved (Contextful Hooks in Svelte)

This is a series about applying some common React concepts and patterns in sveltejs. This season is about hooks. Last episode We've set the objective of exploring hooks and how to implement some of its features in svelte (Check it out if You haven't so far). In this episode, I'll show how we make use of context inside "svelte hooks". Ready your popcorn and welcome to:

🙃

Svelte For The Stubborn React Developer

Abstract

We defined hooks as:

A function to extract behavior that allows you to react to lifecycle and access the state and context.

This time We'll be focusing on the "access the state and context". Mostly on the context part, because the state part is svelte is not really a big issue (I'll come back to that). All functionality You get using context can be achieved by using stores (the same thing can be said about React Context and Redux/Mobx Store). For me, context come to solve "props drilling" problem. Where You need to pass around information to a deeply nested child object.

TL;DR

Sveltejs exports 2 funcions: getContext and setContext. The getContext function retrieves a context value that belongs to the closest parent component. Since setContext and getContext are just functions, they can be simply imported into svelte hooks and used as such. The component on which hooks are imported define the closest parent.

A Note About State

Since svelte compiles your svelte code into javascript, the component state is just variables. You don't have to worry too much about the lifecycle when You think about the state. The scoped variable reference get "invalidated" any time some part of the code changes it, and when invalidated, other parts of the code react to this change. In React functional components You have to use useState hooks and such because React runtime might "recalculate" your component in many situations. When this happens everything on the scope of that component gets recalculated.

What We Will Build

The first thing that pops into your head when You hear "context" is "themes", right? That's a very common use case, but I wanted to try something different. Context is great when there's something You have to pass around a lot between several components, especially when your app has a deeply nested structure. The app will have a simple structure (for simplicity sake), but imagine it has a really nested component tree.

We'll build a very simple app to display clocks from different timezones. Our app structure will look something like this:

<APP>
    <City> <!-- ⭠ SET Context A -->
        <Name></Name>
        <Date></Date> <!-- ⭠ USE Context B -->
        <Clock></Clock> <!-- ⭠ USE Context A -->
    </City>
    <City> <!-- ⭠ SET Context B -->
        <Name></Name>
        <Date></Date> <!-- ⭠ USE Context B -->
        <Clock></Clock> <!-- ⭠ USE Context B -->
    </City>
</APP>

As You can see in my detailed diagram above, the City component will set some context it's child Clock component will use.

Components Basic Structure

Let's start by creating the structure of our basic components, and then We gradually change them to implement what we want.

<!-- Clock.svelte -->
<script>
    let time = null;
</script>
<div>
    <h3>{time}</h3>
</div>
<!-- Date.svelte -->
<script>
    let date = null;
</script>
<div>
    <h3>{date}</h3>
</div>
<!-- City.svelte -->
<script>
    import Clock from './Clock.svelte'
    import Date from './Date.svelte'
    export let name;
    export let timezone; //⭠ will be used in a minute
    export let format; //⭠ will be used in 2 minutes
</script>
<div>
    <div>{name}</div>
    <Date></Date>
    <Clock></Clock>
</div>
<!-- App.svelte -->
<script>
    import City from './components/City.svelte';
</script>
<h2>Cities</h2>
<City name="New York City" timezone="America/New_York"></City>
<City name="Rio de Janeiro" timezone="America/Sao_Paulo"></City>

So... The idea here is that App.svelte has 2 cities (New York and Rio de Janeiro) and each has it's own timezone (and format, but ignore that for now). On the City.svelte some context value will be set, and this value will then be used by Date.svelte and Clock.svelte.

Now this could be done directly on the three components, but this is not so great for one basic reason:

It makes the components tightly coupled. This context logic would be scattered around these 3 components and if You have to change it for some reason, You would have to change everywhere (in a larger app this will not scale well).

We can do it better. If only we have learned in the last episode a way to extract behavior that allows you to react to lifecycle and access the state and context.

Wait a minute... That's right. A hook!

Implementing the hook

Soooo... We know our hook has to be able to access context values defined in parent components. Good thing svelte has just the right tools: getContext and setContext, which are just functions and can be imported and used in any file (such as our hook file). The catch here is that You need to call them during component initialization, so don't call them inside onMount, onDestroy, clickEvents, etc.

The setContext(key, value) defines a context value for the specified key. While getContext(key) returns the value for the key on the closest parent component. Our hook will be used by both: parent and child component, so it needs to export a way to set the context and to access the context. With that in mind here we go:

//useTimezone.js
// SET context
export function setTimezone({timezone, format}) {
  if (timezone) setContext('contextTimeZone', timezone);
  if (format) setContext('contextTimeFormat', format);
}

Exported function setTimezone simply set 2 context variables (if passed): contextTimeZone and contextTimeFormat. The first will hold the desired timezone and the second the desired date format. They will be indirectly used by Clock.svelte and Date.svelte.

Great! Now we need a way for both functions to access these context variables and do something with it. Our hook's heavy logic (or shared behavior).

//useTimezone.js
// SET context
export function setTimezone({timezone, format}) {
  if (timezone) setContext('contextTimeZone', timezone);
  if (format) setContext('contextTimeFormat', format);
}
//helper function
function getFormattedDate(format, options) {
  return new Intl.DateTimeFormat(format, options).format(new Date())
}

// ACCESS context and so something useful
export function getTime({onSecond, onDate}) {
  let interval;
  const timezone = getContext('contextTimeZone') || 'UTC';
  const format = getContext('contextTimeFormat') || 'default';
  if (onDate) onDate(getFormattedDate(format, timezone, {
    year: 'numeric',
    month: 'numeric',
    day: 'numeric',
    timeZone: timezone
  }));
  onMount(() => {
    if (onSecond) {
      interval = setInterval(() => {
        console.log('onsecond::'+format);
        onSecond(
            getFormattedDate(format, {
              hour: 'numeric',
              minute: 'numeric',
              second: 'numeric',
              timeZone: timezone
            })
        )
      }, 200);
    }
    return () => interval && clearInterval(interval);
  })
}

Let's analyze what's going on here by parts as jack the ripper would.

Function getFormattedDate is just a helper to.. well... format the date. Lame!

Function getTime is much more interesting. function basic structure can represented as such:

export function getTime({onSecond, onDate}) {
    //get context value (this is outside onMount)
    const timezone = getContext('contextTimeZone') || 'UTC';
....
    //call onDate callback passing the formated Date
    if (onDate) onDate(getFormattedDate(format, timezone, {
....   
    //register on components onMount a interval calling onSecond callback
    onMount(() => {
    if (onSecond) {
      interval = setInterval(() => {
....
    //register onDestroy event to clear interval (check last episode for details) 
    return () => interval && clearInterval(interval);
}

Now a few things to notice:

  • getContext calls happen outside onMount events
  • onSecond and onDate callbacks could be retrieved from context, but for learning, It's best not to get overcomplicated.

The important part is that getContext will look for the closest parent context relative to the component it gets imported into. Nice, but how can we use it?

Hooking The Components

Our first order of business is to set the context at the City.svelte component, for that we'll receive the values as props:

<!-- City.svelte -->
<script>
    import Clock from './Clock.svelte'
    import Date from './Date.svelte'
    import {setTimezone} from './useTimezone';
    export let name;
    export let timezone;
    export let format;
    setTimezone({timezone, format}); // ⭠ set context values
</script>
<div>
    <div>{name}</div>
    <Date></Date> <!-- ⭠ No props passed to the compoent -->
    <Clock></Clock> <!-- ⭠ No props passed to the compoent -->
</div>

and we need to pass the values as props in App.svelte

<!-- App.svelte -->
<script>
    import City from './components/City.svelte';
</script>
<h2>Cities</h2>
<City name="New York City" timezone="America/New_York" format="en-US"></City>
<City name="Rio de Janeiro" timezone="America/Sao_Paulo" format="pt-BR"></City>

* timezone values passed in a format understood by Intl (https://developer.mozilla.org/pt-BR/docs/Web/JavaScript/Reference/Global_Objects/DateTimeFormat)

Now a timezone and format are passed to each City instance, which sets them as context variables. This values now need to be consumed by Date.svelte and Clock.svelte

<!-- Date.svelte -->
<script>
    import {getTime} from './useTimezone'
    let date = null;
    getTime({onDate: (newTime)=> date=newTime})
</script>
<div>
    <h3>{date}</h3>
</div>
<!-- City.svelte -->
<script>
    import {getTime} from './useTimezone'
    let time = null;
    getTime({onSecond: (newTime)=> time=newTime})
</script>
<div>
    <h3>{time}</h3>
</div>

Both components set a variable (date and time), passes a callback to our Hook function to update its value.

With all in place all, our code is this:

<!-- App.svelte -->
<script>
    import City from './City.svelte';
</script>
<h2>Cities</h2>
<City name="New York City" timezone="America/New_York" format="en-US"></City>
<City name="Rio de Janeiro" timezone="America/Sao_Paulo" format="pt-BR"></City>

<!-- City.svelte -->
<script>
    import Clock from './Clock.svelte'
    import Date from './Date.svelte'
    import {setTimezone} from './useTimezone';
    export let name;
    export let timezone;
    export let format;
    setTimezone({timezone, format});
</script>
<div>
    <div>{name}</div>
    <Date></Date>
    <Clock></Clock>
</div>

<!-- Date.svelte -->
<script>
    import {getTime} from './useTimezone'
    let date = null;
    getTime({onDate: (newTime)=> date=newTime})
</script>
<div>
    <h3>{date}</h3>
</div>

<!-- Clock.svelte -->
<script>
    import {getTime} from './useTimezone'
    let time = null;
    getTime({onSecond: (newTime)=> time=newTime})
</script>
<div>
    <h3>{time}</h3>
</div>

And the final result is:

result-1

New York City Date is in 'MM/DD/YYYY' format as Rio de Janeiro is in 'DD/MM/YYYY', also, times are localized too.

Grandpa's Context

In the example above, Clock.svelte and Date.svelte gets the context from the City.svelte component. But context is evaluated from the closest parent, this means we can also define the context on the App.svelte. To create something like a default value. Check it out:

<!-- App.svelte -->
<script>
    import City from './City.svelte';
    import {setTimezone} from './useTimezone';
    setTimezone({format:'en-US'}); // ⭠ set value in App context
</script>
<h2>Cities</h2>
<!-- USES App context format value -->
<City name="New York City" timezone="America/New_York"></City>
<City name="Philadelphia" timezone="America/New_York"></City>
<!-- OVERRIDES App context format value -->
<City name="Rio de Janeiro" timezone="America/Sao_Paulo" format="pt-BR"></City>

This way, We define some value in App.svelte context, so New York and Philadelphia uses it, and Rio de Janeiro overrides it because a new context (closer to the component) is defined inside City.svelte from the 'format' props passed.

So again in our detailed diagram, We have something like:

<APP><!-- ⭠ SET APP context -->
    <City New York> <!-- ⭠ DO NOT set context -->
        <Name></Name>
        <Date></Date> <!-- ⭠ USE APP context -->
        <Clock></Clock> <!-- ⭠ USE APP context -->
    </City>
    <City Philadelphia> <!-- ⭠ DO NOT set context -->
        <Name></Name>
        <Date></Date> <!-- ⭠ USE APP context -->
        <Clock></Clock> <!-- ⭠ USE APP context -->
    </City>
    <City Rio de Janeiro> <!-- ⭠ SET Rio de Janeiro context -->
        <Name></Name>
        <Date></Date> <!-- ⭠ USE Rio de Janeiro context -->
        <Clock></Clock> <!-- ⭠ USE Rio de Janeiro context -->
    </City>
</APP>

Voilà!

result-2
Great! Now We've mastered context hooks. One more detail. Rember I said:

The catch here is that You need to call them during component initialization, so don't call them inside onMount, onDestroy, clickEvents, etc.

Well, How can we update the context value then?

See you in the next episode.

Top comments (2)

Collapse
 
nicpolhamus profile image
Nicolas Polhamus

Great stuff dude! I just got into svelte, it's been super fun to learn. I look forward to the next entries in the series!

Collapse
 
tiagobnobrega profile image
Tiago Nobrega

Thx. Great to know this stuff is useful for You