Pattern for making Storybook and useAuth play nice?

This is definitely not a Redwood specific question but I have only really ever used Storybook at a surface level and I cannot seem to find any best practicing on using Storybook and mocking out the useAuth hook for components that utilize it so we can display the component at varying auth states. The obvious approach that initially comes to mind is extracting the hook invocation to a higher level in the component tree and drilling its state back down into components, allowing us to easily pass per-determined values into the components at the story definition. However, that still means that page component stories will be dependent on actual auth state. Plus, in my opinion, this kind of defeats the purpose of using hooks in the first place. What is the best practice for making these parts play well together? How are y’all handling this in your apps?

In classic fashion I figured this out after a little hacking tonight. My solution was, for each story for a component, wrap it in the AuthContext.Provider component you can get from @redwood/auth/dist/AuthProvider and pass in whatever values you want useAuth() to return. Hopefully the snippet below saves someone a few hours at some point.

import { AuthContext } from '@redwoodjs/auth/dist/AuthProvider'
import AuthThermometer from './AuthThermometer'
import defaultAuthContext from '../../lib/defaultAuthContext'

export const LoggedOut = () => {
  return <AuthThermometer />
}

export const LoggedIn = () => {
  return (
    <AuthContext.Provider
      value={{
        ...defaultAuthContext,
        isAuthenticated: true,
        userMetadata: {
          email: 'test@example.com',
        },
      }}
    >
      <AuthThermometer />
    </AuthContext.Provider>
  )
}

export default { title: 'Components/AuthThermometer' }

2 Likes

Hi @asdfjackal, Thanks for the question and solution!

Somethings of note: depending on use case, I recommend using Storybook’s decorators.

For example, we can slightly modify your snippet using Story decorators:

// AuthThermometer.stories.js
import { AuthContext } from '@redwoodjs/auth/dist/AuthProvider'
import AuthThermometer from './AuthThermometer'
import defaultAuthContext from '../../lib/defaultAuthContext'

const Template = (args) => <AuthThermometer {...args} />

export const LoggedOut = Template.bind({})

export const LoggedIn = Template.bind({})
LoggedIn.decorators = [
  (Story) => (<AuthContext.Provider
      value={{
        ...defaultAuthContext,
        isAuthenticated: true,
        userMetadata: {
          email: 'test@example.com',
        },
      }}
    >
      <Story />
    </AuthContext.Provider>
  )
]

export default { title: 'Components/AuthThermometer' }

One benefit this decorator approach has is that it allows us more easily compose our stories. If we wanted to write a Logged In/Out story for a different component than AuthThermometer, we could extract the LoggedIn.decorators from above into something like:

// decorators.js
export const LoggedInDecorator = (Story) => (<AuthContext.Provider
      value={{
        ...defaultAuthContext,
        isAuthenticated: true,
        userMetadata: {
          email: 'test@example.com',
        },
      }}
    >
      <Story />
    </AuthContext.Provider>
  )

and then easily reuse it like so:

// AuthThermometer.stories.js
import { LoggedInDecorator } from 'foo/bar/decorators'
...
LoggedIn.decorators = [
  LoggedInDecorator
]
...
// ComponentOtherThanAuthThermometer.stories.js
import { LoggedInDecorator } from 'foo/bar/decorators'
...
LoggedIn.decorators = [
  LoggedInDecorator
]
...
1 Like

It seems that I just raised several, similar issues in Redwood-Stripe integration - currently unresolved issues . Would you care to comment?

Hi, can you explain where defaultAuthContext come from and it’s value ?

Your solution is promising but i can’t figure out this part…

Thank you !

Hi @skapin it is imported:

...
import defaultAuthContext from '../../lib/defaultAuthContext'
...

as I am not the original author of the code, we’d have to reach out to @asdfjackal to see if they are able to share the contents of that file. ( @asdfjackal any chance you are able to share what '../../lib/defaultAuthContext' looks like? )


Going off it’s name, and the rest of the example code given; you can make it a local variable and have it be something such as:

const defaultAuthContext = {
        isAuthenticated: false,
        userMetadata: {
          email: 'default@example.com',
        },
}

but ymmv as these types of things are highly dependent on your use case.


ps, afaik the spread operator doesn’t do nesting and I have not tested that code; so not sure how the defaultAuthContext behaves when overriding nested props such as userMetadata.