Using conditions in custom react hook which uses useQuery - reactjs

I have a custom react hook which is used to get a user's data by sending a POST request. I would like my custom react hook to check if the argument passed into the hook meets certain conditions, however I'm aware that I cant use conditions with hooks. Does any one have any idea how to improve my code so that I can check if an argument is valid?
...
import { useQuery } from "#tanstack/react-query";
export const useLiquidityPositions = (userAddress: string): GraphQLResponse<LiquidityPositions> => {
const {
status: status,
isLoading: loading,
error: error,
data: response,
} = useQuery({
queryKey: ["USER_POSITIONS", userAddress],
queryFn: async () => {
if (!isAddress(userAddress)) {
return undefined
}
const res = await getLiquidityPositionsData(userAddress);
return res;
}});
const payload = response?.data;
return { status, loading, error, response, payload }
}

Two ways to validate query using react-query.
1. only validate a single request. https://react-query-v3.tanstack.com/guides/dependent-queries
import { useQuery } from "#tanstack/react-query";
export const useLiquidityPositions = (userAddress: string): GraphQLResponse<LiquidityPositions> => {
const {
status: status,
isLoading: loading,
error: error,
data: response,
} = useQuery({
queryKey: ["USER_POSITIONS", userAddress],
queryFn: ()=>getLiquidityPositionsData(userAddress),
// The query will not execute until isAddress(userAddress) is true.
enabled: isAddress(userAddress)
});
const payload = response?.data;
return { status, loading, error, response, payload }
}
2. validate all request.
https://react-query-v3.tanstack.com/guides/query-invalidation
Add validate functions to queryClient
const queryClient = new QueryClient()
queryClient.invalidateQueries({
predicate: query =>
query.queryKey[0] === 'USER_POSITIONS' && isAddress(query.queryKey[1]) ,
})
<QueryClientProvider client={queryClient}>
</QueryClientProvider>

