React Query spawns duplicate request with somehow old response from server - reactjs

Here is my setup:
const ParentComponent = ({ post_id }) => {
const { data: post } = useGetPost(post_id); // custom hook with useQuery under the hood
return (
<div>
{post.status === 'published' && (
<div className="margin-bottom">
<ChildComponent post_id={post_id} />
</div>
)}
</div>
);
};
const ChildComponent = ({ post_id }) => {
const { data: post } = useGetPost(post_id);
return <div>{post.title}</div>;
};
Whenever i change post status to 'published' – React Query spawns two getPost requests, first of which, for some reason, returns previous post status and second returns the right one. Problem is gone if i move post.status === 'published' check to ChildComponent, but it's not an option because of ChildComponent wrapper element ("margin-bottom") inside ParentComponent. Is there something i don't get?
UPDATE:
useGetPost hook:
export const useGetPost = (post_id) => {
return useQuery<IDiscussion, Error>(
['posts', post_id],
async () =>
await axios
.get(`/api/posts/${post_id}`)
.then((response) => response.data),
);
};
And my mutation function:
export const useUpdatePost = (post_id) => {
const queryClient = useQueryClient();
return useMutation(
async (data) => await axios.put(`/api/posts/${post_id}`, data),
{
onMutate: (data) => {
// updating react query cache before actual save
queryClient.setQueryData(['posts', post_id], (oldData) => ({
...oldData,
data,
}));
},
onSuccess: () => {
queryClient.refetchQueries(['posts', post_id]);
},
},
);
};

ChildComponent mounts after you manually set new query data in "onMutate". The query refetches on mount (default behavior), but mutation is not yet finished, so you see the old status. Then, on mutation success query refetches again and you see updated status.
That is why if your ChildComponent gets mounted unconditionally, you see only one request. You can just get rid of onMutate function and refetch the query only once, after mutation success.

Related

How to run useQueries only after clicking a button in react-query?

export const useDeleteArticles = ({ ids, onSuccess }) => {
const queryResult = useQueries(
ids.map(id => ({
queryKey: ["article-delete", id],
queryFn: () => articlesApi.destroy(id),
}))
);
const isLoading = queryResult.some(result => result.isLoading);
if (!isLoading) {
onSuccess();
}
return { isLoading, queryResult };
};
This customHook will simply delete some articles.
I tried to use enabled with a state as following.
export const useDeleteArticles = ({ ids, onSuccess, enabled }) => {
const queryResult = useQueries(
ids.map(id => ({
queryKey: ["article-delete", id],
queryFn: () => articlesApi.destroy(id),
enabled,
}))
);
const isLoading = queryResult.some(result => result.isLoading);
if (!isLoading) {
onSuccess();
}
return { isLoading, queryResult };
};
const [enabled, setEnabled] = useState(false);
useDeleteArticles({ ids, onSuccess: refetch, enabled });
enabled && setEnabled(false); //to avoid api call after deleting the articles
const handleArticleDelete = () => { //this function will invoke onClick button
setEnabled(true);
};
But this not making the api call.
could anyone help me to implement this in correct way.
Thank you.
This customHook will simply delete some articles.
a delete operation is almost never a query, but a mutation. Mutations can be fired imperatively by calling the mutate function returned from useMutation. A query is the wrong thing to use for this.

React using setState within useEffect() problem [duplicate]

