Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cannot get react-query (useQuery hook) to work with Storybook with external API #12489

Closed
sdivelbiss opened this issue Sep 15, 2020 · 9 comments

Comments

@sdivelbiss
Copy link

I am using storybook to develop my react components in isolation. I am running into an issue with using react-query (useQuery) with storybook. Implementing the component in the app, I get no errors and things work as expected.

async function searchStationsByCallLetter(_, param) {
  const request = {
    callLettersView: param, 
  };

  const { data } = await ApiService().makeApiCall(API_ENDPOINTS.BROADCAST_LINEUP.SEARCH_STATIONS, request, HTTP_POST);
  return data;
}

export default function useSearchStationsByCallLetter(inputValue = '') {
  return useQuery(['searchStationsByCallLetter', inputValue], searchStationsByCallLetter);

I have a custom react hook useSearchStationsByCallLetter that returns the useQuery hook. React-query should be using searchStationsByCallLetter as the api call. ApiService and makeApiCall are a custom axios setup.

The error I get in storybook is:

TypeError: Object(...) is not a function
    at _callee$ (AddLineupHooks.js:8)
    at tryCatch (runtime.js:63)
    at Generator.invoke [as _invoke] (runtime.js:293)
    at Generator.next (runtime.js:118)
    at asyncGeneratorStep (asyncToGenerator.js:3)
    at _next (asyncToGenerator.js:25)
    at asyncToGenerator.js:32
    at new Promise (<anonymous>)
    at Object.<anonymous> (asyncToGenerator.js:21)
    at Object.searchStationsByCallLetter [as queryFn] (AddLineupHooks.js:8)

Desktop (please complete the following information):

  • OS: Mac
  • Browser: chrome
  • Version: latests
@jonniebigodes
Copy link
Contributor

@sdivelbiss i've kept track of your issue while i was working on getting Storybook and React Query working nicely with Axios.

Here's what i did:

  • Created a new React app with create-react-app.
  • Installed Storybook, react-query, react-query-devtools, axios and msw (mock service worker) for testing purposes.
  • Changed .storybook/preview.js to the following to allow react-query cache provider,the react-query-devtools and my mock service working with Storybook:
import React from 'react'

import { ReactQueryCacheProvider, QueryCache } from "react-query";
import { ReactQueryDevtools } from "react-query-devtools";

const queryCache = new QueryCache();

export const parameters = {
  actions: { argTypesRegex: "^on[A-Z].*" },
};
/**
 * adds a Storybook decorator to get the cache and dev tools showing for each story
 */
export const decorators=[
  (story) => (
    <ReactQueryCacheProvider queryCache={queryCache}>
      {story()}
      <ReactQueryDevtools />
    </ReactQueryCacheProvider>
  ),
]
// get the mocked handlers (endpoints) and starts the service worker
if (typeof global.process === 'undefined') {
  const { worker } = require("../src/mocks/workers");
  worker.start();
}
  • Created a barebones component for the use-query with the following code:
import React from "react";
import Axios from "axios";
import { useQuery } from "react-query";
/**
 * Component to demonstrate how to mock data (GET request) with react-query and axios
 * 
 */
const AxiosRestExample = () => {
  // use query to fetch something with axios out of the url (this case a mocked one)
  const { data, error, isLoading } = useQuery("rest-ricky", async () => {
    const { data } = await Axios.get("/ricky-rest");
    return data;
  });
  if (isLoading) return <h1>Loading...Go grab a coffee</h1>;
  if (error) {
    return (
      <>
        <h2>Upsy daisy something went wrong</h2>
        <h2>{error.message}</h2>
      </>
    );
  }
  return (
    <div>
      <h1>{data.name}</h1>
      <p>{data.description}</p>
    </div>
  );
};

export default AxiosRestExample;
  • Created a file called AxiosRestExample.stories.js with the following:
import React from "react";
import AxiosRestExample from "./AxiosRestExample";

export default {
  title: "React Query Rest (AXIOS) GET Example",
  component: AxiosRestExample,
};

const Template = (args) => <AxiosRestExample {...args} />;

export const Default = Template.bind();
  • To test out a POST method i've created one more component called AxiosRestPostExample.js with the following:
import React, { useState } from "react";
import Axios from "axios";
import { useMutation } from "react-query";
import PropTypes from "prop-types";

/**
 * function to send something to the url
 * @param {String} textValue
 */
async function SendMessage({ textValue }) {
  try {
    const { data } = await Axios.post("/ricky-post", {
      item: textValue,
    });
    return data;
  } catch (error) {
    console.log(`SEND MESSAGE ERROR:${error}`);
  }
}

/**
 * Component to demonstrate how to mock data (POST request) with react-query and axios
 * @param {String} initialvalue a dummy prop for the component 
 */
const AxiosRestPostExample = ({ initialvalue }) => {
  const [textValue, setTextValue] = useState(initialvalue);

  const [mutate, { status, error }] = useMutation(SendMessage, {
    onSuccess: () => {
      console.log("POSTED SOMETHING");
    },
    onError: () => {
      console.log(`SOMETHING WENT WRONG:\n${error}`);
    },
  });
  return (
    <div>
      <input value={textValue} onChange={(e) => setTextValue(e.target.value)} />
      <button
        onClick={() => mutate({ textValue })}
        disabled={status === "loading" || !textValue}
      >
        Post something
      </button>
    </div>
  );
};
AxiosRestPostExample.propTypes = {
  initialvalue: PropTypes.string,
};
AxiosRestPostExample.defaultProps = {
  initialvalue: "Ricky Sanchez",
};

export default AxiosRestPostExample;
  • Included a stories file for this component as well called AxiosRestPostExample.stories.js:
import React from "react";
import AxiosRestPostExample from "./AxiosRestPostExample";

export default {
  component: AxiosRestPostExample,
  title: " React Query Rest Post Example with Axios",
};

const Template = (args) => <AxiosRestPostExample {...args} />;

export const Default = Template.bind({});

export const WithRandomItem = Template.bind({});
WithRandomItem.args = {
  initialvalue: "Oingo Boingo",
};
  • Configured msw by adding two files :
    • src/mocks/server with the following inside:
import { setupWorker } from 'msw'
import { handlers } from './handlers'
// This configures a Service Worker with the given request handlers.
export const worker = setupWorker(...handlers)
  • src/mocks/handlers with the following:
import { rest } from "msw";
/**
 * a list of handlers (endpoints) to emulate (mock) a actual implementation
 */
export const handlers = [
  rest.get("/ricky-rest", (req, res, ctx) => {
    return res(
      ctx.status(200),
      ctx.json({
        name: "Rick Sanchez",
        description: "Scientist extraordinaire",
      })
    );
  }),
  rest.post("/ricky-post", (req, res, ctx) => {
    const {item}= req.body
    return res(
      ctx.status(200),
      ctx.json({
        message: `hey hey ${item}`,
      })
    );
  }),
];
  • Issued yarn storybook and i get the following:
    • The GET request (use-query):
      Storybook-React-Query-get
  • And from the POST component story i get the following:
    Storybook-React-Query-POST
    Storybook-React-Query-Use-Mutation-POST-DEVTOOLS

I know that this is a rather small and possibly skewed approach to the problem. But it seems that Storybook and React-Query will work fine together.

If you can provide additional information about how you're implementing the associated story file and how you've setup Axios we could take a look at it and probably provide you with a more concrete answer.

Or i can hoist up this small reproduction to a repo and you can take a look at it in your time. Let us know and we'll move from there.

Stay safe

@stale
Copy link

stale bot commented Dec 26, 2020

Hi everyone! Seems like there hasn't been much going on in this issue lately. If there are still questions, comments, or bugs, please feel free to continue the discussion. Unfortunately, we don't have time to get to every issue. We are always open to contributions so please send us a pull request if you would like to help. Inactive issues will be closed after 30 days. Thanks!

@stale stale bot added the inactive label Dec 26, 2020
@somanythings
Copy link

somanythings commented Jan 31, 2021

@jonniebigodes thanks for this!

A couple of questions where I've hit stumbling blocks, if you don't mind.
On const { worker } = require("../src/mocks/workers"); did you mean const { worker } = require("../src/mocks/server");
(or name the mocks/server file workers ?

Two, with that change, the msw service worker won't register. index.js:1050 [MSW] Failed to register a Service Worker: this browser does not support Service Workers (see https://caniuse.com/serviceworkers), or your application is running on an insecure host (consider using HTTPS for custom hostnames). it's supported on the browser I'm using (chrome 88)
BUT, I am running storybook with http, not https. Though it seems with your screenshots, you are too?

Thanks in advance.

EDIT (answering myself):
For anyone else who stumbles across this, the answer to the next bit was staring me in the face (in a browser console log). You must run npx msw init

npx msw init public
Initializing the Mock Service Worker at "/home/"...

Service Worker successfully created!
/public/mockServiceWorker.js

@stale stale bot removed the inactive label Jan 31, 2021
@jonniebigodes
Copy link
Contributor

Closing this as we have already documentation within the Build Pages with Storybook docs. The same pattern can be applied.

@mhesham32
Copy link

I think @jonniebigodes answer will be perfect for most scenarios but there is a problem if you're using something like Firebase where the back-end endpoints are hard to track not like a regular API. Does anyone have a suggestion about this case? I think we can mock the data of the query by using queryClient.setQueryData('rest-ricky', mockFn) but will this be applied for all the references of this query or some of them will still call the firebase query

@Arahis
Copy link

Arahis commented Dec 6, 2022

Maybe my question is not exactly regards this topic, but may be someone can answer. The case when I need to show only the loading state from react-query, what should I do in this case?

@webbiscuit
Copy link

@Arahis the above is now mostly replaced by msw-storybook-addon. To simulate loading you can create a story like this:

export const Loading = Template.bind({});
Loading.parameters = {
  msw: {
    handlers: [
      rest.get("http://localhost:3000/endpoint", (req, res, ctx) => {
        return res(ctx.status(200), ctx.json({}), ctx.delay("infinite"));
      }),
    ],
  },
};

Although you do have to be careful, because React Query will now be going to the cache for this infinite wait, which might mess your other stories up!

I bypass this behaviour by setting cacheTime to 0 when setting up the mock query client:

const mockedQueryClient = new QueryClient({
  defaultOptions: {
    queries: {
      retry: false,
      cacheTime: 0,
    },
  },
});

const Template: ComponentStory<typeof YourThing> = () => (
  <QueryClientProvider client={mockedQueryClient}>
    <YourThing/>
  </QueryClientProvider>
);

@santilp95
Copy link

Sorry for my bad english but this answer doesn't work
my preview.js is this

const mockedQueryClient = new QueryClient();

addDecorator((Story) => (
  <QueryClientProvider client={mockedQueryClient}>
    <ThemeProvider theme={daviLabsTheme}>
      <ReactQueryDevtools />
      <Story />
    </ThemeProvider>
  </QueryClientProvider>
))

if (typeof global.process === 'undefined') {
  const { worker } = require("../mocks/server");
  worker.start();
}

but the console in firefox and chrome said

index.js:511 Uncaught (in promise) Error: [MSW] Failed to register a Service Worker for scope ('http://localhost:6006/') with script ('http://localhost:6006/mockServiceWorker.js'): Service Worker script does not exist at the given path.

Did you forget to run "npx msw init <PUBLIC_DIR>"?

Learn more about creating the Service Worker script: https://mswjs.io/docs/cli/init
    at getWorkerInstance (index.js:511:1)
    at async startWorkerInstance (index.js:1645:1)


I tried with Mock Service Worker and execute npx msw init public/ but doesnt work , the react query is work when the api is ON, but I need to creaate and put the mock

in another discution mentioned the same problem but the solution doesnt work #Service Worker script does not exist at the given path
#77

Please help me to create correct mock

my handler.ts : I tried to create mock in two ways but doesnt wotk this same handler I put in storybook but doens't work

import { graphql, rest } from 'msw'
/**
 * a list of handlers (endpoints) to emulate (mock) a actual implementation
 */
export const handlers = [
    graphql.query('listBanksAch', (req, res, ctx) => {
        return res(
            ctx.data({
                tpAccounts: [
                    {
                        "id": 1,
                        "name": "Cuenta de ahorros"
                    },
                    {
                        "id": 2,
                        "name": "Cuenta Corriente"
                    },
                ]
            }),
        )
    }),
    rest.post("http://localhost:4005/graphql", (req, res, ctx) => {
        return res(
            ctx.status(200),
            ctx.json([
                {
                    "id": 1,
                    "name": "Cuenta de ahorros"
                },
                {
                    "id": 2,
                    "name": "Cuenta Corriente"
                },
            ])
        );
    })
]

My component is :

interface Props {
    /**
    * Open / close dialog
    */
    open: boolean;
    /**
     * Close dialog
     */
    onClose: () => void;
    /**
    * Function get form values
    */
    handleSubmit: (data: any) => void;
    /**
     * Init data to form
     */
    initData?: IFormACH;
}

export const DialogFormACH: FC<Props> = ({
    onClose,
    handleSubmit,
    open,
    initData,
}) => {


    const { banksACHData , typesAccountsACH} = useGetDataACH(open)


    const onSubmit = (data: any) => {
        handleSubmit(data);
        onClose();
    }
    return (
        <LayoutDialog
            open={open}
            onClose={onClose}
            fullWidth
            title={'ACH'}
            isLoading={banksACHData.isLoading || typesAccountsACH.isLoading}
        >
            <FormACH
                onSubmit={onSubmit}
                optionsBank={banksACHData.data!}
                optionsTypeCount={typesAccountsACH.data!}
                initData={initData}
            />
        </LayoutDialog>
    )
}

the hook is next:

const getTypesBankACH = async (): Promise<ListBanksAch[]> => {
    const query =
        `query  {
        listBanksAch {
            name
            nit
        }
    }`
    const { data: { data } } = await msListApi().post<IResponseBankACH>('/graphql', { query })

    return data.listBanksAch;
};

const geTypesAccounts = async (): Promise<TpAccount[]> => {
    const query =
        `query  {
        tpAccounts {
            id
            name
        }
    }`
    const { data: { data } } = await msListApi().post<ITypesAccountResponse>('/graphql', { query })

    return data.tpAccounts;
};

export const useGetDataACH = (open:boolean) => {
    const banksACHData = useQuery(['listBankACH',open],
        () => getTypesBankACH(),
        {
            enabled: open
        }
    )

    const typesAccountsACH = useQuery(['listAccountsACH',open],
        () => geTypesAccounts(),
        {
            enabled: open
        }
    )

    return {
        banksACHData,
        typesAccountsACH,
    }

and storybook is

export default {
    title: 'Molecules/Dialog/DialogFormACH',
    component: DialogFormACH,
    args: {
        handleSubmit: action('handleSubmit')
    },
    parameters: {
        layout: 'centered',

    },
} as ComponentMeta<typeof DialogFormACH>;

const Template: ComponentStory<typeof DialogFormACH> = (args) => {
    const [isOpen, setIsOpen] = useState(false);
    return (
        <>
            <Button onClick={() => setIsOpen(true)} variant='contained'>Open Dialog</Button>
            <DialogFormACH {...args} open={isOpen} onClose={() => setIsOpen(false)} />
        </>
    )
}


export const DialogFormACHExample = Template.bind({});
DialogFormACHExample.args = {};

Why mockServiceWorker not read in the browser and how put mock in storybook? , thanks

@iamswain25
Copy link

if you're using suspense, you need to manually set the staleTime zero

export const mockQueryClient = new QueryClient({
  defaultOptions: {
    queries: {
      suspense: true,
      staleTime: 0,
    },
  },
});

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

9 participants