reduxjs / redux-toolkit

The official, opinionated, batteries-included toolset for efficient Redux development
https://redux-toolkit.js.org
MIT License
10.67k stars 1.16k forks source link

How to query with multiple parameters #1028

Closed simonauner closed 1 year ago

simonauner commented 3 years ago

Hi!

I started experimenting with the new RTK query and I must say I really enjoy being able to shave off all that boilerplate that used to be actions and reducers.

I have one question though to which I haven't found any examples or documentation on https://deploy-preview-1016--redux-starter-kit-docs.netlify.app ...

How to query with multiple parameters?

What I would like to do: service.js

export const myApi = createApi({
    reducerPath: 'myApi',
    baseQuery: fetchBaseQuery({ baseUrl: '/api/v2/' }),
    endpoints: (builder) => ({
        getReport: builder.query({
            query: (group, period) =>
                `/group/${groupKey}/${period}`,
        }),
    }),
});
export const { useGetReportQuery } = myApi;

component.jsx

import { useGetReportQuery } from './service';
....
    const { data: report, isLoading } = useGetReportQuery(
        group,
        period
    );

But it seems period is always undefined.

I've modified the example here to show the issue https://deploy-preview-1016--redux-starter-kit-docs.netlify.app/usage/rtk-query/examples

https://codesandbox.io/s/rtk-query-demo-forked-bkvpw?file=/src/app/services/counter.ts

If you add a counter and open the sandbox console and increment you will see that it prints

$ query count hi undefined

I would expect it to print

$ query count hi hello

(see Counter.tsx:7)

I tried passing in the arguments to useGetReportQuery as an array useGetReportQuery([group, period]) but that wasn't correct either :)

What am I missing?

phryneas commented 3 years ago

Query accepts only one argument, as that would otherwise make useGetReportQuery very problematic to write (you are passing period in as options there, because options are the fixed second argument).

Pass an object in instead:

        getReport: builder.query({
            query: ({group, period}) =>
                `/group/${groupKey}/${period}`,
        }),

    useGetReportQuery({
        group,
        period
    });
simonauner commented 3 years ago

Ah, fantastic. Thanks!

timhughes commented 2 years ago

Any idea how to do this with Typescript as the signature for it appears to be (method) query(arg: string): string | FetchArgs so you cannot supply an object

phryneas commented 2 years ago

@timhughes You need to specify the type. The signature is build.query<ReturnType, QueryArgument>({ ... })

KostasChili commented 1 year ago

Hi, I wanted to ask is there a way to include mutliple parameters plus an option ? I want to make a query where I need an id and a date to be pased in as parameters and a skip option to make the query conditional.

  const [skipAppsQuery,setSkipApssQuery]= useState(true)
  const tempDate="2022-12-25"
  const {
    data: apps,
    isLoading: isAppsLoading,
    isUninitialized:isAppsUninitialized,
    isSuccess: isAppsSuccess,
    isError: isAppError,
    error: appError,
  } = useRetrieveAppointmentsPublicQuery({id,tempDate},{skipAppsQuery})
retrieveAppointmentsPublic:builder.query({
      query:({id,date="2022-12-25"})=>({
        url:`/shops/public/appointments/${id}/${date}`,
        method:'GET',     
       })
    })
markerikson commented 1 year ago

@KostasChili : I'm pretty sure that's just useRetrieveAppointmentsPublicQuery({id, date: tempDate}, {skip: skipAppsQuery})

KostasChili commented 1 year ago

Allthough you are right and I am really sorry for uploading invalid code, my problem is that the option skip is not executed. The query happens the moment the component mounts. I fixed this line of code (tempDate is now date) and the problem remains

markerikson commented 1 year ago

@KostasChili I can confirm that if you pass {skip: true}, the query will not run as long as it's still true. So, there's got to be something about what you're passing in that's not matching expectations. Can you show the actual component?

KostasChili commented 1 year ago

I have completely removed the setSkipAppsQuery to make sure it will always be true as set in the useState. Thanks for the help and I apologize beforehand if this is something really simple.I am new to react and programming in general :)

import Box from "@mui/material/Box";
import TextField from "@mui/material/TextField";
import { useParams, useNavigate } from "react-router-dom";
import { useGetShopQuery } from "../shops/shopsApiSlice";
import {
  useMakeAppointmentMutation,
  useRetrieveAppointmentsPublicQuery,
} from "../appointments/appointmentsApiSlice";
import { useEffect, useState } from "react";
import { Button, MenuItem, Typography } from "@mui/material";
import { AdapterDateFns } from "@mui/x-date-pickers/AdapterDateFns";
import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider";
import { DesktopDatePicker } from "@mui/x-date-pickers/DesktopDatePicker";
import { getDate, getMonth, getYear, format, addDays } from "date-fns";

