Why is my AsyncStorage.getItem().then undefined in React Native? - reactjs

I am using AsyncStorage to get information. I previously stored but for some strange reason it is saying Cannot read property 'then' of undefined even though I use this exact same AsyncStorage function in the function below and it works just fine. Does anyone know why this isn't working here?
AsyncStorage.getItem(STORAGE_KEY_PRODUCT_SEARCH_CACHE).then((results) => {
const searchCache = JSON.parse(results);
let containedMatches = [];
if (searchCache) {
containedMatches = searchCache.filter((searchCacheItem, i) => {
return searchCacheItem.includes(searchTerm);
});
}
dispatch({
type: types.HANDLE_LOAD_PRODUCTS_SUCCESS,
containedMatches
}
);
});
Here is a video I made of this. Sorry you can hear my co-workers in the background so you'll have to mute it.
https://youtu.be/qwhywbD74l8

Use await to wait the async operation to finish like this:
async setData() {
var resultCache= await AsyncStorage.getItem(STORAGE_KEY_PRODUCT_SEARCH_CACHE);
}
you can do your further functional work using this code.

Related

How do I separate api / async request logic from react components when using recoil

So at the moment I am having to put my request / api logic directly into my components because what I need to do a lot of the time is set state based on the response I get from the back end.
Below is a function that I have on my settings page that I use to save the settings to recoil after the user hits save on the form:
const setUserConfig = useSetRecoilState(userAtoms.userConfig);
const submitSettings = async (values: UserConfigInterface) => {
try {
const { data: {data} } = await updateUser(values);
setUserConfig({
...data
});
} catch (error) {
console.log('settings form error: ', error);
}
}
This works perfectly...I just dont want the function in my component as most of my components are getting way bigger than they need to be.
I have tried making a separate file to do this but I can only use the recoil hooks (in this instance useSetRecoilState) inside of components and it just complains when I try and do this outside of a react component.
I have tried implementing this with recoils selector and selectorFamily functions but it gets kind of complicated. Here is how I have tried it inside of a file that has atoms / selectors only:
export const languageProgress = atom<LanguageProgress>({
key: "LanguageProgress",
default: {
level: 1,
xp: 0,
max_xp: 0
}
})
export const languageProgressUpdate = selectorFamily<LanguageProgress>({
key: "LanguageProgress",
get: () => async () => {
try {
const { data: { data } } = await getLanguageProgress();
return data;
} catch (error) {
console.log('get language progress error');
}
},
set: (params:object) => async ({set}) => {
try {
const { data: { data } } = await updateLanguageProgress(params);
set(languageProgress, {
level: data.level,
xp: data.xp,
max_xp: data.max_xp
});
} catch (error) {
console.log('language progress update error: ', error);
}
}
});
What I want to do here is get the values I need from the back end and display it in the front which I can do in the selector function get but now I have 2 points of truth for this...my languageProgress atom will initially be incorrect as its not getting anything from the database so I have to use useGetRevoilValue on the languageProgressUpdate selector I have made but then when I want to update I am updating the atom and not the actual value.
I cannot find a good example anywhere that does what I am trying to here (very suprisingly as I would have thought it is quite a common way to do things...get data from back end and set it in state.) and I can't figure out a way to do it without doing it in the component (as in the first example). Ideally I would like something like the first example but outside of a component because that solution is super simple and works for me.
So I dont know if this is the best answer but it does work and ultimately what I wanted to do was seperate the logic from the screen component.
The answer in my situation is a bit long winded but this is what I used to solve the problem: https://medium.com/geekculture/crud-with-recoiljs-and-remote-api-e36581b77168
Essentially the answer is to put all the logic into a hook and get state from the api and set it there.
get data from back end and set it in state
You may be looking for useRecoilValueLoadable:
"This hook is intended to be used for reading the value of asynchronous selectors. This hook will subscribe the component to the given state."
Here's a quick demonstration of how I've previously used it. To quickly summarise: you pass useRecoilValueLoadable a selector (that you've defined somewhere outside the logic of the component), that selector grabs the data from your API, and that all gets fed back via useRecoilValueLoadable as an array of 1) the current state of the value returned, and 2) the content of that API call.
Note: in this example I'm passing an array of values to the selector each of which makes a separate API call.
App.js
const { state, contents } = useRecoilValueLoadable(myQuery(arr));
if (state.hasValue && contents.length) {
// `map` over the contents
}
selector.js
import { selectorFamily } from 'recoil';
export const myQuery = selectorFamily({
key: 'myQuery',
get: arr => async () => {
const promises = arr.map(async item => {
try {
const response = await fetch(`/endpoint/${item.id}`);
if (response.ok) return response.json();
throw Error('API request not fulfilled');
} catch (err) {
console.log(err);
}
});
const items = await Promise.all(promises);
return items;
}
});

RTK Query response state

I'm trying to convert some Axio code to RTK query and having some trouble. The 'data' response from RTK query doesn't seem to act like useState as I thought.
Original axio code:
const [ importantData, setImportantData ] = useState('');
useEffect(() => {
async function axiosCallToFetchData() {
const response = await axiosAPI.post('/endpoint', { payload });
const { importantData } = await response.data;
setImportantData(importantData);
}
axiosCallToFetchData()
.then((res) => res)
.catch((error) => console.log(error));
}, []);
const objectThatNeedsData.data = importantData;
New RTK Query code
const { data, isSuccess } = useGetImportantDataQuery({ payload });
if(isSuccess){
setImportantData(data.importantData);
}
const objectThatNeedsData.data = importantData;
This however is giving me an infinite render loop. Also if I try to treat the 'data' object as a state object and just throw it into my component as:
const objectThatNeedsData.data = data.importantData;
Then I get an undefined error because it's trying to load the importantData before it's completed. I feel like this should be a simple fix but I'm getting stuck. I've gone through the docs but most examples just use the if statement block to check the status. The API calls are being made atleast with RTK and getting proper responses. Any advice?
Your first problem is that you always call setImportantData during render, without checking if it is necessary - and that will always cause a rerender. If you want to do that you need to check if it is even necessary:
if(isSuccess && importantData != data.importantData){
setImportantData(data.importantData);
}
But as you noticed, that is actually not necessary - there is hardly ever any need to copy stuff into local state when you already have access to it in your component.
But if accessing data.importantData, you need to check if data is there in the first place - you forgot to check for isSuccess here.
if (isSuccess) {
objectThatNeedsData.data = data.importantData;
}
All that said, if objectThatNeedsData is not a new local variable that you are declaring during this render, you probably should not just modify that during the render in general.

react useEffect with async-await not working as expected

I have a useEffect hooks in my component, which makes a API call and I want it to run only on first render. But I'm unable to make the API call. what am I missing here?
useEffect(() => {
//should run on first render
(async () => {
const getAllSeasons = await getSeasonList();
setSeasons(getAllSeasons);
})();
}, []);
const getSeasonList = async () => {
if (state && state?.seasonList) {
return state?.seasonList;
} else {
const seasonData = await useSeasonService();
if (seasonData?.status === "loaded") {
return seasonData?.payload?.seasons || [];
} else if (seasonData.status == "error") {
return [];
}
}
};
Best way to fetch APIs using fetch method
fetch('https://apisaddress.me/api/')
.then(({ results }) => consoe.log(results) ));
Another aproach is using axios
const Func= async ()=>{
const response= await axios.get('https://pokeapi.co/api/v2/pokemon?
limit=500&offset=200')
this.setState({Data:response.data.results})
}
// npm install axios
Make the new function and clean all code from useEffect and put inside that function. and then call the function inside the useEffect. Like:
const sampleCall = async () => {
const getAllSeasons = await getSeasonList();
setSeasons(getAllSeasons);
}
useEffect(() => {
sampleCall()
}, [])
Follow these steps, if it is still not working then try to add seasons inside the useEffect array, [seasons].
Thank You.
useEffect works fine. The proof is here https://codesandbox.io/s/set-seasons-9e5cvn?file=/src/App.js.
So, the problem is in getSeasonList function.
await useSeasonService() won't work. React Hooks names start with use word and can't be called inside functions or conditionally. useSeasonService is considered by React engine as a custom hook. Chek this for more details:
https://reactjs.org/docs/hooks-custom.html#extracting-a-custom-hook
Your code example doesn't show what state is and how it's initialized.
state && state?.seasonList check is redundant. state?.seasonList is enough.
It's a bad practice to put such complex logic as in getSeasonList into React components. You'd better use some state container (Recoil might be a good choice if you have some small non-related states in the app).
P.S. You wrote a poor description of the problem and then gave minuses for answers. That's not fair. Nobody would help you if you'll continue to do such thing. You gave 4 minuses and received 4 minuses in response. It's better to clarify what's wrong before giving a plus or minus to any answer.

React playlist player - fetching data and storing the result problem

What I am trying to achieve
I am building a playlist player widget for my app using Soundcloud API. I am able to fetch the data but I am running into a design/technical issue when I need to implement the play/pause feature for a given track.
The GET method returns a JSON track (song) with multiple functions associated, such as response.play(), response.pause(), etc. Currently, my onClick() handle for pausing and playing the song fetches new data for the same track and overrides the current song, which is not what I want. I want to be able to play the song, the pause/play it without restarting.
JSFiddle (Wasn't able to implement StackOverflow JS/HTML/CSS snippet widjet)
[-------- Working JSFiddle here !!---------]
Playlist.tsx
const SC = require('soundcloud');
SC.initialize({
client_id: '1dff55bf515582dc759594dac5ba46e9'
});
const playIcon = 'https://www.pngfind.com/pngs/m/20-201074_play-button-png-image-background-circle-transparent-png.png';
const pauseIcon = 'https://thumbs.dreamstime.com/z/pause-button-icon-transparent-background-sign-flat-style-204042531.jpg';
async function getJson(trackId) {
let response = await SC.stream('/tracks/' + trackId);
return response;
}
const Playlist = () => {
const[playState, setPlayState] = React.useState(false);
const handlePlayClick = () => {
getJson(913128502).then((track) => {
track.setVolume(0.025); //increase if you don't hear background sound
playState ? track.pause() : track.play();
setPlayState(!playState);
});
}
return (
<img src={!playState ? playIcon : pauseIcon }
onClick={() => { handlePlayClick() } }
height="100" width="100" >
</img>
)
}
export default Playlist;
Things I have tried
I have looked at several SOs posts trying to understand how to store fetched data in a global variable. From my understanding, this can't quite be done because of synchronicity, I can only use the track GET response and its associated functions only inside the function
getJson(trackId).then((track) => {
// can only call track.play() or track.pause() locally here ?
});
To bypass this, I tried to do
let globalTrack;
getJson(trackId).then((track) => {
globalTrack = track;
});
globalTrack.play() // undefined error
I have further tried to use other tools like async/await but with no success.
I am at a loss for ideas and lost enough hair over this issue. Does anyone know how I can achieve what I want?
You can use useEffect() hook to get a track, it will fetch the track once when the component is first mounted.
https://jsfiddle.net/x7j0qwvh/20/
const [track, setTrack] = React.useState(null);
React.useEffect(() => {
musicFetch(913128502).then((music) => {
music.setVolume(0.025);
setTrack(music);
});
}, [])

AsyncStorage complains of array and object incompatibility during mergeItem

I'm trying to figure out why AsyncStorage in a React Native app refuses to do a mergeItem with my data. My function looks like this:
const arrToObj = (array) =>
array.reduce((obj, item) => {
obj[Object.keys(item)[0]] = Object.values(item)[0]
return obj
}, {})
export const addThingToThing = async (title, thing) => {
try {
let subThings = []
let things = {}
await AsyncStorage.getItem(THINGS_STORAGE_KEY)
.then((things) => {
let subThings = []
Object.values(JSON.parse(decks))
.map((thing) => {
if (Object.keys(thing)[0] === title) {
subThings = [...Object.values(thing)[0].subThings, subThing]
}
})
return { decks, subThings }
})
.then(({ decks, subThings }) => {
const obj = {
...arrToObj(JSON.parse(things)),
[title]: {
subThings
}
}
console.log(JSON.stringify(obj))
AsyncStorage.mergeItem(THINGS_STORAGE_KEY,
JSON.stringify(obj))
})
} catch (error) {
console.log(`Error adding thing to thing: ${error.message}`)
}
}
When I do the thing that executes this I get:
13:35:52: {"test":{"subThings":[{"one":"a","two":"a"}]},"test2":{"title":"test2","questions":[]}}
13:35:55: [Unhandled promise rejection: Error: Value [{"test":{"title":"test","subThings":[]}},{"test2":{"title":"test2","subThings":[]}}] of type org.json.JSONArray cannot be converted to JSONObject]
Which is confusing, because when the data is printed out it's an object with {...}, but AsyncStorage shows an array with [...]. Is there something I'm missing? This seems pretty dumb to me and I can't seem to figure out how to get RN to play nice.
PS. IMO the structure of the data is gross, but it's what I'm working with. I didn't decide on it, and I can't change it.
I recall working with AsyncStorage, and it is fundamentally different than localStorage because it returns Promises.
Your code looks fine which makes this super confusing, but I am suddenly suspecting that the problem may be due to a missing await keyword.
try:
.then(async ({ decks, subThings }) => {
const obj = {
...arrToObj(JSON.parse(things)),
[title]: {
subThings
}
}
console.log(JSON.stringify(obj))
// We are adding await here
await AsyncStorage.mergeItem(THINGS_STORAGE_KEY, JSON.stringify(obj))
})
It may be a classic "not waiting for the Promise to resolve" before moving on, async problem. Don't delete this question if so. It will be helpful for others including probably myself in the future.
Here's what I think is happening:
JavaScript throws AsyncStorage.mergeItem() into the function queue
There is no await for this function that returns a Promise
The interpreter does not wait for it to resolve and immediately moves to the next line of code
There is no next line of code, so it returns from the then() block 0.1ms later (keep in mind it is implicitly doing return undefined which inside an async function is the same as resolve(undefined), with the point being that it is trying to resolve the entire chain back up to await AsyncStorage.getItem(THINGS_STORAGE_KEY)
obj gets garbage collected 0.1ms after that
AsyncStorage.mergeItem() observes a falsy value where it was expecting obj, but I don't actually know for certain. It may not be doing step 5 or 6 and instead detecting that mergeItem is still pending when it tries to resolve the getItem chain, either way:
AsyncStorage then gives a confusing error message
What I ended up using to handle the promises correctly is:
export const addThingToThing = async (title, thing) => {
try {
AsyncStorage.getItem(THINGS_STORAGE_KEY, (err, things) => {
let subThings = []
Object.values(JSON.parse(decks))
.map((thing) => {
if (Object.keys(thing)[0] === title) {
subThings = [...Object.values(thing)[0].subThings, subThing]
}
})
const obj = {
[title]: {
title,
subThings
}
}
console.log(JSON.stringify(obj))
AsyncStorage.mergeItem(THINGS_STORAGE_KEY,
JSON.stringify(obj))
})
} catch (error) {
console.log(`Error adding thing to thing: ${error.message}`)
}
}
I saw some other similar examples online that used .done() that looked identical to what I'd done other than that call. That may have been something to try as well, but this worked for me.

Resources