React Native – CRUD Aplikasi Todo Menggunakan AsyncStorage
Pada artikel ini akan membahas aplikasi simple TodoApp menggunakan penyimpanan sederhana yaitu 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.
Berdasarkan penjelasan dari website React Native dapat diartikan AsyncStorage adalah sistem penyimpanan nilai kunci yang tidak terenkripsi, asinkron, persisten, dan bersifat global untuk aplikasi. Itu harus digunakan sebagai pengganti LocalStorage. Disarankan agar Anda menggunakan abstraksi di atas AsyncStorage daripada AsyncStorage secara langsung untuk apa pun selain penggunaan ringan karena beroperasi secara global.
Jadi AsyncStorage merupakan penyimpanan yang tidak di enkripsi, sehingga tidak disarankan digunakan untuk aplikasi dengan proses yang kompleks, Kita bisa menggunakannya sebagai penyimpanan sederhana seperti konfigurasi aplikasi mobile, namun ingat apabila ingin menyimpan informasi krusial lebih baik ditambahkan enkripsi terlebih dahulu.
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',
},
});
File App.js merupakan bagian utama dari aplikasi, terdapat berbagai fungsi CRUD, seperti addTodo, deleteTodo, updateTodo dan lain sebagainya. Berikut adalah fungsi menyimpan langsung ke AsyncStorage dengan nama kunci 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
Contoh sumber sudah di modifikasi agar dapat berjalan langsung melalui Snack, di antaranya: penambahan react-native-get-random-values agar bisa menggunakan uuid, penambahan fungsi addTodo di property onSubmitEditing={this.addTodo} pada TextInput, dan perbaikan sintak minor
Sumber : https://blog.eduonix.com/mobile-programming/learn-build-react-native-todo-application-part-1/