export default function CreateAppointment() {
  const navigate = useNavigate();
  const [skipAppsQuery,setSkipAppsQuery] = useState(true);
  const [name, setName] = useState("");
  const [lastName, setLastName] = useState("");
  const [canSelectDate, setCanSelectDate] = useState(false);
  const [dateFormated, setDateFormated] = useState(addDays(new Date(), 1));
  const [date, setDate] = useState("");
  const [dateSubmitted, setDateSubmitted] = useState(false);
  const [service, setService] = useState("");
  const [canSelectTime, setCanSelectTime] = useState(false);
  const [startTime, setStartTime] = useState("");
  const [email, setEmail] = useState("");
  const [comments, setComments] = useState("");
  const { id } = useParams();

  {
    /* QUERIES */
  }
  const {
    data: shop,
    isLoading,
    isSuccess,
    isError,
    error,
  } = useGetShopQuery(id);

  const [setAppointment, { isLoading: isAppLoading, isSuccess: isAppSuccess }] =
    useMakeAppointmentMutation();

    const tempDate="2022-12-25"
  const {
    data: apps,
    isLoading: isAppsLoading,
    isUninitialized:isAppsUninitialized,
    isSuccess: isAppsSuccess,
    isError: isAppError,
    error: appError,
  } = useRetrieveAppointmentsPublicQuery({id,date:tempDate},{skipAppsQuery})

  {
    /* END OF  QUERIES */
  }

  useEffect(() => {
    if ((name, lastName, email, service) !== "") {
      setCanSelectDate(true);
    }
  }, [name, lastName, email, service]);

  useEffect(() => {
    if (dateSubmitted && date) {
      // setCanSelectTime(true);

    }
  }, [dateSubmitted]);

  if(isAppsSuccess)
  {
    console.log(apps)
  }

  {
    /* FUNCTIONS FOR THE DATE PICKER - TODO The date Picker could be moved to a component of its own with all function needed */
  }

  const handleDateSubmit = (e) => {
    setDateFormated(e);
    setDate(
      `${getDate(dateFormated)}-${getMonth(dateFormated) + 1}-${getYear(
        dateFormated
      )}`
    );
    setDateSubmitted(true);
  };

  function disableWeekends(date) {
    return date.getDay() === 0 || date.getDay() === 6;
  }

  let availableSlots = null;
  let occTimeSlots = null;

  if (isAppsSuccess) {
    occTimeSlots = apps.appList.map((ap) => {
      return `${ap.startTime}-${ap.endTime}`;
    });

    availableSlots = apps.allTimeSlots.filter(
      (item) => !occTimeSlots.includes(item)
    );
  }

  const canSave =
    [name, lastName, date, service, startTime, email].every(Boolean) &&
    !isLoading;

  {
    /* FORM SUBMITION */
  }

  const handleAppSubmit = () => {
    if (canSave) {
      setAppointment({
        id,
        date,
        service,
        customerName: name + " " + lastName,
        startTime: startTime.split("-")[0],
        email,
        comments,
      });
    }
    setName("");
    setLastName("");
    setDate("");
    setService("");
    setStartTime("");
    setEmail("");
    setComments("");
    navigate(`/shops/public/${id}/appsuccess`);
  };

  return (
    <Box
      component="form"
      sx={{
        "& .MuiTextField-root": { m: 1, width: "25ch" },
      }}
      noValidate
      autoComplete="off"
    >
      <div>
        <Typography variant="h6">
          Καλώς ήρθατε στην σελίδα του καταστήματος {shop?.title}
        </Typography>
        <Typography>
          Παρακαλώ συμπληρώστε τα παρακάτω στοιχεία για να κλείσετε το ραντεβού
          σας
        </Typography>
      </div>
      <div>
        <TextField
          id="name"
          label="Ονομα"
          value={name}
          onChange={(e) => setName(e.target.value)}
        />
        <TextField
          id="lastName"
          label="Επίθετο"
          value={lastName}
          onChange={(e) => setLastName(e.target.value)}
        />
        <TextField
          id="email"
          label="email"
          type="email"
          value={email}
          onChange={(e) => setEmail(e.target.value)}
        />
      </div>
      <div>
        <TextField
          id="service"
          label="Υπηρεσία"
          value={service}
          onChange={(e) => setService(e.target.value)}
        />

        <TextField
          id="comments"
          label="Σχόλια Για το Ραντεβού σας"
          multiline
          maxRows={5}
          value={comments}
          onChange={(e) => setComments(e.target.value)}
        />

        {/* DATE  PICKER CONDITIONAL RENDERING */}
        {canSelectDate ? (
          <>
            <LocalizationProvider dateAdapter={AdapterDateFns}>
              <DesktopDatePicker
                label="Ημερομηνία"
                inputFormat="dd/MM/yyyy"
                value={dateFormated}
                onChange={handleDateSubmit}
                shouldDisableDate={disableWeekends}
                renderInput={(params) => <TextField {...params} />}
              />
            </LocalizationProvider>
          </>
        ) : null}

        {/* TIME  PICKER CONDITIONAL RENDERING */}
        {canSelectTime ? (
          <>
            <TextField
              select
              id="startTime"
              label="Ωρα"
              value={startTime}
              selected
              onChange={(e) => {
                setStartTime(e.target.value);
              }}
            >
              {availableSlots !== null ? (
                availableSlots.map((slot) => {
                  return <MenuItem value={slot}>{slot}</MenuItem>;
                })
              ) : (
                <MenuItem>Δεν υπάρχουν διαθέσημα ραντεβού</MenuItem>
              )}
            </TextField>
          </>
        ) : null}
      </div>

      <Button disabled={!canSave} onClick={handleAppSubmit} variant="contained">
        Καταχωρηση Ραντεβου
      </Button>
    </Box>
  );
}
markerikson commented 1 year ago

