Im mostly using SWR to get data, however I have a situation that I need to update data. The problem is, I need an indicator that this request is ongoing, something like isLoading flag. In the docs there's a suggestion to use
const isLoading = !data && !error;
But of course when updating (mutating) the data still exists so this flag is always false. The same with isValidating flag:
const { isValidating } = useSWR(...);
This flag does NOT change when mutation is ongoing but only when its done and GET request has started.
Question
Is there a way to know if my PUT is loading? Note: I dont want to use any fields in state because it won't be shared just like SWR data is. Maybe Im doing something wrong with my SWR code?
const fetcher = (url, payload) => axios.post(url, payload).then((res) => res);
// ^^^^^ its POST but it only fetches data
const updater = (url, payload) => axios.put(url, payload).then((res) => res);
// ^^^^^ this one UPDATES the data
const useHook = () => {
const { data, error, mutate, isValidating } = useSWR([getURL, payload], fetcher);
const { mutate: update } = useSWRConfig();
const updateData = () => {
update(getURL, updater(putURL, payload)); // update data
mutate(); // refetch data after update
};
return {
data,
updateData,
isValidating, // true only when fetching data
isLoading: !data && !error, // true only when fetching data
}
Edit: for any other who reading this and facing the same issue... didnt find any solution for it so switched to react-query. Bye SWR
const { mutate: update } = useSWRConfig();
const updateData = () => {
// this will return promise
update(getURL, updater(putURL, payload)); // update data
mutate(); // refetch data after update
};
By using react-toastify npm module to show the user status.
// first wrap your app with: import { ToastContainer } from "react-toastify";
import { toast } from "react-toastify";
const promise=update(getURL, updater(putURL, payload))
await toast.promise(promise, {
pending: "Mutating data",
success: "muttation is successfull",
error: "Mutation failed",
});
const markSourceMiddleware = (useSWRNext) => (key, fetcher, config) => {
const nextFetcher = (...params) =>
fetcher(...params).then((response) => ({
source: "query",
response,
}));
const swr = useSWRNext(key, nextFetcher, config);
return swr;
};
const useHook = () => {
const {
data: { source, response },
mutate,
} = useSWR(key, fetcher, { use: [markSourceMiddleware] });
const update = mutate(
updateRequest().then((res) => ({
source: "update",
response,
})),
{
optimisticData: {
source: "update",
response,
},
}
);
return {
update,
updating: source === "update",
};
};
Hmm based on that:
https://swr.vercel.app/docs/conditional-fetching
It should work that the "is loading" state is when your updater is evaluates to "falsy" value.
REMAINDER! I don't know react swr just looked into docs - to much time at the end of the weekend :D
At least I hope I'll start discussion :D
Related
I found something about this bug I explained at end;
Component codes
async fetch(){ await this.$store.dispatch('bots/getBots') },
computed: { ...mapState('bots', ['bots']) },
Store codes
export const state = () => {
return {
bots: []
}
}
export const mutations = {
UPDATE_BOTS(state, bots) {
state.bots = bots
}
}
export const actions = {
getBots({commit}) {
this.$axios.$get('url', {headers: {uid: '12345'}})
.then(res => {
commit('UPDATE_BOTS',res.robots)
})
.catch(e => {
console.log(e)
})
}
}
Issue: When moving between pages via nuxt-link data loads perfectly but when I reload the page bots state is empty...
Found Issue:
I use nuxt-auth and I had one plugin for checking status of axios request that if it was 401 unauthorized I logout user if he was loggedIn, So status undefined error was from here but I commented the plugin codes and I got other error from nuxt-auth that causes that problem I had So I related that issue in other question u can see it here:
Nuxt-Auth Bug: Looks for autherization in any get request that has headers config
It is the expected behavior. Vuex state is kept in memory and when you reload the page it gets purged.
Instead of this state
export const state = () => {
return {
bots: []
}
}
try this
export const state = () => ({
bots: []
})
I get todo list with useQuery.
const { data, refetch } = useQuery(GET_TODOS);
After creating a todo, I get todo list with refetch like below.
const [ addTodo ] = useMutation(ADD_TODO, {
onComplete: () => refetch()
});
const handleAddTodo = useCallback((todoArgs) => {
addTodo({ variables: todoArgs });
}, []);
But It is obviously wasted time.
I tried to update only in an updated part. for that, I saved todos into a state and I changed this.
const [todos, setTodos] = useState([]);
...
const [ addTodo ] = useMutation(ADD_TODO, {
onComplete: (updatedData) => {
setTodos((prevTodos) => {
const newTodos = prevTodos.map((todo) => todo.id === updatedData.id ? updatedData : todo);
return newTodos;
});
}
}
...
useEffect(() => {
setTodos(data);
}, [data]);
...
But I'm not sure It is a right way. I think there may be an official way for updating a part of data.
What's the best way to fetch a partial data after Creating, Updating, Deleting?
I'm using 'no-cache' as a default option in the project.
Managing the query response in a new state seems a bit overkill to me.
In fact, Apollo GraphQL client automatically refetch the updated data, as long as you are returning the updated data id field in the mutation result.
For other cases, you may want to use a custom update function option.
You can read more about that here:
https://www.apollographql.com/blog/apollo-client/caching/when-to-use-refetch-queries/
I'm currently working on a search functionality in React Native using axios.
When implementing search functionality i'm using debounce from lodash to limit the amount of requests sent.
However, since request responses are not received in same order there is a possibility of displaying incorrect search results.
For example when the user input 'Home deco' in input field there will be two requests.
One request with 'Home' and next with 'Home deco' as search query text.
If request with 'Home' takes more time to return than second request we will end up displaying results for 'Home' query text not 'Home deco'
Both results should be displayed to the user sequentially, if responses are returned in order but if 'Home' request is returned after 'Home deco' request then 'Home' response should be ignored.
Following is a example code
function Search (){
const [results, setResults] = useState([]);
const [searchText, setSearchText] = useState('');
useEffect(() => {
getSearchResultsDebounce(searchText);
}, [searchText]);
const getSearchResultsDebounce = useCallback(
_.debounce(searchText => {
getSearchResults(searchText)
}, 1000),
[]
);
function getSearchResults(searchText) {
const urlWithParams = getUrlWithParams(url, searchText);
axios.get(urlWithParams, { headers: config.headers })
.then(response => {
if (response.status === 200 && response.data)
{
setResults(response.data);
} else{
//Handle error
}
})
.catch(error => {
//Handle error
});
}
return (
<View>
<SearchComponent onTextChange={setSearchText}/>
<SearchResults results={results}/>
</View>
)
}
What is the best approach to resolve above issue?
If you want to avoid using external libraries to reduce package size, like axios-hooks, I think you would be best off using the CancelToken feature included in axios.
Using the CancelToken feature properly will also prevent any warnings from react about failing to cancel async tasks.
Axios has an excellent page explaining how to use the CancelToken feature here. I would recommend reading if you would like a better understanding of how it works and why it is useful.
Here is how I would implement the CancelToken feature in the example you gave:
OP clarified in the replies that they do not want to implement a cancelation feature, in that case I would go with a timestamp system like the following:
function Search () {
//change results to be a object with 2 properties, timestamp and value, timestamp being the time the request was issued, and value the most recent results
const [results, setResults] = useState({
timeStamp: 0,
value: [],
});
const [searchText, setSearchText] = useState('');
//create a ref which will be used to store the cancel token
const cancelToken = useRef();
//create a setSearchTextDebounced callback to debounce the search query
const setSearchTextDebounced = useCallback(
_.debounce((text) => {
setSearchText(text)
), [setSearchText]
);
//put the request inside of a useEffect hook with searchText as a dep
useEffect(() => {
//generate a timestamp at the time the request will be made
const requestTimeStamp = new Date().valueOf();
//create a new cancel token for this request, and store it inside the cancelToken ref
cancelToken.current = CancelToken.source();
//make the request
const urlWithParams = getUrlWithParams(url, searchText);
axios.get(urlWithParams, {
headers: config.headers,
//provide the cancel token in the axios request config
cancelToken: source.token
}).then(response => {
if (response.status === 200 && response.data) {
//when updating the results compare time stamps to check if this request's data is too old
setResults(currentState => {
//check if the currentState's timeStamp is newer, if so then dont update the state
if (currentState.timeStamp > requestTimeStamp) return currentState;
//if it is older then update the state
return {
timeStamp: requestTimeStamp,
value: request.data,
};
});
} else{
//Handle error
}
}).catch(error => {
//Handle error
});
//add a cleanup function which will cancel requests when the component unmounts
return () => {
if (cancelToken.current) cancelToken.current.cancel("Component Unmounted!");
};
}, [searchText]);
return (
<View>
{/* Use the setSearchTextDebounced function here instead of setSearchText. */}
<SearchComponent onTextChange={setSearchTextDebounced}/>
<SearchResults results={results.value}/>
</View>
);
}
As you can see, I also changed how the search itself gets debounced. I changed it where the searchText value itself is debounced and a useEffect hook with the search request is run when the searchText value changes. This way we can cancel previous request, run the new request, and cleanup on unmount in the same hook.
I modified my response to hopefully achieve what OP would like to happen while also including proper response cancelation on component unmount.
We can do something like this to achieve latest api response.
function search() {
...
const [timeStamp, setTimeStamp] = "";
...
function getSearchResults(searchText) {
//local variable will always have the timestamp when it was called
const reqTimeStamp = new Date().getTime();
//timestamp will update everytime the new function call has been made for searching. so will always have latest timestampe of last api call
setTimeStamp(reqTimeStamp)
axios.get(...)
.then(response => {
// so will compare reqTimeStamp with timeStamp(which is of latest api call) if matched then we have got latest api call response
if(reqTimeStamp === timeStamp) {
return result; // or do whatever you want with data
} else {
// timestamp did not match
return ;
}
})
}
}
This is part of my code, what I want to do is this component at any time can receive a message on any of the conversations. Sending a message triggers a Socket event which triggers this code below, but I can't seem to get the "latest" conversations, as the useEffect only triggers when the component mounts (at that point my conversations array has zero length).
What I was thinking is that I should include "conversations" on the useEffect's dependency but that would create multiple websocket connection, one each time a Socket.io event is triggered because it does change the state. Is this the best solution? Thanks in advance!
const [conversations, setConversations] = useState<Array<Conversations>>([]);
useEffect(() => {
async function getConversations() {
try {
const { data } = await axios.get("/api/conversations/");
if (data.success) {
setConversations(data.details);
}
} catch (err) {}
}
getConversations();
socketInstance.on("connect", () => {
console.log("Connecting to Sockets...");
socketInstance.emit("authenticate", Cookies.get("token") || "");
});
socketInstance.on("ackAuth", ({ success }) => {
console.log(
success
? "Successfully connected to Sockets"
: "There has been an error connecting to Sockets"
);
});
socketInstance.on("newMessage", (data) => {
const modifiedConversation: Conversations = conversations.find(
(conv: Conversations) => {
return conv.conversationId === data.conversationId;
}
);
modifiedConversation.messages.push({
from: {
firstName: data.firstName,
lastName: data.lastName,
profilePhoto: data.profilePhoto,
userId: data.userId,
},
content: data.content,
timeStamp: data.timeStamp,
});
const updatedConversations = [
...conversations.filter(
(conv) => conv.conversationId !== data.conversationId
),
modifiedConversation,
];
setConversations(updatedConversations);
});
}, []);
While attaching and removing the socket listeners every time conversations changes is a possibility, a better option would be to use the callback form of the setters. The only time you reference the state, you proceed to update the state, luckily. You can change
socketInstance.on("newMessage", (data) => {
const modifiedConversation: Conversations = conversations.find(
// lots of code
setConversations(updatedConversations);
to
socketInstance.on("newMessage", (data) => {
setConversations(conversations => {
const modifiedConversation: Conversations = conversations.find(
// lots of code
setConversations(updatedConversations);
You should also not mutate the state, since this is React. Instead of
modifiedConversation.messages.push({
do
const modifiedConversationWithNewMessage = {
...modifiedConversation,
messages: [
...modifiedConversation.messages,
{
from: {
// rest of the object to add
I have data between sessions that is saved in window.localStorage. When a new session starts a plugin will grab the data and add it to the store.
// ./store/data.js
export const state = () => ({
data: []
})
export const mutations = {
addItemToData (state, item) {
state.data = state.data.push(item)
},
setData (state, data) {
state.data = data
},
}
// ./store/index.js
import localStorage from '../plugins/localStorage'
export const plugins = [localStorage]
// plugins/localStorage.js
const localStorage = store => {
store.subscribe((mutation, state) => {
if (mutation.type === 'data/addItemToData') {
console.log('saving added item to storage')
window.localStorage.setItem('data', JSON.stringify(state.data.data))
}
})
// called when the store is initialized
if (typeof window !== 'undefined') {
if (window.localStorage.data) {
store.commit('data/setData', JSON.parse(window.localStorage.getItem('data')))
}
}
}
export default localStorage
I've thrown all sorts of console statements in, and they all seem to output what they should. The data is in the localStorage, the mutations are firing, but after all that the data isn't in the store.
Any help would be great, thanks!
several things that do not make sense and that you can resolve
#1
addItemToData (state, item) {
state.data = state.data.push(item)
}
Array.push() does not return anything, so it should be written as
addItemToData (state, item) {
state.data.push(item)
}
#2
in your localStorage.js file, when you initialize the data storage, you are assuming that a data variable exists:
if (window.localStorage.data) { ...
but that will never exist from the code you show, as you are adding the data to another variable
window.localStorage.setItem('cart', ...
either change cart into data or data into cart
#3
If still does not work, I would suspect that your plugin is running before the store is actually initialized, for that, make sure you wait a moment before attempt to fill the store with the localStorage data
something like
...
if (window.localStorage.data) {
setTimeout(() => {
store.commit('data/setData', JSON.parse(window.localStorage.getItem('data')))
}, 500)
}
Working example is available on Netlify and code on GitHub
you can use vuex-persistedstate for saving in localstorage
Array.push Returns the length of the Array after insertion, In your case, it overrides the data Array to Number when you make a mutation. Do like the following..
export const state = () => ({
data: []
})
export const mutations = {
addItemToData (state, item) {
state.data.push(item)
// OR
state.data = [...state.data, item]
},
setData (state, data) {
state.data = data
},
}
And also make sure, you have data in localStorage.
try { // handles if any parsing errors
const data = JSON.parse(window.localStorage.getItem('data')
store.commit('data/setData', data)
} catch(err) {
console.log('JSON parsing error', err)
}