AsyncStorage complains of array and object incompatibility during mergeItem - reactjs

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.

Related

Any more optimized way to handle the mutiple slices in react components

I have created slices for POST and GET operation -
export const getAllAnnotationType = createAsyncThunk<IGetAllAnnotationType>(
"annotationType/getAll",
async (annotationType, { rejectWithValue }) => {
try {
const response = await Services.get("/globalAnnotationTypes");
return response.data.slice().reverse() as IGetAllAnnotationType;
} catch (err) {
const error = err as any;
return rejectWithValue(error?.response?.data);
}
}
);
export const createAnnotationType = createAsyncThunk<
ICreateAnnotationType,
{ name: string; des: string }
>("annotationType/create", async (annotationType, { rejectWithValue }) => {
try {
const { name, des } = annotationType;
const response = await Services.post("/globalAnnotationTypes", {
name,
des,
});
return response.data as ICreateAnnotationType;
} catch (err) {
const error = err as any;
return rejectWithValue(error?.response?.data);
}
});
I have created one common slice for both of the operations.
And I am using these in my react components like this -
useEffect(() => {
switch (typeLoader) {
case 'pending':
setLoader(true);
break;
case 'succeeded':
if (getAllData) {
dispatch(resetAnnotationType());
setLoader(false);
setRows(getAllData);
}
if (createData) {
showToast();
dispatch(getAllAnnotationType());
setNoticeMsg(createData);
}
break;
case 'failed':
showToast();
}
}, [typeLoader]);
I am looking for more optimized way to use them in react components. In future, I will have DELETE and PUT operations, and I will have to use more conditions inside 'succeeded' state. Any other way I can do this ?
My favorite way to handle fetching data is Redux Toolkit Query.
It creates slices. You can custom the fetch function if needed, and you are given hooks, to work with in the component directly.
You can also cache the fetched data. A simple example of use can be (in the component):
const {data, isLoading, error} = useGetDataQuery(args) //if you needed args
If you had further questions about sharing cached data across the application, you can comment here, I'll be happy to share how I solved my problem (using something called fixedCacheQuery).
Even though you can find the boilerplate on the website, if you needed, let me know and I will edit this response to share the boilerplate as I write and use it.

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;
}
});

How to throw argument in RTK Query (queryFn)

I have queryFn query in RTK, and I need to get some data from firebase DB by element ID. But when I give this arg to queryFn like in example below, I got undefined.
and I'm calling it like this:
The reason you got undefined is because the useGetCardByIdQuery hook returns the data undefined initially. The data is going to be available after a success fetch.
As far I understand from your code, you are trying to get the cards of authorized firebase user; so you don't need to pass any id indeed since I see that you are not using the id in the queryFn.
In that case, just pass the undefined like useGetCardByIdQuery(undefined); and return the cardList.
And for better typing, you can define the builder query with <OutputType, InputType>
getCardsById: builder.query<CardList, string>({
queryFn: async (id, api, extraOptions, fetchWithBQ) => {
try {
const user = getAuth();
...
const cardList = cardSnapshot.docs.map(doc => doc.data())
return { data: cardList }
} catch (error) {
return { error }
}
},
})
Then you can call the hook in the component.
const response = useGetCardsByIdQuery(undefined);
if (response.data) {
const cards = response.data;
console.log(cards);
}

react promise in functional component with UseEffect and UseState doesn't work

I'm having issue fetching data and setting them to state in a functional component using useEffect and useState.
My problem is that I would like to keep the data fetching done with axios async/await in a separate file for improving application scalability but then I don't understand how to update the state in case the promise is resolved (not rejected).
In particular I'm trying to retrieve from the promise an array of table rows called data in state, but I can't figure out how to set the result of the responce in the state
Here's the code in the component file:
const [data, setData] = React.useState([]);
useEffect(() => {
const { id } = props.match.params;
props.getTableRows(id).then((res) => {
setData(res);
});
//or is it better:
//props.getTableRows(id).then(setData); ?
}, []);
and my action.js:
export const getTableRows = (id, history) => async (dispatch) => {
try {
const res = await axios.get(`/api/test/${id}`);
dispatch({
type: GET_TEST,
payload: res.data.rows,
});
} catch (error) {
history.push("/test");
}
};
In the above picture it can be seen that the rows array inside the promise response called in action.js is present.
This code unfortunately doesn't work, error: Uncaught (in promise) TypeError: Cannot read property 'forEach' of undefined
I've found out another solution which is the define the promise in the useEffect method like this:
useEffect(() => {
const { id } = props.match.params;
const fetchData = async () => {
const result = await axios.get(`/api/test/${id}`);
setData(result.data.rows);
};
fetchData();
}, []);
this code is working in my app but as I said I don't like having the promises in the components files I would like instead to have them all the promise in action.js for app scalability (in case url change I don't have to change all files) but in that case I don't know where to put the setData(result.data.rows); which seems the right choise in this last example
Any suggestions?
Thanks
You still need to use async/await. The .then() is executed when the value is returned, however your function will continue rendering and won't wait for it. (causing it to error our by trying to access forEach on a null state). After it errors the promise via .then() will update the values and that is why you can see them in the console.
useEffect(() => {
async function getData() {
const { id } = props.match.params;
await props.getTableRows(id).then((res) => {
setData(res);
});
}
getData()
}, []);
Additionally, before you access a state you can check for null values (good practice in general).
if (this.state.somestate != null) {
//Run code using this.state.somestate
}
I don't see you return anything from getTableRows. You just dispatch the result, but hadn't return the res for the function call.
And it will be helpful if you provided error trace.

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

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.

Resources