I am trying to pre-fetch data using react-query prefetchQuery. When I am inspecting browser DevTools network tab I can see that data that was requested for prefetchQuery is coming from the back-end but for some reason when I look into react-query DevTools it does generate the key in the cache but for some reason the Data is not there. Let me know what I am doing wrong.
import { useState, useEffect } from 'react';
import { useQuery, useQueryClient } from 'react-query';
import axios from 'axios';
const baseURL = process.env.api;
async function getSubCategoryListByCategoryId(id) {
// await new Promise((resolve) => setTimeout(resolve, 300));
console.log(`${baseURL}/category/subcategories/${id}`);
try {
const { data } = await axios.request({
baseURL,
url: `/category/subcategories/${id}`,
method: 'get',
});
console.log('data getSubCategoryListByCategoryId index: ', data);
return data;
} catch (error) {
console.log('getSubCategoryListByCategoryId error:', error);
}
}
// const initialState = {
// };
const ProductCreate = () => {
const [values, setValues] = useState(initialState);
const queryClient = useQueryClient();
const { data, isLoading, isError, error, isFetching } = useQuery(
'categoryList',
getPosts
);
const dataList = JSON.parse(data);
useEffect(() => {
setValues({ ...values, categories: dataList });
dataList.map((item) => {
console.log('useEffect values.categories item.id: ', item._id);
queryClient.prefetchQuery(
['subCategoryListByCategoryId', item._id],
getSubCategoryListByCategoryId(item._id)
);
});
}, []);
return <h1>Hello</h1>;
};
export default ProductCreate;
The second parameter to prefetchQuery expects a function that will fetch the data, similar to the queryFn passed to useQuery.
But here, you are invoking the function, thus passing the result of it into prefetchQuery:
getSubCategoryListByCategoryId(item._id)
if you want to do that, you can manually prime the query via queryClient.setQueryData, which accepts a key and the data for that key passed to it.
otherwise, the fix is probably just:
() => getSubCategoryListByCategoryId(item._id)
Related
I have the following code in my React App component, where I am trying to get some JSON and want to send that to my custom hook.
const fetchFn = useCommonFetch();
const [user1, status1] = fetchFn('myapi/urls', {}, 'GET');
useEffect(() => {
someCustomHookCall(user1); // Pass "user1" JSON info to someCustomHookCall
}, [user1])
Below is how the useCommonFetch looks;
import {useEffect, useState} from 'react';
export default function useCommonFetch() {
const fetchData = async (url, reqData, requestType) => {
try {
var statusObj = {
statMsg: null,
errMsg: null,
status: null,
};
var reqOptions = {
credentials: 'same-origin'
}
if (requestType === 'POST') {
reqOptions.headers = {};
reqOptions.headers['Accept'] = 'application/json';
reqOptions.method = 'POST';
reqOptions.body = JSON.stringify(reqData);
}
const response = await fetch(url, reqOptions);
if (!response.ok) {
throw response;
}
statusObj.status = "success";
const json = await response.json();
return [json, statusObj];
} catch (error) {
statusObj.status = "error";
if (error && error.status) {
switch (error.status) {
case 401:
statusObj.errMsg = "Unauthorized";
console.error("Unauthorized: " + error.status);
break;
default:
statusObj.errMsg = "Sys error";
}
}
return [null, statusObj];
}
}
return fetchData;
}
I am getting the following error while running
Uncaught TypeError: Invalid attempt to destructure non-iterable instance.
In order to be iterable, non-array objects must have a Symbol.iterator method.
Not sure what is wrong in the calling code.
The error occurs because you're trying to destructure a pending promise.
I have created a short codesandbox that suggests another way of achieving your expectation, but something causes the component to re-render nonstop.
fetchFn is an asynchronous function and it returns a promise(and not an array), I suggest you change the useCommonFetch as follows:
import {useEffect, useState} from 'react';
const fetchData = async (url, reqData, requestType) => {
...
}
export default function useCommonFetch(url, reqData, requestType) {
const [state, setState] = useState([null, null]);
useEffect(() => {
async function getData() {
const result = await fetchData(url, reqData, requestType);
setState(result);
}
getData();
}, [url, reqData, requestType])
return state;
}
You can use it like this:
const [user1, status1] = useCommonFetch('myapi/urls', null, 'GET');
useEffect(() => {
if(user1) {
someCustomHookCall(user1); // Pass "user1" JSON info to someCustomHookCall
}
}, [user1])
I want to use the useSelector state to send the new bugs state to a server inside an async function, but the value does not change. What am I doing wrong?
When submitting a form I want to update the bugs state and send it to the server
Inside an async function I do dispatch(updateBugs(newBug)) and the state is updated so this works
I get the state with const { bugs } = useSelector((state) => state.bugs); and the state seems to be updated
When I send the updated bugs list with await sendUpdatedBugsToServer(bugs); inside the async function, the bugs state seems to be the old one.
■bug-actions.js
export const sendUpdatedBugsToServer = (newBugsList) => {
const storeData = async (newBugsList) => {
const response = await fetch(`${databaseURL}/bugs.json`, {
method: 'PUT',
body: JSON.stringify(newBugsList),
headers: { 'Content-Type': 'application/json' },
});
if (!response.ok) {
throw new Error('cannot store new bug');
}
};
try {
storeData(newBugsList);
} catch (error) {
console.error(error.message);
}
};
■UpdateBugs.js
const EditBug = () => {
const dispatch = useDispatch();
const { bugs } = useSelector((state) => state.bugs); //new bug state reflected here
const submitUpdatedBugs = async () => {
const newBug = {
title: enteredTitle,
details: enteredDetails,
steps: enteredSteps,
version: enteredVersion,
priority: enteredPriority,
assigned: enteredAssigned,
creator: enteredCreator,
id: enteredId,
};
await dispatch(updateBugs(newBug)); //update bugs state
await sendUpdatedBugsToServer(bugs); //new bug state not reflected here
}
};
const submitUpdatedBugHandler = (e) => {
e.preventDefault();
submitUpdatedBugs();
};
I am currently making a Spotify clone which gives user a preview of the song. The problem occurs when I am making many different api requests. When there are more than one requests on the page, it throws a 429 error(making too many requests at once).
Please read through the whole question as I have mentioned the steps I have taken to fix this below.
Profile.js
const { api, refreshableCall } = useSpotify()
const [error, setError] = useState(null)
const [userName, setUserName] = useState("")
const [userFollowers, setUserFollowers] = useState("")
const [userImage, setUserImage] = useState([])
const [userLink, setUserLink] = useState("")
const [userId, setUserId] = useState("")
const [userFollowing, setUserFollowing] = useState("")
const [userTopArtists, setUserTopArtists] = useState([])
const [userTopSongs, setUserTopSongs] = useState([])
useEffect(() => {
let disposed = false
refreshableCall(() => api.getMyTopTracks({
limit: 10,
time_range: "long_term"
}))
.then((res) => {
if (disposed) return
setUserTopSongs(res.body.items)
setError(null)
})
.catch((err) => {
if (disposed) return
setUserTopSongs([])
setError(err)
});
return () => disposed = true
})
useEffect(() => {
let disposed = false
refreshableCall(() => api.getMe())
.then((res) => {
if (disposed) return
var data = res.body
setUserName(data.display_name)
setUserImage(data.images)
setUserFollowers(data.followers["total"])
setUserLink(data.external_urls.spotify)
setUserId(data.id)
setError(null)
})
.catch((err) => {
if (disposed) return
setUserName("")
setUserImage([])
setUserFollowers("")
setUserLink("")
setUserId("")
setError(err)
});
return () => disposed = true
})
useEffect(() => {
let disposed = false
refreshableCall(() => api.getFollowedArtists())
.then((res) => {
if (disposed) return
var data = res.body
var artists = data.artists
setUserFollowing(artists.total)
})
.catch((err) => {
if (disposed) return
setUserFollowing([])
setError(err)
});
return () => disposed = true
})
useEffect(() => {
let disposed = false
refreshableCall(() => api.getMyTopArtists({
limit: 10,
time_range: "long_term"
}))
.then((res) => {
if (disposed) return
var data = res.body
var artists = data.items
setUserTopArtists(artists)
setError(null)
})
.catch((err) => {
if (disposed) return
setUserTopArtists([])
setError(err)
});
return () => disposed = true
})
SpotifyContext.js
import React, { useState, useEffect, useContext } from "react"
import axios from "axios"
import SpotifyWebApi from 'spotify-web-api-node';
const spotifyApi = new SpotifyWebApi({
clientId: 1234567890,
});
export const SpotifyAuthContext = React.createContext({
exchangeCode: () => { throw new Error("context not loaded") },
refreshAccessToken: () => { throw new Error("context not loaded") },
hasToken: spotifyApi.getAccessToken() !== undefined,
api: spotifyApi
});
export const useSpotify = () => useContext(SpotifyAuthContext);
function setStoredJSON(id, obj) {
localStorage.setItem(id, JSON.stringify(obj));
}
function getStoredJSON(id, fallbackValue = null) {
const storedValue = localStorage.getItem(id);
return storedValue === null
? fallbackValue
: JSON.parse(storedValue);
}
export function SpotifyAuthContextProvider({ children }) {
const [tokenInfo, setTokenInfo] = useState(() => getStoredJSON('myApp:spotify', null))
const hasToken = tokenInfo !== null
useEffect(() => {
if (tokenInfo === null) return;
// attach tokens to `SpotifyWebApi` instance
spotifyApi.setCredentials({
accessToken: tokenInfo.accessToken,
refreshToken: tokenInfo.refreshToken,
})
// persist tokens
setStoredJSON('myApp:spotify', tokenInfo)
}, [tokenInfo])
function exchangeCode(code) {
return axios
.post("http://localhost:3001/login", {
code
})
.then(res => {
// TODO: Confirm whether response contains `accessToken` or `access_token`
const { accessToken, refreshToken, expiresIn } = res.data;
// store expiry time instead of expires in
setTokenInfo({
accessToken,
refreshToken,
expiresAt: Date.now() + (expiresIn * 1000)
});
})
}
function refreshAccessToken() {
const refreshToken = tokenInfo.refreshToken;
return axios
.post("http://localhost:3001/refresh", {
refreshToken
})
.then(res => {
const refreshedTokenInfo = {
accessToken: res.data.accessToken,
// some refreshes may include a new refresh token!
refreshToken: res.data.refreshToken || tokenInfo.refreshToken,
// store expiry time instead of expires in
expiresAt: Date.now() + (res.data.expiresIn * 1000)
}
setTokenInfo(refreshedTokenInfo)
// attach tokens to `SpotifyWebApi` instance
spotifyApi.setCredentials({
accessToken: refreshedTokenInfo.accessToken,
refreshToken: refreshedTokenInfo.refreshToken,
})
return refreshedTokenInfo
})
}
async function refreshableCall(callApiFunc) {
if (Date.now() > tokenInfo.expiresAt)
await refreshAccessToken();
try {
return await callApiFunc()
} catch (err) {
if (err.name !== "WebapiAuthenticationError")
throw err; // rethrow irrelevant errors
}
// if here, has an authentication error, try refreshing now
return refreshAccessToken()
.then(callApiFunc)
}
return (
<SpotifyAuthContext.Provider value={{
api: spotifyApi,
exchangeCode,
hasToken,
refreshableCall,
refreshAccessToken
}}>
{children}
</SpotifyAuthContext.Provider>
)
}
Errors
Without the dependency, it keeps cycling and firing off requests, likely hundreds per second. (Error 429)
With the dependency, it seems the Access Token is being ignored or sidestepped. (Error: WebApiAuthentication - No token provided)
What I have tried to do ?
I tried to implement all the requests in a single useEffect, still getting the errors.
Calling useEffect with dependency array and without.
Link to the Github Repo
https://github.com/amoghkapoor/spotify-clone
status 429 means you have made too many calls in a specific time window.
you are therefore banned for this specific time window.
try waiting a bit before retrying.
did you try :
useEffect(..., [])
this guaranties it will be run only once.
None of your useEffect calls are using a dependency array, remember if useEffect is called without any dependencies it goes into an infinite loop. Either find what dependency or state change should re-run the useEffect hook and include it in the dependency array:
useEffect(() => { /* your logic */ }, [dependencies])
or if there are no dependencies simply fire it once the component mounts:
useEffect(() => { /* your logic */ }, [])
I'm new to redux and pulling out my hair trying to get a basic test to work with redux and moxios.
API is just axios, with some custom headers set.
I get an error on my post method:
TypeError: Cannot read property 'then' of undefined
my method:
const login = ({username, password}) => (dispatch) => {
dispatch(actions.loginRequested());
return API.post(`curavi/v2/authentication`, {username, password})
.then(response => dispatch(actions.loginSuccess(response.data.payload)))
.catch((error) => errorHandler(dispatch, error.response));
};
My Test case:
describe('login', () => {
beforeEach(function () {
// import and pass your custom axios instance to this method
moxios.install(API)
});
afterEach(function () {
// import and pass your custom axios instance to this method
moxios.uninstall(API)
});
test('calls loginSuccess when the response is successful', () => {
const store = mockStore();
const mockData = {
data: { payload: 'yay' }
};
moxios.wait(() => {
const request = API.requests.mostRecent();
request.respondWith({
status: 200,
response: mockData
});
});
const expectededActions = [
{type: types.LOGIN_REQUESTED},
{type: types.LOGIN_SUCCESS, payload: 'yay'}
];
actions.loginRequested.mockReturnValue({type: types.LOGIN_REQUESTED});
actions.loginSuccess.mockReturnValue({type: types.LOGIN_SUCCESS, payload: 'yay'});
actions.loginFail.mockReturnValue({type: types.LOGIN_FAIL, message: 'boo'});
return store.dispatch(operations.login({username: 'theuser', password: 'thepassword'}))
.then(() => {
expect(store.getActions()).toEqual(expectededActions);
expect(API.post).toHaveBeenCalledWith('curavi/v2/authentication',
{username: 'theuser', password: 'thepassword'});
});
})
});
Are you sure you get a TypeError in login as you suggest? It doesn't make sense; you'd get that error if API were not an axios instance, in which case API.post() could return undefined. On the other hand, your test won't work for 2 reasons:
You need to replace API.requests.mostRecent() with moxios.requests.mostRecent().
The function you have inside moxios' await won't execute for 0.5 secs, see here. If the return statement in your test were to be reached before then, your test would simply return a promise. You could do the following instead:
test('...', async () => {
// ...
const result = await store.dispatch(
operations.login({
username: 'theuser',
password: 'thepassword',
})
);
expect(store.getActions()).toEqual(expectededActions);
expect(API.post).toHaveBeenCalledWith(/* ... */);
});
You should also make sure to set up the store correctly:
import configureStore from 'redux-mock-store';
import thunk from 'redux-thunk';
const middlewares = [thunk];
const mockStore = configureStore(middlewares);
// use your store inside your tests
const store = mockStore();
I'm working in a project with react and redux, I'm enough new so I'm trying to understand better how to use redux-thunk and redux-promise together.
Below you can see my files, in my actions I created a fetch generic function apiFetch() in order to use every time I need to fetch. This function return a promise, that I'm going to resolve in loadBooks(), the code is working and the records are uploaded but when I check the log of the actions I see that the first action is undefined, after there is BOOKS_LOADING, LOAD_BOOKS, BOOKS_LOADING and LOAD_BOOKS_SUCCESS.
I've 2 questions about that:
1) Why is the first action undefined and I've LOAD_BOOKS instead than LOAD_BOOKS_START?
action # 22:54:37.403 undefined
core.js:112 prev state Object {activeBook: null, booksListing: Object}
core.js:116 action function (dispatch) {
var url = './src/data/payload.json';
dispatch(booksIsLoading(true));
return dispatch({
type: 'LOAD_BOOKS',
payload: new Promise(function (resolve) {
…
core.js:124 next state Object {activeBook: null, booksListing: Object}
action # 22:54:37.404 BOOKS_LOADING
action # 22:54:37.413 LOAD_BOOKS
action # 22:54:39.420 BOOKS_LOADING
action # 22:54:39.425 LOAD_BOOKS_SUCCESS
2) If for example the url for the fetch is wrong, I expected to see the action LOAD_BOOKS_ERROR, instead this is the result of the log:
action # 23:06:06.837 undefined action # 23:06:06.837 BOOKS_LOADING
action # 23:06:06.846 LOAD_BOOKS GET
http://localhost:8000/src/data/payldoad.json 404 (Not Found) error
apiFetch Error: request failed at index.js:66 error
TypeError: Cannot read property 'json' of undefined at index.js:90
If I don't use apiFetch(), but normal fetch function, all is working correctly, also the part of the error, with the exception that anyway LOAD_BOOKS is not LOAD_BOOKS_START.
Thank you in advance for any help!
configureStore.js
import { createStore, applyMiddleware, compose, preloadedState } from 'redux';
import reducers from './configureReducer';
import configureMiddleware from './configureMiddleware';
const middleware = configureMiddleware();
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(reducers, preloadedState, composeEnhancers(applyMiddleware(...middleware)));
export default store;
actions/index.js
import fetch from 'isomorphic-fetch';
export const booksIsLoading = (bool) => {
return {
type: 'BOOKS_LOADING',
booksLoading: bool,
};
};
const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
export const apiFetch = (url) => {
const getPromise = () => (
fetch(url, {
method: 'GET',
})
.then((response) => {
if (response.status !== 200) {
throw Error('request failed');
}
return response;
})
.catch((err) => {
console.log('error apiFetch', err);
// dispatch(fetchBooksError(true));
})
);
return getPromise();
};
export const loadBooks = () => (dispatch) => {
const url = './src/data/payload.json';
dispatch(booksIsLoading(true));
return dispatch({
type: 'LOAD_BOOKS',
payload: new Promise((resolve) => {
delay(2000).then(() => {
apiFetch(`${url}`)
// fetch(`${url}`, {
// method: 'GET',
// })
.then((response) => {
resolve(response.json());
dispatch(booksIsLoading(false));
}).catch((err) => {
console.log('error', err);
});
});
}),
});
};
constants/application.js
export const LOAD_BOOKS = 'LOAD_BOOKS';
reducers/reducer_book.js
import initialState from '../model.js';
import * as types from '../constants/application';
export default function (state = initialState, action) {
switch (action.type) {
case `${types.LOAD_BOOKS}_SUCCESS`: {
console.log('reducer', action.payload);
const data = action.payload.data.items;
const items = Object.values(data);
if (items.length > 0) {
return {
...state,
books: Object.values(data),
booksFetched: true,
booksError: false,
};
}
return state;
}
case `${types.LOAD_BOOKS}_ERROR`: {
return {
...state,
booksError: true,
};
}
case 'BOOKS_LOADING':
return {
...state,
booksLoading: action.booksLoading,
};
default:
return state;
}
}
In which order did you specify middlewares?
Following usage makes action go undefined:
'applyMiddleware(reduxPromiseMiddleware(), reduxThunk)'
Please change the order to: ( thunk first! )
'applyMiddleware(reduxThunk, reduxPromiseMiddleware())'