You can enable or disable useQuery by your condition
like this :
export const useLiquidityPositions = (
userAddress: string,
enabled: boolean
): GraphQLResponse<LiquidityPositions> => {
const {
status: status,
isLoading: loading,
error: error,
data: response,
} = useQuery({
queryKey: ['USER_POSITIONS', userAddress],
queryFn: async () => {
if (!isAddress(userAddress)) {
return undefined;
}
const res = await getLiquidityPositionsData(userAddress);
return res;
},
enabled,
});
const payload = response?.data;
return { status, loading, error, response, payload };
};
and in usage:
const result = useLiquidityPositions('...',true //your condition )

Related

How to use a thunk dispatch to create a standard promise that returns `success` or `error`

I have a toast package that receives a standard promise as an argument and does something upon success or error:
toast.promise(
updateNotePromise,
{
loading: 'Saving...',
success: (data: any) => 'Note saved!',
error: (err) => err.toString()
}
);
This is the promise I pass to the toast, but it returns a <PayloadAction> because it calls a thunk:
const updateNotePromise = await dispatch(
updateNoteInFirestore({ note: noteInput, noteDocId: noteProp.docId })
);
How can I return success or error from this dispatch thunk operation?
I thought of processing the returned <PayloadAction> by wrapping the thunk. This would be my naive approach:
const updateNotePromise = async(): Promise<{success: boolean | error: any}> => {
try {
await dispatch(updateNoteInFirestore({ note: noteInput, noteDocId: noteProp.docId
return success }))
}
catch {
(error)=> return error}
Am I on the right track?
Edit: here's the thunk code:
export const updateNoteInFirestore = createAsyncThunk(
'updateNoteInFirestore',
async (
{ note, noteDocId }: { note: string; noteDocId?: string },
{ getState, dispatch }
) => {
const poolState = (getState() as RootState).customerPool.pool;
const userState = (getState() as RootState).user;
const time = Timestamp.now();
const path = noteDocId ? noteDocId : undefined;
const message = note;
if (poolState?.docID) {
await notesService.updateNote(
{
pool: poolState.docID,
customer: userState?.user?.uid ?? 'Undefined Customer',
//we do not update dateFirstCreated
...(path ? { dateLastUpdated: time } : { dateFirstCreated: time }),
dateLastUpdated: time,
message: message,
editHistory: [],
seenByAdmin: false
},
path
);
dispatch(fetchNotesByCustomerId(userState?.user?.uid));
return { error: false };
}
return { error: true };
}
);
If you want to return an error with createAsyncThunk you can use rejectWithValue
const fetchUserById = createAsyncThunk(
'users/fetchById',
async (userId, { rejectWithValue }) => {
const response = await fetch(`https://example.com/api/stuff`)
if (response.status === 404)
return rejectWithValue(new Error("Impossible to do stuff"));
return response.json()
}
)
I think for your use case, it's better to use a promise-based function followed by a dispatch reducer action rather than an asyncThunk.
asyncThunks return value can only be consumed by builders that are defined within slice as far as I know.
You need to break your problem into three steps:
Creating a wrapper promiseFunction as needed by your toast.
Creating a promise helper function where you must be able to supply the variables poolState and userState as these variables were accessed through getState() in your async thunk but that isn't possible in your promiseHelperFunction If you define promiseHelperFunction within your functional component you could use useAppSelector to access those states. I have added the comment for the same in the promiseHelperFunction.
Now once you're done with this you can now consume promiseFunction in your toast.
You might need to import fetchNotesByCustomerId that you're using in your asyncThunk as it may not be accessible to the component where you're writing the toast implementation.
Here's the code for same:
const updateNotePromise = async () => {
return updatePromiseHelperFunction({
note: noteInput,
noteDocId: noteProp.docId,
});
};
const updatePromiseHelperFunction = async ({
note,
noteDocId,
}: {
note: string;
noteDocId?: string;
}) => {
/*
// Before the Promise you must ensure you're able to access these variables:
const poolState = useAppSelector(state => state.customerPool.pool);
const userState = useAppSelector(state => state.user);
*/
const time = Timestamp.now();
const path = noteDocId ? noteDocId : undefined;
if (poolState?.docID) {
await notesService.updateNote(
{
pool: poolState.docID,
customer: userState?.user?.uid ?? 'Undefined Customer',
//we do not update dateFirstCreated
...(path ? { dateLastUpdated: time } : { dateFirstCreated: time }),
dateLastUpdated: time,
message: note,
editHistory: [],
seenByAdmin: false,
},
path
);
dispatch(fetchNotesByCustomerId(userState?.user?.uid));
return { error: false };
}
return { error: true };
};

How can I use SWR hook for all API methods? (CRUD)

I am switching certain CRUD functionality that I used to provide with a token, but now I am using SWR and I don't know how to convert it.
I used this hook for GET methods but for others, I don't know what to do!
export default function useGetData(apiKey) {
const fetcher = async (...args) => await fetch(...args).then(res => res.json());
const { data, mutate, error } = useSWR(apiKey, fetcher);
const loading = !data && !error;
return {
loading,
user: data,
mutate
}
}
OK, I found the answer :
import useSWR from 'swr';
import { getTokenFromLocalStorage } from '../../services/storage';
export default function useGetData({ url, payload, options = {}}) {
const mainUrl = process.env.NEXT_PUBLIC_BASE_URL + URL;
const method = payload ? 'POST' : 'GET';
const fetcher = async () => {
const headers = {
Authorization: `Bearer ${getTokenFromLocalStorage()}`
};
const options = {
method,
headers,
...(payload && { body: payload }),
};
return await fetch(mainUrl, options).then((res) => res.json());
};
const defaultOptions = {
revalidateIfStale: false,
revalidateOnFocus: false,
revalidateOnReconnect: false,
};
const { data, mutate, error, isValidating } = useSWR(url + method, fetcher, {
...defaultOptions,
...options,
});
const loading = !data && !error;
return { data, loading, error, mutate, isValidating };
}

useMutation status is always 'idle' and 'isLoading' always 'false' - react-query

So I use react-query to handle API requests. Current problem is that when i try to submit form data, post request, mutation state is always idle, and loading is always false. I also use zustand for state management.
This is useSubmitFormData hook. Post request executes as expected, just mutation status and isLoading does not get changed.
export const useSubmitFormData = () => {
const { postDataPlaceholder } = usePlaceholderApi();
// data which is submiting is getting from the store - reducer
const { data, filesToUpload } = useFormDataStore();
const { mutate, status, isLoading } = useMutation(() => postDataPlaceholder({ data }), {
onMutate: () => console.log('onMutate', status),
onSuccess: (res) => console.log(res),
onError: (err) => console.log('err', err),
});
return {
submitForm: mutate,
isLoading,
};
};
Now on FormPage.jsx it is triggered like this:
const { submitForm, isLoading } = useSubmitFormData();
const onSubmit = () => submitForm();
And this is how usePlaceholderApi looks like. It is kind of custom hook in purpose to use axios in combination with interceptors to handle authorization token.
const usePlaceholderApi = () => {
const { post } = usePlaceholderAxios();
return {
postDataPlaceholder: async (data) => post('/posts', { data }),
};
};
export default usePlaceholderApi;
And this is usePlaceholderAxios.
import axios from 'axios';
const usePlaceholderAxios = () => {
axios.interceptors.request.use(async (config) => {
const api = 'https://jsonplaceholder.typicode.com';
if (config.url.indexOf('http') === -1) {
// eslint-disable-next-line no-param-reassign
config.url = `${api}${config.url}`;
}
return config;
});
return {
get: (url, config) => axios.get(url, config),
post: (url, data, config) => axios.post(url, data, config),
};
};
export default usePlaceholderAxios;
Any ideas what could go wrong here ? Am I missing something ? Also tried to call axios directly in mutation, without usePlaceholderApi hook in between, but same outcome.

When a post call doesn't return any response, do I need a reducer?

I'm wondering since my POST call doesn't give any data in response, do I need to have action types and reducer to maintain State?
This is my actioncreator. What will actionType and reducer contain?
export const postData = (...args) => async dispatch => {
const [ url, body ] = args;
const params = [ url, body, undefined, false, undefined , 'application/json'];
try {
const data = configs.ENV.DEVELOPMENT ? await API.getData(url, dispatch, actions, false) : await API.postData(params, dispatch, actions);
if (!data) {
window.location = configs.endpoints.RESULTS_PAGE;
}
}
catch(err) {
console.log('Please try again')
} };

useEffect infinite loop occurs only while testing, not otherwise - despite using useReducer

I'm trying to test a useFetch custom hook. This is the hook:
import React from 'react';
function fetchReducer(state, action) {
if (action.type === `fetch`) {
return {
...state,
loading: true,
};
} else if (action.type === `success`) {
return {
data: action.data,
error: null,
loading: false,
};
} else if (action.type === `error`) {
return {
...state,
error: action.error,
loading: false,
};
} else {
throw new Error(
`Hello! This function doesn't support the action you're trying to do.`
);
}
}
export default function useFetch(url, options) {
const [state, dispatch] = React.useReducer(fetchReducer, {
data: null,
error: null,
loading: true,
});
React.useEffect(() => {
dispatch({ type: 'fetch' });
fetch(url, options)
.then((response) => response.json())
.then((data) => dispatch({ type: 'success', data }))
.catch((error) => {
dispatch({ type: 'error', error });
});
}, [url, options]);
return {
loading: state.loading,
data: state.data,
error: state.error,
};
}
This is the test
import useFetch from "./useFetch";
import { renderHook } from "#testing-library/react-hooks";
import { server, rest } from "../mocks/server";
function getAPIbegin() {
return renderHook(() =>
useFetch(
"http://fe-interview-api-dev.ap-southeast-2.elasticbeanstalk.com/api/begin",
{ method: "GET" },
1
)
);
}
test("fetch should return the right data", async () => {
const { result, waitForNextUpdate } = getAPIbegin();
expect(result.current.loading).toBe(true);
await waitForNextUpdate();
expect(result.current.loading).toBe(false);
const response = result.current.data.question;
expect(response.answers[2]).toBe("i think so");
});
// Overwrite mock with failure case
test("shows server error if the request fails", async () => {
server.use(
rest.get(
"http://fe-interview-api-dev.ap-southeast-2.elasticbeanstalk.com/api/begin",
async (req, res, ctx) => {
return res(ctx.status(500));
}
)
);
const { result, waitForNextUpdate } = getAPIbegin();
expect(result.current.loading).toBe(true);
expect(result.current.error).toBe(null);
expect(result.current.data).toBe(null);
await waitForNextUpdate();
console.log(result.current);
expect(result.current.loading).toBe(false);
expect(result.current.error).not.toBe(null);
expect(result.current.data).toBe(null);
});
I keep getting an error only when running the test:
"Warning: Maximum update depth exceeded. This can happen when a component calls setState inside useEffect, but useEffect either doesn't have a dependency array, or one of the dependencies changes on every render."
The error is coming from TestHook: node_modules/#testing-library/react-hooks/lib/index.js:21:23)
at Suspense
I can't figure out how to fix this. URL and options have to be in the dependency array, and running the useEffect doesn't change them, so I don't get why it's causing this loop. When I took them out of the array, the test worked, but I need the effect to run again when those things change.
Any ideas?
Try this.
function getAPIbegin(url, options) {
return renderHook(() =>
useFetch(url, options)
);
}
test("fetch should return the right data", async () => {
const url = "http://fe-interview-api-dev.ap-southeast-2.elasticbeanstalk.com/api/begin";
const options = { method: "GET" };
const { result, waitForNextUpdate } = getAPIbegin(url, options);
expect(result.current.loading).toBe(true);
await waitForNextUpdate();
expect(result.current.loading).toBe(false);
const response = result.current.data.question;
expect(response.answers[2]).toBe("i think so");
});
I haven't used react-hooks-testing-library, but my guess is that whenever React is rendered, the callback send to RenderHook will be called repeatedly, causing different options to be passed in each time.

Resources