React Native - CRUD Todo App Using AsyncStorage
This article will discuss a simple TodoApp application using simple storage, namely AsyncStorage.
AsyncStorage is an unencrypted, asynchronous, persistent, key-value storage system that is global to the app. It should be used instead of LocalStorage. It is recommended that you use an abstraction on top of AsyncStorage instead of AsyncStorage directly for anything more than light usage since it operates globally.
Based on the explanation from the website React Native it can be interpreted that AsyncStorageis a key value storage system which is unencrypted, asynchronous, persistent, and global for the application. It should be used instead of LocalStorage. It is recommended that you use the above abstraction AsyncStorage instead of AsyncStorage directly for anything but light use as it operates globally.
So AsyncStorageis non-encrypted storage, so it is not recommended to be used for applications with complex processes, We can use it as simple storage such as mobile application configurations, but remember if you want to store crucial information better added encryption first.
1. App.js
import React, { Component } from 'react';
import {
Text,
View,
TextInput,
StyleSheet,
StatusBar,
Dimensions,
Platform,
ScrollView,
ActivityIndicator,
AsyncStorage,
} from 'react-native';
import 'react-native-get-random-values';
import { v1 as uuidv1 } from 'uuid';
import { LinearGradient } from 'expo-linear-gradient';
const { height, width } = Dimensions.get('window');
import TodoList from './TodoList';
export default class App extends Component {
state = {
newTodoItem: '',
dataIsReady: false,
todos: {}, // add this
};
newTodoItemController = (textValue) => {
this.setState({
newTodoItem: textValue,
});
console.log('data', this.state);
};
componentDidMount = () => {
this.loadTodos();
};
loadTodos = async () => {
try {
const getTodos = await AsyncStorage.getItem('todos');
const parsedTodos = JSON.parse(getTodos);
this.setState({ dataIsReady: true, todos: parsedTodos || {} });
} catch (err) {
console.log(err);
}
};
saveTodos = (newToDos) => {
AsyncStorage.setItem('todos', JSON.stringify(newToDos));
};
addTodo = () => {
const { newTodoItem } = this.state;
if (newTodoItem !== '') {
this.setState((prevState) => {
const ID = uuidv1();
const newToDoObject = {
[ID]: {
id: ID,
isCompleted: false,
textValue: newTodoItem,
createdAt: Date.now(),
},
};
const newState = {
...prevState,
newTodoItem: '',
todos: {
...prevState.todos,
...newToDoObject,
},
};
this.saveTodos(newState.todos); // add this
return { ...newState };
});
}
};
deleteTodo = (id) => {
this.setState((prevState) => {
const todos = prevState.todos;
delete todos[id];
const newState = {
...prevState,
...todos,
};
this.saveTodos(newState.todos); // add this
return { ...newState };
});
};
inCompleteTodo = (id) => {
this.setState((prevState) => {
const newState = {
...prevState,
todos: {
...prevState.todos,
[id]: {
...prevState.todos[id],
isCompleted: false,
},
},
};
this.saveTodos(newState.todos); // add this
return { ...newState };
});
};
completeTodo = (id) => {
this.setState((prevState) => {
const newState = {
...prevState,
todos: {
...prevState.todos,
[id]: {
...prevState.todos[id],
isCompleted: true,
},
},
};
this.saveTodos(newState.todos); // add this
return { ...newState };
});
};
updateTodo = (id, textValue) => {
this.setState((prevState) => {
const newState = {
...prevState,
todos: {
...prevState.todos,
[id]: {
...prevState.todos[id],
textValue: textValue,
},
},
};
this.saveTodos(newState.todos); // add this
return { ...newState };
});
};
render() {
const { newTodoItem, dataIsReady, todos } = this.state;
if (!dataIsReady) {
return <ActivityIndicator />;
}
return (
<LinearGradient style={styles.container} colors={['#DA4453', '#89216B']}>
<StatusBar barStyle="light-content" />
<Text style={styles.appTitle}>Todo App{'\n'}Using AsyncStorage</Text>
<View style={styles.card}>
<TextInput
style={styles.input}
placeholder={'Add an item!'}
value={newTodoItem}
onChangeText={this.newTodoItemController}
placeholderTextColor={'#999'}
returnKeyType={'done'}
autoCorrect={false}
onSubmitEditing={this.addTodo}
/>
<ScrollView contentContainerStyle={styles.listContainer}>
{Object.values(todos).map((todo) => (
<TodoList
key={todo.id}
{...todo}
deleteTodo={this.deleteTodo}
inCompleteTodo={this.inCompleteTodo}
completeTodo={this.completeTodo}
updateTodo={this.updateTodo} // add this
/>
))}
</ScrollView>
</View>
</LinearGradient>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
backgroundColor: '#f23657',
},
appTitle: {
color: '#fff',
fontSize: 36,
marginTop: 60,
marginBottom: 30,
fontWeight: '300',
textAlign: 'center',
},
card: {
backgroundColor: '#fff',
flex: 1,
width: width - 25,
borderTopLeftRadius: 10,
borderTopRightRadius: 10,
...Platform.select({
ios: {
shadowColor: 'rgb(50,50,50)',
shadowOpacity: 0.5,
shadowRadius: 5,
shadowOffset: {
height: -1,
width: 0,
},
},
android: {
elevation: 5,
},
}),
},
input: {
padding: 20,
borderBottomColor: '#bbb',
borderBottomWidth: 1,
fontSize: 24,
},
listContainer: {
alignItems: 'center',
},
});
The App.js file is the main part of the application, there are various CRUD functions, such as addTodo, deleteTodo, updateTodo and so on. Here is the function to save directly to AsyncStorage with the key name todos.
saveTodos = (newToDos) => {
AsyncStorage.setItem('todos', JSON.stringify(newToDos));
};
2. TodoList.js
import React, { Component } from 'react';
import {
View,
Text,
StyleSheet,
TouchableOpacity,
Dimensions,
TextInput,
} from 'react-native';
const { height, width } = Dimensions.get('window');
import PropTypes from 'prop-types';
class TodoList extends Component {
constructor(props) {
super(props);
this.state = {
isEditing: false,
todoValue: props.textValue,
};
}
// in our class before we define state
static propTypes = {
textValue: PropTypes.string.isRequired,
isCompleted: PropTypes.bool.isRequired,
deleteTodo: PropTypes.func.isRequired,
id: PropTypes.string.isRequired,
inCompleteTodo: PropTypes.func.isRequired,
completeTodo: PropTypes.func.isRequired,
updateTodo: PropTypes.func.isRequired,
};
state = {
todoValue: '',
isEditing: false,
};
toggleItem = () => {
const { isCompleted, inCompleteTodo, completeTodo, id } = this.props;
if (isCompleted) {
inCompleteTodo(id);
} else {
completeTodo(id);
}
};
startEditing = () => {
this.setState({
isEditing: true,
});
};
finishEditing = () => {
this.setState({
isEditing: false,
});
};
controlInput = (textValue) => {
this.setState({ todoValue: textValue });
};
finishEditing = () => {
const { todoValue } = this.state;
const { id, updateTodo } = this.props;
updateTodo(id, todoValue);
this.setState({
isEditing: false,
});
};
render() {
const { isEditing, todoValue } = this.state;
const { textValue, id, deleteTodo, isCompleted } = this.props;
return (
<View style={styles.container}>
<View style={styles.rowContainer}>
<TouchableOpacity onPress={this.toggleItem}>
<View
style={[
styles.circle,
isCompleted ? styles.completeCircle : styles.incompleteCircle,
]}></View>
</TouchableOpacity>
{isEditing ? (
<TextInput
value={todoValue}
style={[
styles.text,
styles.input,
isCompleted ? styles.strikeText : styles.unstrikeText,
]}
multiline={true}
returnKeyType={'done'}
onBlur={this.finishEditing}
onChangeText={this.controlInput}
/>
) : (
<Text
style={[
styles.text,
isCompleted ? styles.strikeText : styles.unstrikeText,
]}>
{textValue}
</Text>
)}
</View>
{isEditing ? (
<View style={styles.buttons}>
<TouchableOpacity onPressOut={this.finishEditing}>
<View style={styles.buttonContainer}>
<Text style={styles.buttonText}>✅</Text>
</View>
</TouchableOpacity>
</View>
) : (
<View style={styles.buttons}>
<TouchableOpacity onPressOut={this.startEditing}>
<View style={styles.buttonContainer}>
<Text style={styles.buttonText}>✏</Text>
</View>
</TouchableOpacity>
<TouchableOpacity onPressOut={() => deleteTodo(id)}>
<View style={styles.buttonContainer}>
<Text style={styles.buttonText}>❌</Text>
</View>
</TouchableOpacity>
</View>
)}
</View>
);
}
}
const styles = StyleSheet.create({
container: {
width: width - 50,
borderBottomColor: '#bbb',
borderBottomWidth: StyleSheet.hairlineWidth,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
backgroundColor: '#fff',
},
input: {
marginVertical: 15,
width: width / 2,
paddingBottom: 5,
},
text: {
fontWeight: '500',
fontSize: 18,
marginVertical: 20,
},
// Styles
circle: {
width: 30,
height: 30,
borderRadius: 15,
// remove borderColor property from here
borderWidth: 3,
marginRight: 20,
},
completeCircle: {
borderColor: '#bbb',
},
incompleteCircle: {
borderColor: '#DA4453',
},
rowContainer: {
flexDirection: 'row',
width: width / 2,
alignItems: 'center',
justifyContent: 'space-between',
},
buttons: {
flexDirection: 'row',
},
buttonContainer: {
marginVertical: 10,
marginHorizontal: 10,
},
// Styles
strikeText: {
color: '#bbb',
textDecorationLine: 'line-through',
},
unstrikeText: {
color: '#29323c',
},
});
export default TodoList;
3. Dependencies
{
"dependencies": {
"uuid": "9.0.0",
"prop-types": "*",
"expo-constants": "~13.2.4",
"@expo/vector-icons": "^13.0.0",
"expo-linear-gradient": "~11.4.0",
"react-native-dropdownalert": "*",
"react-native-get-random-values": "~1.8.0"
}
}
Link : https://snack.expo.dev/@rudiahmad/react-native—todo-app-using-asyncstorage
The sample source has been modified to run directly through Snack, including: adding react-native-get-random-values to use uuid, adding the addTodo function in the onSubmitEditing= property {this.addTodo} on TextInput, and minor syntax fixes
Source : https://blog.eduonix.com/mobile-programming/learn-build-react-native-todo-application-part-1/