When you build mobile apps in react-native, eventually you will come across a use-case which requires you to persist data in local storage.
Most popular alternative to achieving this task today is Async Storage
I'm actually questioning this assumption -- Async Storage seems to be antiquated and outdated. You really do not know how it's implemented, and it may use completely different approach for storing your data on different platforms (Android vs iOS).
AsyncStorage has serious limitations for how much data you can store -- around 6Mb. Yes, resources on mobile devices are scarce, and you have to be very careful not to kill your user's devices, but, if you know what you are doing -- 6Mb seems to be too restrictive for modern smart phones.
Since AsyncStorage uses file system to store the data anyways, why not to create a simple alternative which does not have these limitations, and, perhaps, will be more flexible and will be able to provide additional features and benefits when needed?
Here is my attempt at building a simple wrapper around expo-file-system, which will offer features comparable to Async Storage , but will not have the same limitations.
Did I say comparable features? Well, I will not attempt to implement every method AsyncStorage provides. Instead, I will implement the bare minimum one may need to effectively store/retrieve data.
Another goal is to keep this little module as simple as possible, as such, the dependencies should also be kept to a minimum.
The only import I have in the implementation file is expo-file-system:
import * as FileSystem from 'expo-file-system'
I also import CONST like this:
import * as CONST from './consts.js'
The only const that is defined in the consts.js file is:
const DOCUMENT_FOLDER = `${FileSystem.documentDirectory}`
FileSystem.documentDirectory is one of the default locations supported by expo-filesystem, and behaves exactly like we need. Unlike FileSystem.cacheDirectory, which behaves like a magic (it may clean up files when it feels like it, and it will not be reflecting anywhere the amount of the storage used).
Just like async-storage, expo-storage works with string values and can only store string data, so in order to store object data you need to serialize it first. For data that can be serialized to JSON you can use JSON.stringify() when saving the data and JSON.parse() when loading the data back.
Import the module in your code like this:
import { Storage } from 'expo-storage'
setItem
Here is is how setItem function is implemented in our npm module:
setItem: async ({ key, value }) => {
const writtenContents =
await FileSystem.writeAsStringAsync(
`${CONST.DOCUMENT_FOLDER}${key}`,
value
)
return writtenContents
},
Very straight forward. Since we are using keys as file names, be careful not to override keys and not to cause file names collision. We are not doing any extra work to create and maintain another subfolder -- the documentDirectory is unique per application, so it's highly unlikely that you'd use the same key more than once in your app.
We expect that writeAsStringAsync will create a new file if it does not exist, and will override contents of already existing file.
To store an item, you can call the method like this:
await Storage.setItem({
key: `${photo.id}`,
value: JSON.stringify(myJsonObject),
})
You can also use keys that correspond to file system naming convention, for instance "@my_key". Just be careful with special characters, as they may have a special meaning in a file system naming (don't use slash, back slash, dot, quotes etc'...).
getItem
getItem: async ({ key }) => {
try {
const value =
await FileSystem
.readAsStringAsync(`${CONST.DOCUMENT_FOLDER}${key}`)
return value
} catch (error) {
return null
}
},
Very straight forward, simple wrapper. Here we tried to implement behavior similar to the AsyncStorage -- if the item does not exist, it will return null.
And to use it in your code:
const item =
JSON.parse(await Storage.getItem({ key: `${item.id}` }))
removeItem
removeItem: async ({ key }) => {
await FileSystem.deleteAsync(
`${CONST.DOCUMENT_FOLDER}${key}`,
{ idempotent: true } // don't throw an error if there is
// no file or directory at this URI
)
},
To delete a record by key:
await Storage.removeItem({ key: `${item.id}` })
getAllKeys
getAllKeys: async () => {
const keys =
await FileSystem.readDirectoryAsync(
`${CONST.DOCUMENT_FOLDER}`
)
return keys
},
This will return all keys (file names) stored in the FileSystem.documentDirectory. You may want to filter only the keys you created. To be able to achieve this, you may prefix all your keys with "@".
Again, calling this method is very straight forward:
const keys = await Storage.getAllKeys()
Conclusion
As you can see, all methods we are defining are async, and will always return promise.
One may question, since this implementation is so simple, why not to use the FileSystem API directly? Well, this little library does abstract away and hides some unnecessary implementation details from the user, and, in case we do want to change the implementation later, we can do it without having to change our applications that already use the module (assuming we will keep the API unchanged of course). The result of using this module in our apps -- our code will be simple to read and concise.
And that's all. The Async Storage does implement variety of additional methods, but, we will stay minimalistic and will consider adding them to our simple npm only if needed.
This code is open-sourced and published as an npm module, so any feedback or suggestions will be greatly appreciated.
It's been successfully used in my react-native app and I hope you like it too.
Comments