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
]
...

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