Sapper: Derived stores don't work with session from @sapper/app

Created on 14 Aug 2019  路  17Comments  路  Source: sveltejs/sapper

I don't know if this is a feature request or bug report.

There's no way to create a store derived from session as calling const {session} = stores() outside of a component produces the error:

Error: Function called outside component initialization

question

Most helpful comment

Here's the hack I ended up with.

//src/stores/index.js

import axios from 'axios'
import { readable, derived, writable, get } from 'svelte/store';
import { stores } from '@sapper/app'

//my derived session
export const session = (() => {
    const { subscribe, set, update } = writable(false)
    let sapSession
    return {
        init() {
            sapSession = stores().session
            sapSession.subscribe(val => {
                set(val)
            })
        },
        set(val) { sapSession.set(val) },
        subscribe,
        unsubscribe() {
            sapSession()
        }
    }
})()

initializing in the root layout

<-- src/routes/_layout.svelte -->

<script>
    import { session } from "../stores";
    session.init()
</script>

Now the session is available outside of component initiation and no more having to call {session} = store() first

<-- src/routes/timemachines/fluxcapacitator.svelte -->

<script>
    import { session } from "../stores";
</script>

<button on:click={()=>$session.foo = "bar"}>change a session variable</button>

I don't know why session was originally restricted to component initialization and I can't help but wonder if I broke something by removing the restriction. I'll keep a fire extinguisher near.

All 17 comments

You can create a function that returns a derived based on the session store, but you can only call that function in a page component script. (Not module context)

Let me get this straight.

  • My JWT token is stored in the session.
  • An authorized http request requires the JWT token.
  • The JWT tokenis only available in component initiation.
  • Therefore authorized request calls aren't immediately available outside of component initiation.

Is there a clean way to add a store method that uses the jwt token? Ie. a hydration request.

@jakobrosenberg not solving this exact problem but it's not ideal to store your JWT token inside the application. It is better stored as a httpOnly cookie, and the browser will pass it to your API when required.

@antony Thanks. How would that work with SSR and SSOT for the token?

Not sure what your concern with SSOT is, but it works well with SSR since your JWT is available in req.cookies:

Here's how I do it (obviously abbreviated for clarity):

// server.js
import jwt from 'jsonwebtoken'
import cookieParser from 'cookie-parser'

polka()
  .use(
    ...
    cookieParser(),
    (req, res, next) => {
      return sapper.middleware({
        session: (req) => {
          const token = req.cookies['cookieName']
          const profile = token ? jwt.decode(token) : false
          return { authenticated: !!profile, profile }
        }
      })(req, res, next)
    }
  )
  ...

@antony How would you pass your token to axios or fetch?

fetch({
  credentials: 'include'
})

The nice thing about a cookie is that you don't need to pass it anywhere. It's automatic.

Also, noting the use of the word "would". This is how https://www.beyonk.com works and has done for a year or more. I use this internally to avoid a lot of boilerplate: https://github.com/beyonk-adventures/sapper-httpclient

As for axios - I don't know what the equivalent is, but I wouldn't use Axios anyway.

@antony I'm still confused. How would that solve SSR calls to various microservices that rely on web-tokens?

My current approach looks similar to the example below, but it feels like a lot of hacking making it work.

    const tokenA = typeof document !== 'undefined' ?
        Cookies.get('service-a-jwt') :
        get(stores().session)['service-a-jwt']

@jakobrosenberg When you call a microservice, this.fetch (I assume you are using preload) will populate your request with the JWT as long as you { credentials: 'include' }. Every SSR request is initated by the user's browser requesting the page, and this.fetch is aware of the cookies passed to it by that browser request.

@antony It seems it all boils down to the magic included in this.fetch. I'll try have a look at the source code and see if I can grok it.

Quick question. If I receive a httpOnly cookie from micro service from domaina.com can I pass that cookie to the Sapper server on domainb.com?

@jakobrosenberg I have a feeling that cookies can't be cross-domain, but they can be cross-subdomain.

yes - the magic is in this.fetch, it's a special Sapper thing for allowing authenticated SSR requests.

Here's the hack I ended up with.

//src/stores/index.js

import axios from 'axios'
import { readable, derived, writable, get } from 'svelte/store';
import { stores } from '@sapper/app'

//my derived session
export const session = (() => {
    const { subscribe, set, update } = writable(false)
    let sapSession
    return {
        init() {
            sapSession = stores().session
            sapSession.subscribe(val => {
                set(val)
            })
        },
        set(val) { sapSession.set(val) },
        subscribe,
        unsubscribe() {
            sapSession()
        }
    }
})()

initializing in the root layout

<-- src/routes/_layout.svelte -->

<script>
    import { session } from "../stores";
    session.init()
</script>

Now the session is available outside of component initiation and no more having to call {session} = store() first

<-- src/routes/timemachines/fluxcapacitator.svelte -->

<script>
    import { session } from "../stores";
</script>

<button on:click={()=>$session.foo = "bar"}>change a session variable</button>

I don't know why session was originally restricted to component initialization and I can't help but wonder if I broke something by removing the restriction. I'll keep a fire extinguisher near.

@jakobrosenberg Does the preload work with this?

I would also love sessions "outside component initialization", if it is sane.

@peterbabic

I don't know. I gave up on Sapper and ported the router to Svelte.

Is there any official advice on accessing session in a regular JS file, i.e. outside a component?

// queries.js: needs access to some API tokens
import { stores } from '@sapper/app'

const { session } = stores()
// Error: Function called outside component initialization

stores() needs to be called during some component's initialization because it uses the context API to keep the sessions of the different requests separate during server side rendering.

Thanks for clearing that up. So how to access environment variables outside components? @rollup/plugin-replace? I have a file of queries that are called in preload functions across various components. IIUC, @rollup/plugin-replace would only make them accessible after bundling? Does preload happen before bundling?

Was this page helpful?
0 / 5 - 0 ratings

Related issues

milosdjakovic picture milosdjakovic  路  3Comments

benmccann picture benmccann  路  3Comments

Rich-Harris picture Rich-Harris  路  3Comments

UnwrittenFun picture UnwrittenFun  路  4Comments

SARFEX picture SARFEX  路  3Comments