Related
I'm trying to pass the queryKey variable alongside the pageParam in useInfiniteQuery? I've tried for a while but:
Cannot get the page data
In some cases, the data is rendered repeatedly.
How should I pass the variables?
export const fetchInfiniteVariants = async (
filters = {},
{ pageParam = 0 }
) => {
const records = await axios.get(baseURL, {
headers: authHeader,
params: {
pageSize: 24,
offset: pageParam,
fields: [
"name",
"packshot",
"packshot_size",
"brand_logo",
"price",
"slug",
],
// filterByFormula: `({validated} = 1)`,
filterByFormula: `(${filterByFields(filters)})`,
"sort[0][field]": "priority",
"sort[0][direction]": "asc",
},
})
return records
}
export const useInfiniteVariantsQuery = (
initialRecords,
offset,
filters = { brand: "HAY" }
) => {
const infiniteVariantsQuery = useInfiniteQuery(
["infiniteVariants", filters],
() => fetchInfiniteVariants(filters),
{
initialStale: true,
staleTime: 6000,
getNextPageParam: (lastPage, pages) => lastPage.data.offset,
}
)
return {
...infiniteVariantsQuery,
}
}
The queryFn you're passing to useInfiniteQuery has request context as a parameter to that callback, as documented on the useInfiniteQuery page:
Receives a QueryFunctionContext object with the following variables:
queryKey: EnsuredQueryKey: the queryKey, guaranteed to be an Array
pageParam: unknown | undefined
You can destructure and retrieve your queryKey from that, as below:
export const useInfiniteVariantsQuery = (
initialRecords,
offset,
filters = { brand: "HAY" }
) => {
const infiniteVariantsQuery = useInfiniteQuery(
["infiniteVariants", filters],
({ queryKey, pageParam }) => fetchInfiniteVariants(queryKey[1], pageParam),
{
initialStale: true,
staleTime: 6000,
getNextPageParam: (lastPage, pages) => lastPage.data.offset,
}
)
return {
...infiniteVariantsQuery,
}
}
I have a very basic prototype of app that allows to book a seat. User selects the seat/seats, clicks book, patch request with available: false is sent to the fake api (json-server) with React Query, library invalidates the request and immediately shows response from the server.
Database structure looks like this:
{
"hallA": [
{
"id": 1,
"seat": 1,
"available": false
},
{
"id": 2,
"seat": 2,
"available": true
},
{
"id": 3,
"seat": 3,
"available": false
}
]
}
and the logic for selecting, booking seats looks like this:
const App = () => {
const { data, isLoading } = useGetHallLayout("hallA");
const [selected, setSelected] = useState<
{ id: number; seat: number; available: boolean }[]
>([]);
const handleSelect = useCallback(
(seat: { id: number; seat: number; available: boolean }) => {
const itemIdx = selected.findIndex((element) => element.id === seat.id);
if (itemIdx === -1) {
setSelected((prevState) => [
...prevState,
{ id: seat.id, seat: seat.seat, available: !seat.available },
]);
} else {
setSelected((prevState) =>
prevState.filter((element) => element.id !== seat.id)
);
}
},
[selected]
);
const takeSeat = useTakeSeat({
onSuccess: () => {
useGetHallLayout.invalidate();
},
});
const sendRequest = useCallback(() => {
selected.forEach((item) =>
takeSeat.mutateAsync({ id: item.id, hall: "hallA" })
);
setSelected([]);
}, [selected, takeSeat]);
return (
<>
{!isLoading && (
<ConcertHall
layout={data}
onSeatSelect={handleSelect}
activeSeats={selected}
/>
)}
<button disabled={isLoading} onClick={sendRequest}>
Take selected
</button>
</>
);
};
Queries look like this:
export const useGetHallLayout = (hall: string) => {
const { data, isLoading } = useQuery(["hall"], () =>
axios.get(`http://localhost:3000/${hall}`).then((res) => res.data)
);
return { data, isLoading };
};
export const useTakeSeat = (options?: UseMutationOptions<unknown, any, any>) =>
useMutation(
(data: { hall: string; id: number }) =>
axios.patch(`http://localhost:3000/${data.hall}/${data.id}`, {
available: false,
}),
{
...options,
}
);
useGetHallLayout.invalidate = () => {
return queryClient.invalidateQueries("hall");
};
The problem of the above code is that I perform very expensive operation of updating each id in a for each loop (to available: false) and query invalidates it after each change not once all of them are updated.
The question is: is there any better way to do this taking into account the limitations of json-server? Any batch update instead of sending request to each and every id seperately? Maybe some changes in a logic?
Thanks in advance
You can certainly make one mutation that fires of multiple requests, and returns the result with Promise.all or Promise.allSettled. Something like:
useMutation((seats) => {
return Promise.allSettled(seats.map((seat) => axios.patch(...))
})
then, you would have one "lifecycle" (loading / error / success) for all queries together, and onSuccess will only be called once.
Another gotcha I'm seeing is that you'd really want the hall string to be part of the query key:
- useQuery(["hall"], () =>
+ useQuery(["hall", hall], () =>
I have page where I want to add some actualities. These actualities will be first set in the Sanity and then fetched via Next.js .
My Sanity schema
export default{
name:"actuality",
title:"Aktuality",
type:"document",
fields:[
{
name:"headline",
title:"Nadpis",
type:"string"
},
{
name:"publishedAt",
title:"Datum zveřejnění",
type:"datetime"
},
{
name:"body",
title:"Text",
type:"blockContent"
}
],
preview:{
select:{
title:"headline",
}
}
}
Problem is in fetching the data.
If I do this it will work, but will return only first actuality in the Sanity
export const getServerSideProps = async (pageContext: any) => {
const query = `*[ _type == "actuality"][0]`;
const recipe = await client.fetch(query);
console.log(recipe);
if (!recipe) return { props: null, notFound: true };
else
return {
props: {
headline: recipe.headline,
publishedAt: recipe.publishedAt,
body: recipe.body,
},
};
};
But if I remove the [0] it will throw error: "Reason: undefined cannot be serialized as JSON. Please use null or omit this value."
What do I need to change in order to get an array of Actualities?
Wrap the response in a data object to serialize and call {data} in your page props like this:
export const getServerSideProps = async (pageContext: any) => {
const query = `*[ _type == "actuality"]`;
const recipe = await client.fetch(query);
console.log(recipe);
if (!recipe) return { props: null, notFound: true };
else
return {
props: {
data: {
headline: recipe.headline,
publishedAt: recipe.publishedAt,
body: recipe.body,
},
},
};
};
Few things:
it returns an array if you remove [0], you can do return just the data, regardless an array or not.
props: {
data: recipe
}
if you want to return single data with obj vallue as multiple props
props: {...recipe}
Next.js enables us to define dynamic routes in our apps using the brackets [param]. It allows us to design URL in such a way that for example language is passed as parameter. When no route matches the user is redirected to error page.
The idea is simple and documentation abut dynamic routes in Next.js is rather limited. Does anybody know if it's possible to assign a default value to dynamic route parameter?
There are docs pages about i18n routing and redirects and special support for locale parameters (which I have not used personally).
In a more general sense, it sounds like what you want is optional catch all routes.
You can define a file in your foo directory with the name [[...slug]].js. The params which correspond to a path like /foo/us/en is { slug: ["us", "en"] } where each segment of the path becomes an element of the slug array.
You can use getStaticPaths to generate all of the known country/language pairs. Setting fallback: true allows for the user to enter another combo and not get a 404.
export const getStaticPaths = async () => {
return {
paths: [
{ params: { slug: ["us", "en"] } },
{ params: { slug: ["us", "es"] } },
{ params: { slug: ["ca", "en"] } },
{ params: { slug: ["ca", "fr"] } },
/*...*/
],
fallback: true, // allows unknown
};
};
As far as redirection, it depends on whether you want an actual redirect such that typing in /foo leads to /foo/us/en or if those are two separate pages which show the same content. I'm going to assume that we want an actual redirect.
You'll convert from slug to props in your getStaticProps function. This is also where you implement your redirects. I'm going to assume that you have (or can create) some utility functions like isValidCountry(country) and getDefaultLanguage(country)
export const getStaticProps = async ( context ) => {
const [country, language] = context.params?.slug ?? [];
// if there is no country, go to us/en
if (!country || !isValidCountry(country)) {
return {
redirect: {
statusCode: 301, // permanent redirect
destination: "/foo/us/en",
},
};
}
// if there is no language, go to the default for that country
if (!language || !isValidLanguage(language, country)) {
return {
redirect: {
statusCode: 301, // permanent redirect
destination: `/foo/${country}/${getDefaultLanguage(country)}`,
},
};
}
// typical case, return country and language as props
return {
props: {
country,
language,
},
};
};
There are things that you can do in the component itself with useRouter and isFallback, but I'm not sure if it's needed. In dev mode at least I'm getting proper redirects.
/foo/ca/en - ok
/foo/ca/fr - ok
/foo/ca/xx - redirects to /foo/ca/en
/foo/ca - redirects to /foo/ca/en
/foo - redirects to /foo/us/en
Complete code with TypeScript types:
import { GetStaticPaths, GetStaticProps } from "next";
export interface Props {
country: string;
language: string;
}
export default function Page({ country, language }: Props) {
return (
<div>
<h1>
{country} - {language}
</h1>
</div>
);
}
const pairs = [
["us", "en"],
["us", "es"],
["ca", "en"],
["ca", "fr"],
];
const isValidCountry = (c: string) => pairs.some(([cc]) => cc === c);
const isValidLanguage = (l: string, c: string) =>
pairs.some(([cc, ll]) => cc === c && ll === l);
const getDefaultLanguage = (c: string) =>
pairs.find(([cc]) => cc === c)?.[1] ?? "en";
export const getStaticProps: GetStaticProps<Props, { slug: string[] }> = async (
context
) => {
const [country, language] = context.params?.slug ?? [];
// if there is no country, go to us/en
if (!country || !isValidCountry(country)) {
return {
redirect: {
statusCode: 301, // permanent redirect
destination: "/foo/us/en",
},
};
}
// if there is no language, go to the default for that country
if (!language || !isValidLanguage(language, country)) {
return {
redirect: {
statusCode: 301, // permanent redirect
destination: `/foo/${country}/${getDefaultLanguage(country)}`,
},
};
}
// typical case, return country and language as props
return {
props: {
country,
language,
},
};
};
export const getStaticPaths: GetStaticPaths<{ slug: string[] }> = async () => {
return {
paths: pairs.map((slug) => ({
params: { slug },
})),
fallback: true, // allows unknown
};
};
This isn't the first time a question like this has been asked, but for some reason I'm having a real hard time wrapping my head around Observables and RxJs, so I thought I'd ask for some clarification.
I am trying to write an epic which will do the following:
Receive a string
Make an API call based on that string
Make three more (independent) API calls based on the results of the first, and write them into state.
I have read about concatMap and forkJoin which seem like they could be useful, as concatMap should allow me to execute multiple actions consecutively, and forkJoin becuase it can be used for returning the results of the final 3 API calls as a single array.
Here's some code to try and give an idea of what I want. Apologies for the repetitive question, but I'd be grateful for an answer.
export const apiFetchEpic = (
action$: any,
state$: any,
{ apiClient }: IDependencies,
) =>
action$.pipe(
ofType(API_FETCH),
debounce(() => timer(300)),
withLatestFrom(state$),
mergeMap(
([
{
payload: { config, uri },
},
]: [{ payload: IApiFetchActionProps }, any]) => {
return from(apiClient.get(uri, config)).pipe(
mergeMap(res =>
of(
apiSetAction(res), // <-- I need the results of this to be passed into the next calls
apiCompleteAction({ uri }),
),
),
catchError(error =>
of(apiCompleteAction({ uri }), apiErrorAction(error)),
),
)
},
),
)
I solved it like this:
import { ofType, combineEpics } from 'redux-observable'
import {
mergeMap,
withLatestFrom,
catchError,
debounce,
switchMap,
} from 'rxjs/operators'
import { from, of, timer, forkJoin } from 'rxjs'
import { IDependencies } from '../../configure-store'
import { apiErrorAction } from '../../reducers/errors/actions'
import {
TOC_FETCH,
tocSetAction,
ITocFetchActionProps,
tocCompleteAction,
} from '../../reducers/table-of-contents/actions'
import { defaults } from 'lodash'
// Can probably use forkJoin here, and create multiple API calls as a parallel observable
// https://github.com/patricktran/react-redux-observable-epic-forkjoin-apis/blob/master/src/epics/index.js
export const tocFetchEpic = (
action$: any,
state$: any,
{ apiClient }: IDependencies,
) => {
return action$.pipe(
ofType(TOC_FETCH),
debounce(() => timer(300)), // anti-DDoS
withLatestFrom(state$),
mergeMap(
([
{
payload: { instance },
},
]: [{ payload: ITocFetchActionProps }, any]) => {
const params = { 'myParam': instance, limit: 1 }
return from(apiClient.get('endpoint', { params })).pipe(
mergeMap(
(result): any => {
const def = {
limit: -1,
'newParam': result.data.results[0],
}
const configurations = [
{
uri: 'endpointOne',
config: defaults({}, def),
},
{
uri: 'endpointTwo',
config: defaults(
{
'specialParam': 'table',
'specialParamTwo': 'http://example.com',
'iso2:lang': 'en',
},
def,
),
},
{
uri: 'endpointThree',
config: defaults(
{
'anotherSpecialParam': 'role',
},
def,
),
},
]
// Create a bunch of observables
const parallelObservables = configurations.map(api =>
from(
apiClient.get(api.uri, { params: api.config }),
).pipe(
switchMap(response => of(response)),
catchError(err =>
of(apiErrorAction(err), tocCompleteAction({ instance })),
),
),
)
// Return a forkJoin of them.
return forkJoin(
parallelObservables,
(names: any, schoolNames: any, storeNames: any) => {
//projection
return { // <- The object returned here is ultimately what gets written into state
instance: result.data.results[0],
tables: names.data.results,
labels: [
...schoolNames.data.results,
...storeNames.data.results,
],
}
},
).pipe(
switchMap(response => {
return of(
tocSetAction(response),
tocCompleteAction({ instance }),
)
}),
)
},
),
catchError(error =>
of(tocCompleteAction({ instance }), apiErrorAction(error)),
),
)
},
),
)
}
export default combineEpics(tocFetchEpic)