Atom Lab logo

How to use React Native FS to access the filesystem

Tired of reinventing the wheel?

I'm building a React Native component library - built with Tailwind CSS - designed to help developers build awesome apps in record time.

Find out more

How to access the filesystem in React Native

Occasionally when working in React Native you might find that you need to access the native filesystem on iOS and Android. The best way to do this by using the react-native-fs package. In this article I’ll explain how react-native-fs works and how to use it.

What is React Native FS

React Native FS (or react-native-fs to use the proper name) is a package that allows you to easily work with the native filesystem in React Native.

Install react-native-fs with npm or yarn

To get started, install react-native-fs in your project by running the following command in your terminal, at the root of your React Native project:

npm install react-native-fs

Alternatively if your project is using Yarn, run the following command:

yarn add react-native-fs

Lastly we need to use React Native autolink to link the package:

react-native link react-native-fs

Done! You should now be able to use React Native FS.

How to write a file to the filesystem using react-native-fs

First, let's add a TextInput that writes it's value to state:

import React, { useState } from 'react'; import { SafeAreaView, StyleSheet, Text, TextInput, Button, View, Alert } from 'react-native'; const App = () => { const [fileText, setFileText] = useState(''); const saveFile = async () => { // }; return ( <SafeAreaView style={styles.wrapper}> <View style={styles.container}> <View style={styles.main}> <Text style={styles.title}>Enter text for your file:</Text> <TextInput value={fileText} onChangeText={setFileText} style={styles.textArea} multiline textAlignVertical="top" /> </View> <Button title="Save File" onPress={saveFile} /> </View> </SafeAreaView> ); }; const styles = StyleSheet.create({ wrapper: { flex: 1, }, container: { padding: 16, flex: 1, }, main: { flex: 1, display: 'flex', paddingVertical: 16, }, textArea: { height: 200, borderWidth: 1, borderColor: '#ccc', borderRadius: 4, marginBottom: 16, paddingLeft: 16, paddingRight: 16, paddingTop: 16, paddingBottom: 16, fontSize: 18, }, title: { fontSize: 24, fontWeight: 'bold', marginBottom: 16, color: '#333', }, }); export default App;

Our saveFile function doesn't currently do anything. Let's get it working. When we hit the save button, we want to save a text file containing the value of fileText. First, import RNFS at the top of your component file, like so:

import { DocumentDirectoryPath, writeFile } from 'react-native-fs';

Then we need to create the filesystem path that we want to write the file to. We do this using a constant called DocumentDirectoryPath:

const saveFile = async () => { const path = `${DocumentDirectoryPath}/${Date.now()}.txt`; // };

Then we can write the file to the filesystem using the writeFile function, and show an Alert to the user to confirm that the file has been saved:

const saveFile = async () => { const path = `${DocumentDirectoryPath}/${Date.now()}.txt`; try { await writeFile(path, fileText, 'utf8'); Alert.alert('File saved', null, [{ text: 'OK' }]); } catch (e) { console.log('error', e); } };

Here's our complete form:

import React, { useState } from 'react'; import { SafeAreaView, StyleSheet, Text, TextInput, Button, View, Alert } from 'react-native'; import { DocumentDirectoryPath, writeFile } from 'react-native-fs'; const App = () => { const [fileText, setFileText] = useState(''); const saveFile = async () => { const path = `${DocumentDirectoryPath}/${Date.now()}.txt`; try { await writeFile(path, fileText, 'utf8'); Alert.alert('File saved', null, [{ text: 'OK' }]); } catch (e) { console.log('error', e); } }; return ( <SafeAreaView style={styles.wrapper}> <View style={styles.container}> <View style={styles.main}> <Text style={styles.title}>Enter text for your file:</Text> <TextInput value={fileText} onChangeText={setFileText} style={styles.textArea} multiline textAlignVertical="top" /> </View> <Button title="Save File" onPress={saveFile} /> </View> </SafeAreaView> ); }; const styles = StyleSheet.create({ wrapper: { flex: 1, }, container: { padding: 16, flex: 1, }, main: { flex: 1, display: 'flex', paddingVertical: 16, }, textArea: { height: 200, borderWidth: 1, borderColor: '#ccc', borderRadius: 4, marginBottom: 16, paddingLeft: 16, paddingRight: 16, paddingTop: 16, paddingBottom: 16, fontSize: 18, }, title: { fontSize: 24, fontWeight: 'bold', marginBottom: 16, color: '#333', }, }); export default App;
Saving a file on Android
Saving a file on iOS

As you can see, we're now saving the file to the filesystem. Great stuff! Save a few documents using them form and lets use react-native-fs to query them and display them in a list.

How to list files using react-native-fs

As our App will now have two separate views, lets move our create file component into it's own view:

const CreateView = () => { const [fileText, setFileText] = useState(''); const saveFile = async () => { const path = `${DocumentDirectoryPath}/${Date.now()}.txt`; try { await writeFile(path, fileText, 'utf8'); Alert.alert('File saved', null, [{ text: 'OK' }]); } catch (e) { console.log('error', e); } }; return ( <> <View style={styles.main}> <Text style={styles.title}>Enter text for your file:</Text> <TextInput value={fileText} onChangeText={setFileText} style={styles.textArea} multiline textAlignVertical="top" /> </View> <Button title="Save File" onPress={saveFile} /> </> ); }; const App = () => { return <CreateView />; };

Let's create a ListView component that will eventually contain our list of files, and contains a button that will open our CreateView using a state value we're going to create with useState called createViewActive:

const ListView = () => { return ( <> <View style={styles.main}> <Text>Files here</Text> </View> <Button title="Create new file" onPress={() => setCreateViewActive(true)} /> </> ); };

Let's also change our saveFile function so that it sets createViewActive to false once our file has been saved, so that we go back to the ListView:

const CreateView = ({ setCreateViewActive }) => { // const saveFile = async () => { const path = `${DocumentDirectoryPath}/${Date.now()}.txt`; try { await writeFile(path, fileText, 'utf8'); setCreateViewActive(false); } catch (e) { console.log('error', e); } }; // };

In our App component, we'll create a createViewActive state value. If the value is true, our CreateView will be rendered - if it's false, our ListView will be rendered.

const App = () => { const [createViewActive, setCreateViewActive] = useState(false); const getCurrentView = () => { if (createViewActive) { return <CreateView setCreateViewActive={setCreateViewActive} />; } return <ListView setCreateViewActive={setCreateViewActive} />; }; return ( <SafeAreaView style={styles.wrapper}> <View style={styles.container}>{getCurrentView()}</View> </SafeAreaView> ); };
Create file toggle on Android
Create file toggle on iOS

Now let's display a list of the files we've created. In order to do that we need to use the readDir function.

First we'll create two state values - a boolean we set to true once we've loaded our data, and an array to hold our list of files:

const ListView = ({ setCreateViewActive }) => { const [ready, setReady] = useState(false); const [files, setFiles] = useState(null); /// };

Now we'll add a useEffect hook to load our files when the component mounts. Using the readDir function, we'll read our DocumentDirectoryPath which will give us an array of our files:

import React, { useState, useEffect } from 'react'; import { DocumentDirectoryPath, writeFile, readDir } from 'react-native-fs'; const ListView = ({ setCreateViewActive }) => { const [ready, setReady] = useState(false); const [files, setFiles] = useState(null); useEffect(() => { (async () => { try { const filesArr = await readDir(DocumentDirectoryPath); setFiles(filesArr); setReady(true); } catch (e) { console.log('error', e); } })(); return () => null; }, []); /// };

Before we render our list of files, we need to add a LoadingView component to show when our ready state value is false:

const LoadingView = () => ( <View style={styles.loading}> <Text style={styles.body}>Loading...</Text> </View> ); const styles = StyleSheet.create({ /// loading: { flex: 1, display: 'flex', justifyContent: 'center', alignItems: 'center', }, body: { fontSize: 18, }, });

To render our list of files, we'll use the React Native Flatlist component to output a TouchableOpacity containing the file name:

import { DocumentDirectoryPath, writeFile, readDir } from 'react-native-fs'; const ListView = ({ setCreateViewActive }) => { /// const renderItem = ({ item }) => ( <TouchableOpacity style={styles.file}> <Text style={styles.body}>{item.name}</Text> </TouchableOpacity> ); return ( <> <View style={styles.main}> {files.length > 0 ? ( <FlatList data={files} renderItem={renderItem} keyExtractor={(item) => item.name} /> ) : ( <Text>No files</Text> )} </View> <Button title="Create new file" onPress={() => setCreateViewActive(true)} /> </> ); };
The list of saved files on Android
The list of saved files on iOS

Now you should be able to see a list of files you've saved! Here's our complete App as of now:

import React, { useState, useEffect } from 'react'; import { SafeAreaView, StyleSheet, Text, TextInput, Button, View, TouchableOpacity, FlatList, } from 'react-native'; import { DocumentDirectoryPath, writeFile, readDir } from 'react-native-fs'; const LoadingView = () => ( <View style={styles.loading}> <Text style={styles.body}>Loading...</Text> </View> ); const CreateView = ({ setCreateViewActive }) => { const [fileText, setFileText] = useState(''); const saveFile = async () => { const path = `${DocumentDirectoryPath}/${Date.now()}.txt`; try { await writeFile(path, fileText, 'utf8'); setCreateViewActive(false); } catch (e) { console.log('error', e); } }; return ( <> <View style={styles.main}> <Text style={styles.title}>Enter text for your file:</Text> <TextInput value={fileText} onChangeText={setFileText} style={styles.textArea} multiline textAlignVertical="top" /> </View> <Button title="Save File" onPress={saveFile} /> </> ); }; const ListView = ({ setCreateViewActive }) => { const [ready, setReady] = useState(false); const [files, setFiles] = useState(null); useEffect(() => { (async () => { try { const filesArr = await readDir(DocumentDirectoryPath); setFiles(filesArr); setReady(true); } catch (e) { console.log('error', e); } })(); return () => null; }, []); if (!ready) return <LoadingView />; const renderItem = ({ item }) => ( <TouchableOpacity style={styles.file}> <Text style={styles.body}>{item.name}</Text> </TouchableOpacity> ); return ( <> <View style={styles.main}> {files.length > 0 ? ( <FlatList data={files} renderItem={renderItem} keyExtractor={(item) => item.name} /> ) : ( <Text>No files</Text> )} </View> <Button title="Create new file" onPress={() => setCreateViewActive(true)} /> </> ); }; const App = () => { const [createViewActive, setCreateViewActive] = useState(false); const getCurrentView = () => { if (createViewActive) { return <CreateView setCreateViewActive={setCreateViewActive} />; } return <ListView setCreateViewActive={setCreateViewActive} />; }; return ( <SafeAreaView style={styles.wrapper}> <View style={styles.container}>{getCurrentView()}</View> </SafeAreaView> ); }; const styles = StyleSheet.create({ wrapper: { flex: 1, }, container: { padding: 16, flex: 1, }, main: { flex: 1, display: 'flex', paddingVertical: 16, }, textArea: { height: 200, borderWidth: 1, borderColor: '#ccc', borderRadius: 4, marginBottom: 16, paddingLeft: 16, paddingRight: 16, paddingTop: 16, paddingBottom: 16, fontSize: 18, }, title: { fontSize: 24, fontWeight: 'bold', marginBottom: 16, color: '#333', }, loading: { flex: 1, display: 'flex', justifyContent: 'center', alignItems: 'center', }, body: { fontSize: 18, }, }); export default App;

How to read a single file from the filesystem using react-native-fs

We can now create a file and view a list of files that we've created. But what if we just want to read a single file from our filesystem?

Currently our list view is a bit condensed, so it's not very tap-friendly. Let's add some vertical padding to each list item to make them easier to select:

// const styles = StyleSheet.create({ // file: { paddingVertical: 12, }, });
List view with added padding on Android
List view with added padding on iOS

Let's create a selectedFile state value in our App component. We'll check if this value is truthy to detect whether we should display our single file view, and it will contain the file object that we pass to that single file view:

// const FileView = ({ file }) => { return ( <> <View style={styles.main}> <Text>{file.name}</Text> </View> </> ); }; const App = () => { const [createViewActive, setCreateViewActive] = useState(false); const [selectedFile, setSelectedFile] = useState(null); const getCurrentView = () => { if (createViewActive) { return <CreateView setCreateViewActive={setCreateViewActive} />; } if (selectedFile) { return <FileView file={selectedFile} />; } return <ListView setCreateViewActive={setCreateViewActive} />; }; return ( <SafeAreaView style={styles.wrapper}> <View style={styles.container}>{getCurrentView()}</View> </SafeAreaView> ); };

When the user taps on one of the files in the list, we’ll select the file and open the single view:

const ListView = ({ setCreateViewActive, selectFile }) => { // const renderItem = ({ item }) => ( <TouchableOpacity style={styles.file} onPress={() => selectFile(item)}> <Text style={styles.body}>{item.name}</Text> </TouchableOpacity> ); // }; const App = () => { // const [selectedFile, setSelectedFile] = useState(null); const getCurrentView = () => { // return <ListView setCreateViewActive={setCreateViewActive} selectFile={setSelectedFile} />; }; // };

When the view is mounted, we’ll use the path value with the readFile method to return our file contents and display them in the UI:

// import { DocumentDirectoryPath, writeFile, readDir, stat, readFile } from 'react-native-fs'; // const FileView = ({ file }) => { const [ready, setReady] = useState(false); const [contents, setContents] = useState(null); useEffect(() => { (async () => { try { const fileStat = await stat(file.path); if (fileStat.isFile()) { const fileData = await readFile(fileStat.path, 'utf8'); setContents(fileData); setReady(true); } } catch (e) { console.log('e', e); } })(); return () => null; }, [file]); if (!ready) return <LoadingView />; return ( <> <View style={styles.main}> <Text style={styles.title}>{file.name}</Text> <Text style={styles.body}>{contents}</Text> </View> </> ); };

Finally we'll add a close button to the header to allow the user to return back to the list view, by resetting the selectedFile value to null.

const FileView = ({ file, onClose }) => { // return ( <> <View style={styles.header}> <TouchableOpacity onPress={onClose}> <Text style={styles.body}>&lt; Back</Text> </TouchableOpacity> </View> <View style={styles.main}> <Text style={styles.title}>{file.name}</Text> <Text style={styles.body}>{contents}</Text> </View> </> ); }; const App = () => { const [createViewActive, setCreateViewActive] = useState(false); const [selectedFile, setSelectedFile] = useState(null); const getCurrentView = () => { // if (selectedFile) { return <FileView file={selectedFile} onClose={() => setSelectedFile(null)} />; } // }; // };
Added close button on Android
Added close button on iOS

Here's our App with the single file view:

import React, { useState, useEffect } from 'react'; import { SafeAreaView, StyleSheet, Text, TextInput, Button, View, TouchableOpacity, FlatList, } from 'react-native'; import { DocumentDirectoryPath, writeFile, readDir, stat, readFile } from 'react-native-fs'; const LoadingView = () => ( <View style={styles.loading}> <Text style={styles.body}>Loading...</Text> </View> ); const FileView = ({ file, onClose }) => { const [ready, setReady] = useState(false); const [contents, setContents] = useState(null); useEffect(() => { (async () => { try { const fileStat = await stat(file.path); if (fileStat.isFile()) { const fileData = await readFile(fileStat.path, 'utf8'); setContents(fileData); setReady(true); } } catch (e) { console.log('e', e); } })(); return () => null; }, [file]); if (!ready) return <LoadingView />; return ( <> <View style={styles.header}> <TouchableOpacity onPress={onClose}> <Text style={styles.body}>&lt; Back</Text> </TouchableOpacity> </View> <View style={styles.main}> <Text style={styles.title}>{file.name}</Text> <Text style={styles.body}>{contents}</Text> </View> </> ); }; const CreateView = ({ setCreateViewActive }) => { const [fileText, setFileText] = useState(''); const saveFile = async () => { const path = `${DocumentDirectoryPath}/${Date.now()}.txt`; try { await writeFile(path, fileText, 'utf8'); setCreateViewActive(false); } catch (e) { console.log('error', e); } }; return ( <> <View style={styles.main}> <Text style={styles.title}>Enter text for your file:</Text> <TextInput value={fileText} onChangeText={setFileText} style={styles.textArea} multiline textAlignVertical="top" /> </View> <Button title="Save File" onPress={saveFile} /> </> ); }; const ListView = ({ setCreateViewActive, selectFile }) => { const [ready, setReady] = useState(false); const [files, setFiles] = useState(null); useEffect(() => { (async () => { try { const filesArr = await readDir(DocumentDirectoryPath); setFiles(filesArr); setReady(true); } catch (e) { console.log('error', e); } })(); return () => null; }, []); if (!ready) return <LoadingView />; const renderItem = ({ item }) => ( <TouchableOpacity style={styles.file} onPress={() => selectFile(item)}> <Text style={styles.body}>{item.name}</Text> </TouchableOpacity> ); return ( <> <View style={styles.main}> {files.length > 0 ? ( <FlatList data={files} renderItem={renderItem} keyExtractor={(item) => item.name} /> ) : ( <Text>No files</Text> )} </View> <Button title="Create new file" onPress={() => setCreateViewActive(true)} /> </> ); }; const App = () => { const [createViewActive, setCreateViewActive] = useState(false); const [selectedFile, setSelectedFile] = useState(null); const getCurrentView = () => { if (createViewActive) { return <CreateView setCreateViewActive={setCreateViewActive} />; } if (selectedFile) { return <FileView file={selectedFile} onClose={() => setSelectedFile(null)} />; } return <ListView setCreateViewActive={setCreateViewActive} selectFile={setSelectedFile} />; }; return ( <SafeAreaView style={styles.wrapper}> <View style={styles.container}>{getCurrentView()}</View> </SafeAreaView> ); }; const styles = StyleSheet.create({ wrapper: { flex: 1, }, container: { padding: 16, flex: 1, }, main: { flex: 1, display: 'flex', paddingVertical: 16, }, textArea: { height: 200, borderWidth: 1, borderColor: '#ccc', borderRadius: 4, marginBottom: 16, paddingLeft: 16, paddingRight: 16, paddingTop: 16, paddingBottom: 16, fontSize: 18, }, title: { fontSize: 24, fontWeight: 'bold', marginBottom: 16, color: '#333', }, loading: { flex: 1, display: 'flex', justifyContent: 'center', alignItems: 'center', }, body: { fontSize: 18, }, file: { paddingVertical: 12, }, }); export default App;

How to delete a file using react-native-fs

To delete a file, we need to use the unlink method. First, lets add a delete button to our single file view:

const FileView = ({ file, onClose }) => { // return ( <> <View style={styles.header}> <TouchableOpacity onPress={onClose}> <Text style={styles.body}>&lt; Back</Text> </TouchableOpacity> </View> <View style={styles.main}> <Text style={styles.title}>{file.name}</Text> <Text style={styles.body}>{contents}</Text> </View> <Button title="Delete File" color="red" /> </> ); };

When that button is pressed, we’ll run the unlink method using our file path, and run our onClose method to clear the selectedFile value and return to the ListView:

import { DocumentDirectoryPath, writeFile, readDir, stat, readFile, unlink } from 'react-native-fs'; const FileView = ({ file, onClose }) => { // const deleteFile = async () => { try { await unlink(file.path); onClose(); } catch (e) { console.log('e', e); } }; // return ( <> <View style={styles.header}> <TouchableOpacity onPress={onClose}> <Text style={styles.body}>&lt; Back</Text> </TouchableOpacity> </View> <View style={styles.main}> <Text style={styles.title}>{file.name}</Text> <Text style={styles.body}>{contents}</Text> </View> <Button title="Delete File" onPress={deleteFile} color="red" /> </> ); };

And we're done! We can now create a file, view it in a list, and delete it.

Creating, viewing and deleting a file on Android
Creating, viewing and deleting a file on iOS

Our Complete React Native FS example

import React, { useState, useEffect } from 'react'; import { SafeAreaView, StyleSheet, Text, TextInput, Button, View, TouchableOpacity, FlatList, } from 'react-native'; import { DocumentDirectoryPath, writeFile, readDir, stat, readFile, unlink } from 'react-native-fs'; const LoadingView = () => ( <View style={styles.loading}> <Text style={styles.body}>Loading...</Text> </View> ); const FileView = ({ file, onClose }) => { const [ready, setReady] = useState(false); const [contents, setContents] = useState(null); useEffect(() => { (async () => { try { const fileStat = await stat(file.path); if (fileStat.isFile()) { const fileData = await readFile(fileStat.path, 'utf8'); setContents(fileData); setReady(true); } } catch (e) { console.log('e', e); } })(); return () => null; }, [file]); const deleteFile = async () => { try { await unlink(file.path); onClose(); } catch (e) { console.log('e', e); } }; if (!ready) return <LoadingView />; return ( <> <View style={styles.header}> <TouchableOpacity onPress={onClose}> <Text style={styles.body}>&lt; Back</Text> </TouchableOpacity> </View> <View style={styles.main}> <Text style={styles.title}>{file.name}</Text> <Text style={styles.body}>{contents}</Text> </View> <Button title="Delete File" onPress={deleteFile} color="red" /> </> ); }; const CreateView = ({ setCreateViewActive }) => { const [fileText, setFileText] = useState(''); const saveFile = async () => { const path = `${DocumentDirectoryPath}/${Date.now()}.txt`; try { await writeFile(path, fileText, 'utf8'); setCreateViewActive(false); } catch (e) { console.log('error', e); } }; return ( <> <View style={styles.main}> <Text style={styles.title}>Enter text for your file:</Text> <TextInput value={fileText} onChangeText={setFileText} style={styles.textArea} multiline textAlignVertical="top" /> </View> <Button title="Save File" onPress={saveFile} /> </> ); }; const ListView = ({ setCreateViewActive, selectFile }) => { const [ready, setReady] = useState(false); const [files, setFiles] = useState(null); useEffect(() => { (async () => { try { const filesArr = await readDir(DocumentDirectoryPath); setFiles(filesArr); setReady(true); } catch (e) { console.log('error', e); } })(); return () => null; }, []); if (!ready) return <LoadingView />; const renderItem = ({ item }) => ( <TouchableOpacity style={styles.file} onPress={() => selectFile(item)}> <Text style={styles.body}>{item.name}</Text> </TouchableOpacity> ); return ( <> <View style={styles.main}> {files.length > 0 ? ( <FlatList data={files} renderItem={renderItem} keyExtractor={(item) => item.name} /> ) : ( <Text>No files</Text> )} </View> <Button title="Create new file" onPress={() => setCreateViewActive(true)} /> </> ); }; const App = () => { const [createViewActive, setCreateViewActive] = useState(false); const [selectedFile, setSelectedFile] = useState(null); const getCurrentView = () => { if (createViewActive) { return <CreateView setCreateViewActive={setCreateViewActive} />; } if (selectedFile) { return <FileView file={selectedFile} onClose={() => setSelectedFile(null)} />; } return <ListView setCreateViewActive={setCreateViewActive} selectFile={setSelectedFile} />; }; return ( <SafeAreaView style={styles.wrapper}> <View style={styles.container}>{getCurrentView()}</View> </SafeAreaView> ); }; const styles = StyleSheet.create({ wrapper: { flex: 1, }, container: { padding: 16, flex: 1, }, main: { flex: 1, display: 'flex', paddingVertical: 16, }, textArea: { height: 200, borderWidth: 1, borderColor: '#ccc', borderRadius: 4, marginBottom: 16, paddingLeft: 16, paddingRight: 16, paddingTop: 16, paddingBottom: 16, fontSize: 18, }, title: { fontSize: 24, fontWeight: 'bold', marginBottom: 16, color: '#333', }, loading: { flex: 1, display: 'flex', justifyContent: 'center', alignItems: 'center', }, body: { fontSize: 18, }, file: { paddingVertical: 12, }, }); export default App;

Copyright 2024 - Atom Lab | Privacy Policy | Terms and Conditions