This question already has answers here:
The useState set method is not reflecting a change immediately
(15 answers)
Closed 5 months ago.
N.B. I got my answer here but it is not the duplicate question of this thread
I am trying to fetch data from a reusable function that has an API. Here is my code
usaData.js in another page
const useData = () => {
const [data, setData] = useState([]);
const fetchData = async (url, query, variable) => {
const queryResult = await axios.post(url, {
query: query,
variables: variable,
});
setData(queryResult.data.data);
};
return {data, fetchData}
};
I am retrieving data from this MainPage.js file
export const MainPage = props => {
const [state, setState] = useState({
pharam: 'Yes',
value: '',
});
const [field, setField] = useState([])
const {data, fetchData} = useData()
const onClick = (event) => {
setState({ ...state, pharam: '', value: event });
fetchData(url, query, event)
setField(data)
}
return (
<div>
...
<Select
placeholder='select'
>
{field.map(item => (
<Select.Option key={item.name}>
{item.name}
</Select.Option>
))}
</Select>
<Button onClick={onClick}>Change Select</Button>
...
</div>
)
}
The problem is setField(data) within onClick function is not updating immediately as it is a async call. Hence I tried to use a function as a second argument
...
setField(data, () => {
console.log(data)
})
...
It is returning the following warning in red color but the behavior is similar to earlier, not updating data immediately.
Warning: State updates from the useState() and useReducer() Hooks don't support the second callback argument. To execute a side effect after rendering, declare it in the component body with useEffect().
As per the warning then I tried to use useEffect() within the onClick function
...
const onClick = (event) => {
setState({ ...state, pharam: '', value: event });
useEffect(() => {
fetchData(url, query, event)
setField(data)
}, [data])
}
...
which is returning an error
React Hook "useEffect" is called in function "onClick" that is neither a React function component nor a custom React Hook function. React component names must start with an uppercase letter. React Hook names must start with the word "use"
Where do I have to make changes? How can I get expected behavior as the setField will update the field immediately?
My suggestion would be to not setState in your custom hook rather than return promise.
usaData.js
const useData = () => {
const fetchData = async (url, query, variable) => {
return await axios.post(url, {
query: query,
variables: variable,
});
};
return { fetchData };
};
In MainPage.js
Now when you trigger your onClick function just call your fetchData function with await or then syntax and after successfully api call you'll get back the result in the newData variable which you can use it to update your state.
Note: this will save you an extra useEffect.
export const MainPage = (props) => {
const [state, setState] = useState({
pharam: "Yes",
value: "",
});
const [field, setField] = useState([]);
const { fetchData } = useData();
const onClick = async (event) => {
setState({ ...state, pharam: "", value: event });
let newData = await fetchData(url, query, event);
console.log("===>", newData.data.data);
setField(newData.data.data);
};
return (
<div>
...
<Select placeholder="select">
{field.map((item) => (
<Select.Option key={item.name}>{item.name}</Select.Option>
))}
</Select>
<Button onClick={onClick}>Change Select</Button>
...
</div>
);
};
The problem in your case is that setField gets calls before your data is fetched.
So, you can have a useEffect which gets executed every time the data gets changed.
useEffect(() => {
if(data.length > 0) {
setField(data);
}
}, [data])
const onClick = (event) => {
setState(prev => ({ ...prev, pharam: '', value: event }));
fetchData(url, query, event);
}
As far I know, React sets its state asynchronously. So, in order to update the state Field, you need an useEffect hook. Your approch with useEffect is correct, except it neeed to be placed outside onClick (directly in the component function).
export const MainPage = () => {
...
useEffect(() => {
setField(data)
},[data])
...
}

useSWR to get get data of specific ID

I am using NextJS with SWR, and I want to fetch data with SWR for one specific ID.
When user click on Row of table, it will pull data with that ID from server.
Below is SWR code
export const apiExpenses = () => {
const { data: showExpense, error, mutate } = useSWR('/api/expenses/show', () =>
axios
.get('/api/expenses/show/' + expenseId)
.then(res => res.data)
)
return {
showExpense,
}
};
Below code is for another component where showExpense is called,
const { showExpense } = apiExpenses();
const clickTo = async (expenseId) => {
//How can i fetch data with specific ID in SWR?
const expenseData = //Code?
};
//In JSX
<IconButton onClick={() => clickTo(params.id)} color="primary">
I am not sure how can I pass expenseId to SWR API.
Thank you,
First you need to pass expenseId to the hook:
export const apiExpenses = (expenseId) => {
// You need to pass id to the swr hook, not to axios, otherwise it wont refetch data for another id
const { data: showExpense, error, mutate } = useSWR(
// Pass null when there is no id
expenseId ? ('/api/expenses/show/' + expenseId) : null,
(key) => axios.get(key).then(res => res.data)
)
return {
showExpense,
}
};
And on the other side you can just store current id in the state:
const [currentId, setCurrentId] = useState()
const { showExpense } = apiExpenses(currentId);
const clickTo = async (expenseId) => {
setCurrentId(expenseId)
};
//In JSX
<IconButton onClick={() => clickTo(params.id)} color="primary">

Unnecessary refetch is triggered after mutation React Apollo

