Updating React context outside of a component

Published on in JavaScript, React and Testing

React context is normally updated inside the React app. Here's a hacky way how I circumvented that restriction in a test file.

Table of contents

Example problem

Let's say I have a React component called MyComponent which uses a React context called MyContext.

When the value of the context changes, the component does something, e.g. makes an Adobe Analytics tracking call:

function MyComponent() {
  const myContext = useContext(MyContext)

  useEffect(() => {
    window._satellite.track(myContext)
  }, [myContext])

  return null
}

And then I want to write unit tests for that... But how can I do that since React contexts can't be easily updated outside of React components?

Hacky workaround

I realized that in the test file, I can pass an empty object for the wrapper component (App in this case) as a prop (named contextUpdater).

Inside App, I can store a reference to the context value setter function (setValue in this case) under the given prop object.

Then, in a test case, I can call contextUpdater.setValue – outside of the component!

Like so:

/* MyComponent.test.js */

describe('MyComponent', () => {
  function App({ contextUpdater = {} }) {
    const [value, setValue] = useState('initial value')

    // This hack allows us to update the context value outside of the component
    contextUpdater.setValue = setValue

    return (
      <MyContext.Provider value={[value, setValue]}>
        <MyComponent />
      </MyContext.Provider>
    )
  }

  beforeEach(() => {
    global._satellite = { track: jest.fn() }
  })

  it('makes a tracking call when MyContext changes', () => {
    const contextUpdater = {}

    render(<App contextUpdater={contextUpdater} />)
    expect(global._satellite.track).toHaveBeenCalledTimes(1)

    act(() => contextUpdater.setValue('new value'))
    expect(global._satellite.track).toHaveBeenCalledTimes(2)

    act(() => contextUpdater.setValue('third value'))
    expect(global._satellite.track).toHaveBeenCalledTimes(3)
  })
})

It works! IQ 200! Proud moment of the week.

Too hacky?

So yeah, actually this feels very hacky, probably because this is hacky.

Another question is whether the test is too implementation-specific. On the other hand, MyComponent directly depends on MyContext, so I'm not sure.

Anyway, the test passes, so I think this is good enough for now.