A Solution to Control Global State with Storybook

ยท

2 min read

I recently started working with both Storybook and Recoil.

In order to make the Storybook controls faithful to the component's capabilities, it was necessary to somehow connect Storybook's controls to Recoil.

Storybook usually controls a component by changing values that are passed in as props - but to set this up, it might require changing the component to accept props that aren't going to be included in the component when it's used for real.

This defeats the purpose of a component library/Storybook - you shouldn't need to modify a component every time you implement it.

My solution was to create a component specifically for Storybook that implements the original component as well as a controller component.

import OriginalComponent from 'src/components'
import ComponentController from './ComponentController'

interface ComponentStoryProps {
  onClick: () => void // example prop for the main component 
  recoilProp: string // example prop to control Recoil/global state
}

function ComponentStory(args: ComponentStoryProps) {

  // innerArgs may be many more props
  const { recoilProp, ...innerArgs } = args 

  return (
    <>
      <ComponentController recoilProp={recoilProp} />
      <OriginalComponent {...innerArgs} />
    </>
  )
}

export default ComponentStory

Now inside the actual OriginalComponent.stories.ts file, I can add props that are passed into either the controller or the component itself.

Inside the controller is something like this:


import { useEffect } from 'react'
import useExampleHook from '.src/hooks/useExampleHook'

interface ComponentControllerProps {
  recoilProp: string
}

// using value 'props' here to make it easier to distinguish
// between properties with the same name.
function ComponentController(props: ComponentControllerProps) {
  const { componentState setComponentState } = useExampleHook()

  // Convert Storybook control props into recoil state updates
  useEffect(() => {
    if (props.recoilProp !== componentState.recoilProp) {
      setComponentState(props.recoilProp)
    }
  }, [props.recoilProp])

  return null
}

export default ComponentController

This component is responsible for checking its props (the Storybook's control state, then) against Recoil and updating it.

This will propagate into the main component via the hook.

In this example, I show a custom hook that allows access to global state. You can call useRecoilState in this component too, or set it up using any other global state - the same principle should work for other global state managers.

ย