I'm building a small ToDo list app with React Apollo and GraphQL. In order to add a new ToDo item I click "Add" button that redirects me to a different URL that has a form. On form submit I perform a mutation and update the cache using update function. The cache gets updated successfully but as soon as I return to the main page with ToDo list, the component triggers an http request to get the ToDo list from the server. How do I avoid that additional request and make ToDoList component pull data from the cache ?
My AddToDo component:
const AddToDo = () => {
const { inputValue, handleInputChange } = useFormInput();
const history = useHistory();
const [addToDo] = useMutation(ADD_TODO);
const onFormSubmit = (e) => {
e.preventDefault();
addToDo({
variables: { title: inputValue },
update: (cache, { data: { addToDo } }) => {
const data = cache.readQuery({ query: GET_TODO_LIST });
cache.writeQuery({
query: GET_TODO_LIST,
data: {
todos: [...data.todos, addTodo],
},
});
history.push("/");
},
});
};
return (
...
);
};
And ToDoList component
const ToDoList = () => {
const { data, loading, error } = useQuery(GET_TODO_LIST);
if (loading) return <div>Loading...</div>;
if (error || !loading) return <p>ERROR</p>;
return (
...
);
};
Works as expected.
Why unecessary? Another page, new component, fresh useQuery hook ... default(?) fetchPolicy "cache-and-network" will use cached data (if exists) to render (quickly, at once) but also will make request to be sure current data used.
You can force "cache-only" but it can fail if no data in cache, it won't make a request.

How to send request on click React Hooks way?

