Search code examples
reactjsreact-nativeasyncstorage

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.


Solution

  • 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:

    1. JavaScript throws AsyncStorage.mergeItem() into the function queue

    2. There is no await for this function that returns a Promise

    3. The interpreter does not wait for it to resolve and immediately moves to the next line of code

    4. 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)

    5. obj gets garbage collected 0.1ms after that

    6. 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:

    7. AsyncStorage then gives a confusing error message