facebookexperimental / Recoil

Recoil is an experimental state management library for React apps. It provides several capabilities that are difficult to achieve with React alone, while being compatible with the newest features of React.
https://recoiljs.org/
MIT License
19.5k stars 1.18k forks source link

Testing custom hook with recoil atom inside #2292

Open maliyshock opened 8 months ago

maliyshock commented 8 months ago

I have a custom hook useImageActions which returns a set of functions

handleAddImages
handleUpdateImagesPosition
handleDelete
handleFinishEdit

Each function makes some things like mutations with GQL, or else and update the state with the result with recoil.

From your documentation i used RecoilObserver and i came up with this solution

describe("", () => {
  it("", async () => {
    const onChange = jest.fn();
    const atom = imagesAtom(PRODUCT_SET_ID);

    const wrapper = ({ children }: PropsWithChildren<{}>) => (
      <RecoilRoot initializeState={({set}) => set(atom, INIT_IMAGES)}>
        <RecoilObserver node={atom} onChange={onChange}/>
        <Providers>
          <MockedProvider addTypename={false} mocks={[mock]}>
            {children}
          </MockedProvider>
          </Providers>
      </RecoilRoot>
    );

    const { result } = renderHook(
      () => useImageActions({ productSetId: PRODUCT_SET_ID }),
      { wrapper },
    );

    await act(async () => {
      await result.current.handleAddImages(ADD_IMAGES);
    });

    expect(onChange).toHaveBeenCalledTimes(2);
    expect(onChange).toHaveBeenCalledWith(INIT_IMAGES);
    expect(onChange).toHaveBeenCalledWith([...INIT_IMAGES, ...ADD_IMAGES]);
  });
});

As you can see i initialized first state with initializeState, i also provided atom to the observer I ve been using renderHook which gonna be rendered behind the scene in some test component. For my atom i use families because i need to, and PRODUCT_SET_ID is unique ID for each "family" for that purposes. I am using the same PRODUCT_SET_ID for atom, and i am using the same imagesAtom as i use in the project.

it looks like this

export const imagesAtom = atomFamily<ExtendedImage[], string>({
  key: "images",
  default: [],
});

My hook uses this atom as well inside, to provide a possibility to the functions (image actions) to change the state.

So basically, my expectations that the hook and RecoilRoot and RecoilObserver should share the same atom family, because all of them use the same imagesAtom and all of them have the same PRODUCT_SET_ID. But i guess it is not true

The issue It looks like they are disconnected. During the test when i invoke

   await act(async () => {
      await result.current.handleAddImages(ADD_IMAGES);
    });

I get amount of calls 1 and last toHaveBeenCalledWith does not have ADD_IMAGES its still just have INIT_IMAGES only

My question is how can i make this kind of connection in the right way? What am i missing? Or what i am doing it is an antipattern and it should be used in some other way?