Understanding Context in Svelte (convert from React Context)
Asked Answered
R

2

16

I have a react app that uses ContextAPI to manage authentication and I am trying to implement a similar thing in Svelte.

In Authenticate.js I have this:

import React, { useContext, useState, useEffect } from "react"
import { auth } from "../firebase"

const AuthCt = React.createContext()

export function Auth() {
  return useContext(AuthCt)
}

export function AuthComp({ children }) {
  const [currentUser, setCurrentUser] = useState()
  const [loading, setLoading] = useState(true)

  function login(email, password) {
    return auth.signInWithEmailAndPassword(email, password)
  }

  function logout() {
    return auth.signOut()
  }

  useEffect(() => {
    const unmount = auth.onAuthStateChanged(user => {
      setCurrentUser(user)
      setLoading(false)
    })

    return unmount
  }, [])

  const value = {
    currentUser,
    login,
    signup
  }

  return (
    <AuthCt.Provider value={value}>
      {!loading && children}
    </AuthCt.Provider>
  )
}

This context is used in other Login.js component like this:

import { Auth } from "./Authenticate"

const Login = () => {
  const { currentUser, login } = Auth()

And in App.js I have:

import { AuthComp } from "./Authenticate";

function App() {
  return (
          <AuthComp>
               <div> All others go here </div>
          </AuthComp>
  );
}

How do I achieve this in Svelte, particularly the Authenticate context?

I haven't been able to do much in Svelte as I don't know how to proceed from here. So far I have AuthComp.svelte. I don't know if I am doing the right thing.

<script>
    import { getContext, setContext } from 'svelte';
    import  { auth } from '../firebase';
    import { writable } from 'svelte/store';

    let Auth = getContext('AuthCt')
    setContext('Auth', Auth)

    let currentUser;
    let loading = true;

    
     const unmount = auth.onAuthStateChanged(user => {
        currentUser = user;
        loading = false
     });


    function login(email, password) {
        return auth.signInWithEmailandPassWord(email,password)
    }
    
    function logout() {
       return auth.signOut()
    }
    
    const value = { currentUser, login, signUp }
    
</script>

<slot value={value}></slot>
Risner answered 23/5, 2021 at 19:39 Comment(0)
H
86

Migrating from React Context to Svelte

Context in Svelte and React may seem similar, but they are actually used differently. Because at the core, Svelte's context is much more limited. But that's ok. In fact, it actually will make your code simpler to write and understand.

In Svelte, you have more tools at your disposal for passing data round your app (and keeping it in sync) than just context. Each one does pretty much one thing (making everything predictable), and they do it well. Of these, you have:

  • Context
  • Stores
  • Props

As someone who's recently switched from React to Svelte, I think I can help explain some of the differences between each of these and help you avoid some of my conceptual mistakes. I'll also go over some differences in life cycle methods, because if you used to use useEffect, you might feel very lost since Svelte doesn't have an equivalent API. Yet combining everything together in Svelte will make everything simple.

Context

Context in Svelte does one thing: pass data from a parent component to any children (not necessarily direct children). Unlike in React, context is not reactive. It is set once when the component mounts, and then will not be updated again. We'll get to "reactive context" in a second.

<!-- parent.svelte -->

<script>
  import { setContext } from 'svelte'

  setContext('myContext', true)
</script>

<!-- child.svelte -->

<script>
  import { getContext } from 'svelte'

  const myContext = getContext('myContext')
</script>

Notice that context involves two things, a key and a value. Context is set to a specific key, then the value can be retrieved using that key. Unlike React, you do not need to export functions to retrieve the context. Both the key and value for the context can be anything. If you can save it to a variable, you can set it to context. You can even use an object as a key!

Stores

If you have data that needs to stay in sync in multiple places across your app, stores are the way to go. Stores are reactive, meaning they can be updated after they're created. Unlike context in either React or Svelte, stores don't simply pass data to their children. Any part of your app can create a store, and any part of your app can read the store. You can even create stores outside of Svelte components in separate JavaScript files.

// mystore.ts
import { writable } from 'svelte/store'

// 0 is the initial value
const writableStore = writable(0)

// set the new value to 1
writableStore.set(1)

// use `update` to set a new value based on the previous value
writableStore.update((oldValue) => oldValue + 1)

export { writableStore }

Then inside a component, you can subscribe to the store.

<script>
  import { writableStore } from './mystore'

</script>

{$writableStore}

The dollar sign subscribes to the store. Now, whenever the store is updated, the component will rerender automatically.

Using stores with context

Now that we have stores and context, we can create "reactive context"(a term I just made up, but it works). Stores are great because they're reactive, and context is great to pass data down to the children components. But we can actually pass a store down through context. This makes the context reactive and the store scoped.

<!-- parent.svelte -->

<script>
  import { setContext } from 'svelte'
  import { writable } from 'svelte/store'

  const writableStore = writable(0)
  setContext('myContext', writableStore)
</script>

<!-- child.svelte -->

<script>
  import { getContext } from 'svelte'

  const myContext = getContext('myContext')
</script>

{$myContext}

Now, whenever the store updates in the parent, the child will also update. Stores can of course do much more than this, but if you were looking to replicate React context, this is the closest you can get in Svelte. It's also a lot less boilerplate!

Using "reactive context" with "useEffect"

Svelte does not have an equivalent of useEffect. Instead, Svelte has reactive statements. There's a lot on these in the docs/tutorial, so I'll keep this brief.

// doubled will always be twice of single. If single updates, doubled will run again.
$: doubled = single * 2

// equivalent to this

let single = 0
const [doubled, setDoubled] = useState(single * 2)

useEffect(() => {
  setDoubled(single * 2)
}, [single])

Svelte is smart enough to figure out the dependencies and only run each reactive statement as needed. And if you create a dependency cycle, the compiler will yell at you.

This means that you can use reactive statements to update stores (and hence update the context). Here, the valueStore will be update on every keystroke to the input. Since this store is passed down through context, any child can then get the current value of the input.

<script>
  import { setContext } from 'svelte'
  import { writable } from 'svelte/store'

  // this value is bound to the input's value. When the user types, this variable will always update
  let value

  const valueStore = writable(value)

  setContext('inputContext', valueStore)

  $: valueStore.set(value)

</script>

<input type='text' bind:value />

Props

For the most part, props function exactly the same in React and Svelte. There are a few differences because Svelte props can take advantage of two-way binding (not necessary, but possible). That's really a different conversation though, and the tutorial is really good at teaching two-way binding with props.

Authentication in Svelte

Ok, now after all of that, let's look at how you'd create an authentication wrapper component.

  • Create an auth store
  • Pass the auth store down via context
  • Use Firebase's onAuthStateChanged to listen to changes in auth state
  • Subscribe to the auth store in the child
  • Unsubscribe from onAuthStateChanged when the parent is destroyed to prevent memory leaks
<!-- parent.svelte -->
<script>
  import { writable } from 'svelte/store'
  import { onDestroy, setContext } from 'svelte'

  import { auth } from '../firebase'

  const userStore = writable(null)

  const firebaseUnsubscribe = auth.onAuthStateChanged((user) => {
    userStore.set(user)
  })

  const login = (email, password) => auth.signInWithEmailandPassWord(email,password)

  const logout = () => auth.signOut()

  setContext('authContext', { user: userStore, login, logout })

  onDestroy(() => firebaseUnsubscribe())

</script>

<slot />

<!-- child.svelte -->
<script>
  import { getContext } from 'svelte'

  const { login, logout, user } = getContext('authContext')
</script>

{$user?.displayName}
Hackler answered 25/5, 2021 at 2:42 Comment(7)
Really GREAT answer!Risner
Hey @Hackler do you know the impact of calling auth.onAuthStateChanged() like you did above (i.e. directly in the component) vs calling it in onMount()?Amandy
Unless you're doing server side rendering, I don't think there's a noticeable difference. Calling it outside of onMount will run it sooner since onMount waits until the component is on the screen. With server side rendering, I've been just checking to see if typeof window !== 'undefined (since onAuthStateChanged needs to run in the browser) instead of waiting for onMount most of the time. Not sure if this is the best way, but it's worked for me.Hackler
What is the purpose of keeping a store scoped? If you could just import a store into both the parent and child, then context isn't needed. Is there an advantage to this or is this an anti-pattern? Btw this comparison has helped me so much.Gem
You're correct, context isn't needed to use stores. And it's not an anti-pattern to do so. The main thing to be aware of when using a store that way is you'll need to initialize it in a separate file—you can't export stores from inside components (as far as I know). Using context with stores is great if you want to scope the store, or you don't want to deal with hooking up the context. One example are <List> and <ListItem> components. ListItems should always be inside lists, and there can be multiple lists. In this case, it's idiomatic to scope the context. Does that help?Hackler
Really helpful explanation, thanks @Nick! In fact there is a way to export a store from a component from inside a <script context="module"> here's a REPLMontez
Holy smoke! you really nailed it!Gallager
E
1

In Svelte, context is set with setContext(key, value) in a parent component, and children can access the value object with getContext(key). See the docs for more info.

In your case, the context would be used like this:

<script>
    import { getContext, setContext } from 'svelte';
    import  { auth } from '../firebase';
    import { writable } from 'svelte/store';

    // you can initialize this to something else if you want
    let currentUser = writable(null)
    let loading = true
    
    // maybe you're looking for `onMount` or `onDestroy`?
    const unmount = auth.onAuthStateChanged(user => {
        currentUser.set(user)
        loading = false
    });


    function login(email, password) {
        return auth.signInWithEmailandPassWord(email,password)
    }
    
    function logout() {
       return auth.signOut()
    }
    
    const value = { currentUser, login, signUp }

    setContext('Auth', value) 
    
</script>

{#if !loading}
    <slot></slot>
{/if}

Here, currentUser, login, and signup (not sure where that's coming from?) are set as context with setContext(). To use this context, you would probably have something like this:

<!-- App -->
<AuthComp>
    <!-- Some content here -->
    <Component />
</AuthComp>

<!-- Component.svelte -->
<script>
    import { getContext } from 'svelte'

    const { currentUser, login, signup } = getContext('Auth')
    // you can subscribe to currentUser with $currentUser
</script>
<div>some content</div>

As written in the docs, context is not reactive, so currentUser is first converted into a store so it can be subscribed to in a child. As for the useEffect, Svelte has lifecycle functions that you can use to run code at different points, such as onMount or onDestroy.

If you're new to Svelte, their tutorial is very comprehensive with plenty of examples that you can refer back to.

Hope this helped!

Emad answered 24/5, 2021 at 1:0 Comment(4)
This is awesome. About your question - maybe you're looking for onMount or onDestroy? - The idea was to replicate the useEffect to change the currentUser and loading values when the auth state changes. I guess what I would need if afterUpdate but I don't know if that's what will give me the same thing as what the useEffect does. I am not sure if the unmount function will get me that.Risner
@Risner Without knowing more about what the auth.onAuthStateChanged function does, I don't think I can help you with that (maybe that's a different question since this one is mainly about context). It is possible that you don't need any lifecycle hooks at all and can just call it directly.Emad
onAuthStateChanged adds an observer for changes to the user's sign-in state and is triggered when the user signs in or out.Risner
I guess you're using firebase auth. I've used it too and what I did was just subscribe to onAuthStateChanged in onMount of App.svelte. Then, based on the user parameter I set some reactive values that are used to determine what to render.Amandy

© 2022 - 2024 — McMap. All rights reserved.