@KostasChili : yeah, you're missing the actual skip field :) It must literally be a field named skip, exactly that, in the options object, and not skipAppsQuery. It's okay if the state variable is named skipAppsQuery, but you have to put that into the skip options field. So, like I said, this is what you need:

useRetrieveAppointmentsPublicQuery({id,date:tempDate},{skip: skipAppsQuery})

Also, as a side note: setting state in a useEffect is generally not a good approach. For example, you can calculate canSelectDate as "derived" data there in the render logic, without any useState or useEffect:

KostasChili commented 1 year ago

@KostasChili I can confirm that if you pass {skip: true}, the query will not run as long as it's still true. So, there's got to be something about what you're passing in that's not matching expectations. Can you show the actual component?

I am sorry for wasting your time. Sometimes fatique can make as panic and make things more complicated than they really are. My problem is that I named the skip option as skipAppsQuery ...

Sorry for the inconvenience

markerikson commented 1 year ago

No worries! I'd also recommend taking the time to read through the React beta docs pages to get more comfortable with some of the React-specific concepts, like the "Might Not Need an Effect" page I just linked.

Big-Zude commented 1 year ago

for me am having trouble using multiple params on mutation${baseURL}/route/${id},{productId: productId},{headers: {authorization: ${token}}

I am kindly asking for assistance configuring this mutation


postProduct: builder.mutation({
      query: ({ userId, product, token }) => ({
        url: `/product/${userId}`,
        method: 'POST',
        body: {
          productId: `${product}`,
          headers: { authorization: `${token}` },
        },
      }),
      invalidatesTags: ['products'],
    }),
phryneas commented 1 year ago

@Big-Zude what is your question?

Big-Zude commented 1 year ago

I am trying to use the RTK Query to make a mutation query to add a product to a user's account

The request I am trying to make is an axios POST request to the endpoint ${baseURL}/route/${id}, with a body containing the product ID and headers containing the authorization token.

axios.post(`${baseURL}/route/${id}`,{productId: productId},{headers: {authorization: ${token}})

However, when I try to use the RTK mutation query, It is not executing at all.

Here is the code I am using for the RTK mutation query:

postProduct: builder.mutation({
      query: ({ userId, product, token }) => ({
        url: `/product/${userId}`,
        method: 'POST',
        body: {
          productId: `${product}`,
          headers: { authorization: `${token}` },
        },
      }),
      invalidatesTags: ['products'],
    }),

Is this the correct way you use multiple params in a mutation or is there a better way of doing it? I would greatly appreciate any help in resolving this issue.

KostasChili commented 1 year ago

@Big-Zude what is your specific problem ? Does the use of the query generate an error on the front-end ? Does it generate an error no the back-end ? Edit : How are you using the mutation on your application ?

Big-Zude commented 1 year ago

@Big-Zude what is your specific problem ? Does the use of the query generate an error on the front-end ? Does it generate an error no the back-end ? Edit : How are you using the mutation on your application ?

I managed to figure it out, I made a mistake on the headers part.

Big-Zude commented 1 year ago

I am trying to use the RTK Query to make a mutation query to add a product to a user's account

The request I am trying to make is an axios POST request to the endpoint baseURL/route/{id}, with a body containing the product ID and headers containing the authorization token.

axios.post(`${baseURL}/route/${id}`,{productId: productId},{headers: {authorization: ${token}})

However, when I try to use the RTK mutation query, It is not executing at all.

Here is the code I am using for the RTK mutation query:

postProduct: builder.mutation({
      query: ({ userId, product, token }) => ({
        url: `/product/${userId}`,
        method: 'POST',
        body: {
          productId: `${product}`,
          headers: { authorization: `${token}` },
        },
      }),
      invalidatesTags: ['products'],
    }),

Is this the correct way you use multiple params in a mutation or is there a better way of doing it? I would greatly appreciate any help in resolving this issue.

This works for me, apologies for the unclear issue

postProduct: builder.mutation({
      query: ({ userId, product, token }) => ({
        url: `/product/${userId}`,
        method: 'POST',
        body: {
          productId: `${product}`,
        },
        headers: { authorization: `${token}` },
      }),
      invalidatesTags: ['products'],
    }),
KostasChili commented 1 year ago

From my understanding yes this is the correct way

okechukwu0127 commented 1 year ago

Hi!

I started experimenting with the new RTK query and I must say I really enjoy being able to shave off all that boilerplate that used to be actions and reducers.

I have one question though to which I haven't found any examples or documentation on https://deploy-preview-1016--redux-starter-kit-docs.netlify.app ...

How to query with multiple parameters?

What I would like to do: service.js

export const myApi = createApi({
    reducerPath: 'myApi',
    baseQuery: fetchBaseQuery({ baseUrl: '/api/v2/' }),
    endpoints: (builder) => ({
        getReport: builder.query({
            query: (group, period) =>
                `/group/${groupKey}/${period}`,
        }),
    }),
});
export const { useGetReportQuery } = myApi;

component.jsx

import { useGetReportQuery } from './service';
....
    const { data: report, isLoading } = useGetReportQuery(
        group,
        period
    );

But it seems period is always undefined.

I've modified the example here to show the issue https://deploy-preview-1016--redux-starter-kit-docs.netlify.app/usage/rtk-query/examples

https://codesandbox.io/s/rtk-query-demo-forked-bkvpw?file=/src/app/services/counter.ts

If you add a counter and open the sandbox console and increment you will see that it prints

$ query count hi undefined

I would expect it to print

$ query count hi hello

(see Counter.tsx:7)

I tried passing in the arguments to useGetReportQuery as an array useGetReportQuery([group, period]) but that wasn't correct either :)

What am I missing?

Hi,

Its really anoying why the RTK query accepts only one parameter. The only way out if for you to pass a single param of object as your props then destructure it inside your rtk query or call the key params directly

INSIDE YOUR COMPONENT CODE

    const payload = {
      data: {
        email: signup?.data?.user?.email,
        code: randNumber.toString(),
      },
    };

      const params = {
        id: id,
        data: payload,
      };

       updateOTP(params)
        .then((data) => {
          // console.log({ data });

        })

INSIDE YOUR RTK QUERY

updateOTP: builder.mutation({
  query: (params) => ({
    url: `${apiConfig.Onboarding.OTP}/${params.id}`,
    method: "PUT",
    body: params.data,
  }),
}),
FullStack0verfl0w commented 1 year ago

I'm having a trouble with skipping.

Here is the query:

class GetUsersOfGroup {
    id: number;
    skip: number;
    take: number;
}

getUsersOfGroupByID: build.query<User[], GetUsersOfGroup>({
            query: ({ id, ...query }) => ({
                url: `/user/group/id/${id}/users`,
                method: "GET",
                params: query,
            }),
        }),

And here is how I call it:

const { data } = useGetUserGroupQuery(); // Get some data before fetching users
const { data: users } = useGetUsersOfGroupByIDQuery({ id: data?.id ?? skipToken, skip: 0, take: 30 }); // Here we should wait until we get data

The problem is that TypeScript only checks if the object I pass is skipToken, but not its fields. Because of this I get: Type 'number | symbol' is not assignable to type 'number'. Is there any workaround except // @ts-ignore?

EskiMojo14 commented 1 year ago

misclick 😅

Skip token is used in place of the entire argument, not as a property of it.

FullStack0verfl0w commented 1 year ago

Skip token is used in place of the entire argument, not as a property of it.

Well, I can see that.

Is there any workaround except // @ts-ignore?

EskiMojo14 commented 1 year ago

passing skip token as a property of your argument will not skip. that's why you're getting the error, because it won't work.

instead, use it in place of your argument, like i said.

const { data: users } = useGetUsersOfGroupByIDQuery(data?.id ? { id: data.id, skip: 0, take: 30 } : skipToken);