A.D
A.D

Reputation: 2270

Testing in React with preloadedState: redux store gets out of sync when having a manual import of store in a function

I'm writing tests for React using react-testing-library and jest and are having some problems figuring out how to set a preloadedState for my redux store, when another file imports the store.

I Have a function to set up my store like this

store.ts

  export const history = createBrowserHistory()
  export const makeStore = (initalState?: any) => configureStore({
    reducer: createRootReducer(history),
    preloadedState: initalState
  })
  export const store = makeStore()

and another js file like this

utils.ts

  import store from 'state/store'
  const userIsDefined = () => {
    const state = store.getState()
    if (state.user === undefined) return false
    ...
    return true
  }

I then have a test that looks something like this:

utils.test.tsx (the renderWithProvider is basically a render function that also wraps my component in a Render component, see: https://redux.js.org/recipes/writing-tests#connected-components)

  describe("Test", () => {
    it("Runs the function when user is defined", async () => {
      const store = makeStore({ user: { id_token: '1' } })
      const { container } = renderWithProvider(
        <SomeComponent></SomeComponent>,
        store
      );
    })
  })

And the <SomeComponent> in turn calls the function in utils.ts

SomeComponent.tsx

  const SomeComponent = () => {
    if (userIsDefined() === false) return (<Forbidden/>)
    ...
  }

Now.. What happens when the test is run seem to be like this.

  1. utils.ts is read and reads the line import store from 'state/store', this creates and saves a store variable where the user has not yet been defined.
  2. the utils.test.tsx is called and runs the code that calls const store = makeStore({ user: { id_token: '1' } }).
  3. The renderWithProvider() renderes SomeComponent which in turn calls the userIsDefined function.
  4. The if (state.user === undefined) returns false because state.user is still undefined, I think that's because utils.ts has imported the store as it were before I called my own makeStore({ user: { id_token: '1' } })?

The answer I want: I want to make sure that when call makeStore() again it updates the previously imported version of store that is being used in utils.ts. Is there a way to to this without having to use useSelector() and pass the user value from the component to my utils function?

e.g I could do something like this, but I'd rather not since I have a lot more of these files and functions, and rely much on import store from 'state/store':

SomeComponent.tsx

  const SomeComponent = () => {
    const user = useSelector((state: IState) => state.user)
    if (userIsDefined(user) === false) return (<Forbidden/>)
    ...
  }

utils.ts

  //import store from 'state/store'
  const userIsDefined = (user) => {
    //const state = store.getState()
    if (user === undefined) return false
    ...
    return true
  }

As I said above I'd prefer not to do it this way.


(btw I can't seem to create a fiddle for this as I don't know how to do that for tests and with this use case)

Thank you for any help or a push in the right direction.

Upvotes: 3

Views: 3056

Answers (1)

phry
phry

Reputation: 44326

This is just uncovering a bug in your application: you are using direct access to store inside a react component, which is something you should never do. Your component will not rerender when that state changes and get out of sync.

If you really want a helper like that, make a custom hook out of it:

  import store from 'state/store'
  const useUserIsDefined = () => {
    const user = useSelector(state => state.user)
    if (user === undefined) return false
    ...
    return true
  }

That way your helper does not need direct access to store and the component will rerender correctly when that store value changes.

Upvotes: 5

Related Questions