How to send http request on button click with react hooks? Or, for that matter, how to do any side effect on button click?
What i see so far is to have something "indirect" like:
export default = () => {
const [sendRequest, setSendRequest] = useState(false);
useEffect(() => {
if(sendRequest){
//send the request
setSendRequest(false);
}
},
[sendRequest]);
return (
<input type="button" disabled={sendRequest} onClick={() => setSendRequest(true)}
);
}
Is that the proper way or is there some other pattern?
export default () => {
const [isSending, setIsSending] = useState(false)
const sendRequest = useCallback(async () => {
// don't send again while we are sending
if (isSending) return
// update state
setIsSending(true)
// send the actual request
await API.sendRequest()
// once the request is sent, update state again
setIsSending(false)
}, [isSending]) // update the callback if the state changes
return (
<input type="button" disabled={isSending} onClick={sendRequest} />
)
}
this is what it would boil down to when you want to send a request on click and disabling the button while it is sending
update:
#tkd_aj pointed out that this might give a warning: "Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function."
Effectively, what happens is that the request is still processing, while in the meantime your component unmounts. It then tries to setIsSending (a setState) on an unmounted component.
export default () => {
const [isSending, setIsSending] = useState(false)
const isMounted = useRef(true)
// set isMounted to false when we unmount the component
useEffect(() => {
return () => {
isMounted.current = false
}
}, [])
const sendRequest = useCallback(async () => {
// don't send again while we are sending
if (isSending) return
// update state
setIsSending(true)
// send the actual request
await API.sendRequest()
// once the request is sent, update state again
if (isMounted.current) // only update if we are still mounted
setIsSending(false)
}, [isSending]) // update the callback if the state changes
return (
<input type="button" disabled={isSending} onClick={sendRequest} />
)
}
You don't need an effect to send a request on button click, instead what you need is just a handler method which you can optimise using useCallback method
const App = (props) => {
//define you app state here
const fetchRequest = useCallback(() => {
// Api request here
}, [add dependent variables here]);
return (
<input type="button" disabled={sendRequest} onClick={fetchRequest}
);
}
Tracking request using variable with useEffect is not a correct pattern because you may set state to call api using useEffect, but an additional render due to some other change will cause the request to go in a loop
In functional programming, any async function should be considered as a side effect.
When dealing with side effects you need to separate the logic of starting the side effect and the logic of the result of that side effect (similar to redux saga).
Basically, the button responsibility is only triggering the side effect, and the side effect responsibility is to update the dom.
Also since react is dealing with components you need to make sure your component still mounted before any setState or after every await this depends on your own preferences.
to solve this issue we can create a custom hook useIsMounted this hook will make it easy for us to check if the component is still mounted
/**
* check if the component still mounted
*/
export const useIsMounted = () => {
const mountedRef = useRef(false);
const isMounted = useCallback(() => mountedRef.current, []);
useEffect(() => {
mountedRef.current = true;
return () => {
mountedRef.current = false;
};
});
return isMounted;
};
Then your code should look like this
export const MyComponent = ()=> {
const isMounted = useIsMounted();
const [isDoMyAsyncThing, setIsDoMyAsyncThing] = useState(false);
// do my async thing
const doMyAsyncThing = useCallback(async () => {
// do my stuff
},[])
/**
* do my async thing effect
*/
useEffect(() => {
if (isDoMyAsyncThing) {
const effect = async () => {
await doMyAsyncThing();
if (!isMounted()) return;
setIsDoMyAsyncThing(false);
};
effect();
}
}, [isDoMyAsyncThing, isMounted, doMyAsyncThing]);
return (
<div>
<button disabled={isDoMyAsyncThing} onClick={()=> setIsDoMyAsyncThing(true)}>
Do My Thing {isDoMyAsyncThing && "Loading..."}
</button>;
</div>
)
}
Note: It's always better to separate the logic of your side effect from the logic that triggers the effect (the useEffect)
UPDATE:
Instead of all the above complexity just use useAsync and useAsyncFn from the react-use library, It's much cleaner and straightforward.
Example:
import {useAsyncFn} from 'react-use';
const Demo = ({url}) => {
const [state, doFetch] = useAsyncFn(async () => {
const response = await fetch(url);
const result = await response.text();
return result
}, [url]);
return (
<div>
{state.loading
? <div>Loading...</div>
: state.error
? <div>Error: {state.error.message}</div>
: <div>Value: {state.value}</div>
}
<button onClick={() => doFetch()}>Start loading</button>
</div>
);
};
You can fetch data as an effect of some state changing like you have done in your question, but you can also get the data directly in the click handler like you are used to in a class component.
Example
const { useState } = React;
function getData() {
return new Promise(resolve => setTimeout(() => resolve(Math.random()), 1000))
}
function App() {
const [data, setData] = useState(0)
function onClick() {
getData().then(setData)
}
return (
<div>
<button onClick={onClick}>Get data</button>
<div>{data}</div>
</div>
);
}
ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://unpkg.com/react#16/umd/react.development.js" crossorigin></script>
<script src="https://unpkg.com/react-dom#16/umd/react-dom.development.js" crossorigin></script>
<div id="root"></div>
You can define the boolean in the state as you did and once you trigger the request set it to true and when you receive the response set it back to false:
const [requestSent, setRequestSent] = useState(false);
const sendRequest = () => {
setRequestSent(true);
fetch().then(() => setRequestSent(false));
};
Working example
You can create a custom hook useApi and return a function execute which when called will invoke the api (typically through some onClick).
useApi hook:
export type ApiMethod = "GET" | "POST";
export type ApiState = "idle" | "loading" | "done";
const fetcher = async (
url: string,
method: ApiMethod,
payload?: string
): Promise<any> => {
const requestHeaders = new Headers();
requestHeaders.set("Content-Type", "application/json");
console.log("fetching data...");
const res = await fetch(url, {
body: payload ? JSON.stringify(payload) : undefined,
headers: requestHeaders,
method,
});
const resobj = await res.json();
return resobj;
};
export function useApi(
url: string,
method: ApiMethod,
payload?: any
): {
apiState: ApiState;
data: unknown;
execute: () => void;
} {
const [apiState, setApiState] = useState<ApiState>("idle");
const [data, setData] = useState<unknown>(null);
const [toCallApi, setApiExecution] = useState(false);
const execute = () => {
console.log("executing now");
setApiExecution(true);
};
const fetchApi = useCallback(() => {
console.log("fetchApi called");
fetcher(url, method, payload)
.then((res) => {
const data = res.data;
setData({ ...data });
return;
})
.catch((e: Error) => {
setData(null);
console.log(e.message);
})
.finally(() => {
setApiState("done");
});
}, [method, payload, url]);
// call api
useEffect(() => {
if (toCallApi && apiState === "idle") {
console.log("calling api");
setApiState("loading");
fetchApi();
}
}, [apiState, fetchApi, toCallApi]);
return {
apiState,
data,
execute,
};
}
using useApi in some component:
const SomeComponent = () =>{
const { apiState, data, execute } = useApi(
"api/url",
"POST",
{
foo: "bar",
}
);
}
if (apiState == "done") {
console.log("execution complete",data);
}
return (
<button
onClick={() => {
execute();
}}>
Click me
</button>
);
For this you can use callback hook in ReactJS and it is the best option for this purpose as useEffect is not a correct pattern because may be you set state to make an api call using useEffect, but an additional render due to some other change will cause the request to go in a loop.
<const Component= (props) => {
//define you app state here
const getRequest = useCallback(() => {
// Api request here
}, [dependency]);
return (
<input type="button" disabled={sendRequest} onClick={getRequest}
);
}
My answer is simple, while using the useState hook the javascript doesn't enable you to pass the value if you set the state as false. It accepts the value when it is set to true. So you have to define a function with if condition if you use false in the usestate

Resources