@sqliteai/todoapp 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.env.example ADDED
@@ -0,0 +1,3 @@
1
+ # Copy from the SQLite Cloud Dashboard
2
+ # eg: sqlitecloud://myhost.cloud:8860/my-remote-database.sqlite?apikey=myapikey
3
+ CONNECTION_STRING = "<your-connection-string>"
package/App.js ADDED
@@ -0,0 +1,32 @@
1
+ import { PaperProvider } from "react-native-paper";
2
+ import { NavigationContainer } from "@react-navigation/native";
3
+ import { createStackNavigator } from "@react-navigation/stack";
4
+ import Cover from "./screens/Cover";
5
+ import Home from "./screens/Home";
6
+ import AddTaskModal from "./components/AddTaskModal";
7
+ import { GestureHandlerRootView } from "react-native-gesture-handler";
8
+ import Categories from './screens/Categories';
9
+ import { SyncProvider } from './components/SyncContext';
10
+
11
+ export default function App() {
12
+ const Stack = createStackNavigator();
13
+
14
+ return (
15
+ <GestureHandlerRootView style={{ flex: 1 }}>
16
+ <SyncProvider>
17
+ <NavigationContainer>
18
+ <PaperProvider>
19
+ <Stack.Navigator>
20
+ <Stack.Screen name="Cover" component={Cover} />
21
+ <Stack.Screen name="Categories" component={Categories} />
22
+ <Stack.Screen name="Tasks" component={Home} />
23
+ <Stack.Group screenOptions={{ presentation: 'modal' }}>
24
+ <Stack.Screen name="Add Task" component={AddTaskModal} />
25
+ </Stack.Group>
26
+ </Stack.Navigator>
27
+ </PaperProvider>
28
+ </NavigationContainer>
29
+ </SyncProvider>
30
+ </GestureHandlerRootView>
31
+ );
32
+ }
package/README.md ADDED
@@ -0,0 +1,52 @@
1
+ # Expo CloudSync Example
2
+
3
+ A simple Expo example demonstrating SQLite synchronization with CloudSync. Build cross-platform apps that sync data seamlessly across devices.
4
+
5
+ ## 🚀 Quick Start
6
+
7
+ ### 1. Clone the template
8
+
9
+ Create a new project using this template:
10
+ ```bash
11
+ npx create-expo-app MyApp --template @sqliteai/todoapp
12
+ cd MyApp
13
+ npm install
14
+ ```
15
+
16
+ ### 2. Database Setup
17
+
18
+ 1. Create database in [SQLite Cloud Dashboard](https://dashboard.sqlitecloud.io/).
19
+ 2. Execute the exact schema from `to-do-app.sql`.
20
+ 3. Enable OffSync for all tables on the remote database from the **SQLite Cloud Dashboard -> Databases**.
21
+
22
+ ### 3. Environment Configuration
23
+
24
+ Rename the `.env.example` into `.env` and fill with your values.
25
+
26
+ > **⚠️ SECURITY WARNING**: This example puts database connection strings directly in `.env` files for demonstration purposes only. **Do not use this pattern in production.**
27
+ >
28
+ > **Why this is unsafe:**
29
+ > - Connection strings contain sensitive credentials
30
+ > - Client-side apps expose all environment variables to users
31
+ > - Anyone can inspect your app and extract database credentials
32
+ >
33
+ > **For production apps:**
34
+ > - Use the secure [sport-tracker-app](https://github.com/sqliteai/sqlite-sync/tree/main/examples/sport-tracker-app) pattern with authentication tokens and row-level security
35
+ > - Never embed database credentials in client applications
36
+
37
+ ### 4. Build and run the App
38
+
39
+ ```bash
40
+ npx expo prebuild # run once
41
+ npm start
42
+ ```
43
+
44
+ ## ✨ Features
45
+
46
+ - **Add Tasks** - Create new tasks with titles and optional tags.
47
+ - **Edit Task Status** - Update task status when completed.
48
+ - **Delete Tasks** - Remove tasks from your list.
49
+ - **Dropdown Menu** - Select categories for tasks from a predefined list.
50
+ - **Cross-Platform** - Works on iOS and Android
51
+ - **Offline Support** - Works offline, syncs when connection returns
52
+
package/app.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "expo": {
3
+ "name": "todoapp",
4
+ "slug": "todoapp",
5
+ "version": "1.0.0",
6
+ "orientation": "portrait",
7
+ "icon": "./assets/icon.png",
8
+ "userInterfaceStyle": "light",
9
+ "description": "A todo app demonstrating SQLite CloudSync extension functionalities",
10
+ "keywords": ["sqlite", "cloudsync", "todo", "sync"],
11
+ "privacy": "public",
12
+ "platforms": ["ios", "android"],
13
+ "plugins": [
14
+ "./plugins/CloudSyncSetup.js"
15
+ ],
16
+ "splash": {
17
+ "image": "./assets/splash.png",
18
+ "resizeMode": "contain",
19
+ "backgroundColor": "#ffffff"
20
+ },
21
+ "ios": {
22
+ "supportsTablet": true,
23
+ "bundleIdentifier": "ai.sqlite.todoapp"
24
+ },
25
+ "android": {
26
+ "adaptiveIcon": {
27
+ "foregroundImage": "./assets/adaptive-icon.png",
28
+ "backgroundColor": "#ffffff"
29
+ },
30
+ "package": "ai.sqlite.todoapp"
31
+ },
32
+ "extra": {
33
+ "eas": {
34
+ "projectId": "faffd54b-fc15-4368-987a-a73b08640cfd"
35
+ }
36
+ },
37
+ "owner": "sqliteai"
38
+ }
39
+ }
Binary file
Binary file
Binary file
@@ -0,0 +1,16 @@
1
+ module.exports = function (api) {
2
+ api.cache(false);
3
+ return {
4
+ presets: ["babel-preset-expo"],
5
+ plugins: [
6
+ "react-native-paper/babel",
7
+ [
8
+ "module:react-native-dotenv",
9
+ {
10
+ moduleName: "@env",
11
+ path: ".env",
12
+ },
13
+ ],
14
+ ],
15
+ };
16
+ };
@@ -0,0 +1,120 @@
1
+ import { useState, useEffect } from "react";
2
+ import { View, StyleSheet, Alert } from "react-native";
3
+ import { TextInput, Button, Modal } from "react-native-paper";
4
+ import DropdownMenu from "./DropdownMenu";
5
+ import { db } from "../db/dbConnection";
6
+
7
+ export default AddTaskModal = ({
8
+ modalVisible,
9
+ addTaskTag,
10
+ setModalVisible,
11
+ }) => {
12
+ const [taskTitle, setTaskTitle] = useState("");
13
+ const [tagsList, setTagsList] = useState([]);
14
+ const [selectedTag, setSelectedTag] = useState({});
15
+
16
+ const closeModal = () => {
17
+ setModalVisible(false);
18
+ };
19
+
20
+ const handleAddTask = () => {
21
+ if (taskTitle.trim()) {
22
+ addTaskTag({ title: taskTitle.trim(), isCompleted: false }, selectedTag);
23
+ setTaskTitle("");
24
+ setSelectedTag({});
25
+ closeModal();
26
+ } else {
27
+ Alert.alert("Please add a new task.");
28
+ }
29
+ };
30
+
31
+ const getTags = async () => {
32
+ try {
33
+ const tags = await db.execute("SELECT * FROM tags");
34
+ setTagsList(tags.rows);
35
+ } catch (error) {
36
+ console.error("Error getting tags", error);
37
+ }
38
+ };
39
+
40
+ useEffect(() => {
41
+ getTags();
42
+ }, []);
43
+
44
+ return (
45
+ <Modal
46
+ style={styles.modalContainer}
47
+ visible={modalVisible}
48
+ onDismiss={closeModal}
49
+ >
50
+ <View>
51
+ <View style={styles.newTaskBox}>
52
+ <TextInput
53
+ mode="flat"
54
+ style={[
55
+ styles.textInput
56
+ ]}
57
+ contentStyle={styles.textInputContent}
58
+ underlineColor="transparent"
59
+ activeUnderlineColor="#6BA2EA"
60
+ value={taskTitle}
61
+ onChangeText={setTaskTitle}
62
+ label="Enter a new task"
63
+ keyboardType="default"
64
+ />
65
+ </View>
66
+ <DropdownMenu tagsList={tagsList} setSelectedTag={setSelectedTag} />
67
+ <Button
68
+ style={styles.addTaskButton}
69
+ textColor="black"
70
+ onPress={handleAddTask}
71
+ >
72
+ Add task
73
+ </Button>
74
+ </View>
75
+ </Modal>
76
+ );
77
+ };
78
+
79
+ const styles = StyleSheet.create({
80
+ modalContainer: {
81
+ flex: 1,
82
+ backgroundColor: "white",
83
+ padding: 10,
84
+ },
85
+ newTaskBox: {
86
+ flexDirection: "row",
87
+ alignItems: "center",
88
+ justifyContent: "center",
89
+ borderWidth: 1,
90
+ borderColor: "lightgray",
91
+ backgroundColor: "#f0f5fd",
92
+ marginBottom: 10,
93
+ },
94
+ textInput: {
95
+ width: "100%",
96
+ backgroundColor: "transparent",
97
+ height: 50,
98
+ },
99
+ textInputContent: {
100
+ backgroundColor: "transparennt",
101
+ borderWidth: 0,
102
+ paddingLeft: 10,
103
+ },
104
+ button: {
105
+ height: 50,
106
+ width: 50,
107
+ justifyContent: "center",
108
+ alignItems: "center",
109
+ },
110
+ closeButton: {
111
+ alignItems: "flex-start",
112
+ bottom: 180,
113
+ left: -10,
114
+ zIndex: 1,
115
+ },
116
+ addTaskButton: {
117
+ backgroundColor: "#b2cae9",
118
+ marginTop: 10,
119
+ },
120
+ });
@@ -0,0 +1,67 @@
1
+ import { View, StyleSheet } from "react-native";
2
+ import RNPickerSelect from "react-native-picker-select";
3
+
4
+ export default DropdownMenu = ({ tagsList, selectedTag, setSelectedTag }) => {
5
+ const TAGS = tagsList.map((tag) => {
6
+ return { label: tag.name, value: tag.name };
7
+ });
8
+
9
+ const getTagUuid = (value) => {
10
+ const tagUuid = tagsList.filter((tag) => {
11
+ return tag.name === value;
12
+ });
13
+ return tagUuid[0]?.uuid;
14
+ };
15
+
16
+ return (
17
+ <View style={styles.container}>
18
+ <RNPickerSelect
19
+ items={TAGS}
20
+ onValueChange={(value) =>
21
+ setSelectedTag({ uuid: getTagUuid(value), name: value })
22
+ }
23
+ placeholder={{ label: "Select a category", value: null }}
24
+ value={selectedTag}
25
+ style={pickerSelectStyles}
26
+ />
27
+ </View>
28
+ );
29
+ };
30
+
31
+ const styles = StyleSheet.create({
32
+ container: {
33
+ borderColor: "lightgray",
34
+ borderWidth: 1,
35
+ borderRadius: 5,
36
+ backgroundColor: "#f0f5fd",
37
+ marginBottom: 10,
38
+ },
39
+ });
40
+
41
+ const pickerSelectStyles = StyleSheet.create({
42
+ inputIOSContainer: { pointerEvents: "none" },
43
+ inputIOS: {
44
+ height: 50,
45
+ fontSize: 16,
46
+ paddingVertical: 12,
47
+ paddingHorizontal: 10,
48
+ borderWidth: 1,
49
+ borderColor: "lightgray",
50
+ borderRadius: 4,
51
+ color: "black",
52
+ backgroundColor: "#f0f5fd",
53
+ paddingRight: 30,
54
+ },
55
+ inputAndroid: {
56
+ height: 50,
57
+ fontSize: 16,
58
+ paddingHorizontal: 10,
59
+ paddingVertical: 8,
60
+ borderWidth: 1,
61
+ borderColor: "lightgray",
62
+ borderRadius: 4,
63
+ color: "black",
64
+ backgroundColor: "#f0f5fd",
65
+ paddingRight: 30,
66
+ },
67
+ });
@@ -0,0 +1,119 @@
1
+ import { createContext, useContext, useEffect, useState, useRef } from 'react';
2
+ import { InteractionManager, AppState } from 'react-native';
3
+ import { db } from '../db/dbConnection';
4
+
5
+ const SyncContext = createContext();
6
+
7
+ export const SyncProvider = ({ children }) => {
8
+ const refreshCallbacks = useRef(new Set());
9
+ const [isSyncEnabled, setIsSyncEnabled] = useState(false);
10
+ const isCheckingRef = useRef(false);
11
+ const lastCheckTimeRef = useRef(0);
12
+ const appStateRef = useRef(AppState.currentState);
13
+ const timeoutIdRef = useRef(null);
14
+
15
+ const registerRefreshCallback = (callback) => {
16
+ refreshCallbacks.current.add(callback);
17
+ return () => refreshCallbacks.current.delete(callback);
18
+ };
19
+
20
+ const triggerRefresh = () => {
21
+ refreshCallbacks.current.forEach(callback => callback());
22
+ };
23
+
24
+ const setSync = (enabled) => {
25
+ setIsSyncEnabled(enabled);
26
+ };
27
+
28
+ useEffect(() => {
29
+ if (!isSyncEnabled) return;
30
+
31
+ const checkForChanges = () => {
32
+ // Skip if already checking or app is in background
33
+ if (isCheckingRef.current || appStateRef.current !== 'active') {
34
+ timeoutIdRef.current = setTimeout(checkForChanges, 1000);
35
+ return;
36
+ }
37
+
38
+ // Throttle checks - don't check more than once every 2 seconds
39
+ const now = Date.now();
40
+ if (now - lastCheckTimeRef.current < 2000) {
41
+ timeoutIdRef.current = setTimeout(checkForChanges, 2000 - (now - lastCheckTimeRef.current));
42
+ return;
43
+ }
44
+
45
+ // Use InteractionManager to wait for UI interactions to complete
46
+ InteractionManager.runAfterInteractions(async () => {
47
+ if (isCheckingRef.current) return;
48
+
49
+ isCheckingRef.current = true;
50
+ lastCheckTimeRef.current = Date.now();
51
+
52
+ try {
53
+ // Use a timeout for the database query to prevent hanging
54
+ const queryPromise = db.execute('SELECT cloudsync_network_check_changes();');
55
+ const timeoutPromise = new Promise((_, reject) =>
56
+ setTimeout(() => reject(new Error('Query timeout')), 5000)
57
+ );
58
+
59
+ const result = await Promise.race([queryPromise, timeoutPromise]);
60
+
61
+ if (result.rows && result.rows.length > 0 && result.rows[0]['cloudsync_network_check_changes()'] > 0) {
62
+ console.log(`${result.rows[0]['cloudsync_network_check_changes()']} changes detected, triggering refresh`);
63
+ // Defer refresh to next tick to avoid blocking current interaction
64
+ setTimeout(() => triggerRefresh(), 0);
65
+ }
66
+ } catch (error) {
67
+ console.error('Error checking for changes:', error);
68
+ } finally {
69
+ isCheckingRef.current = false;
70
+
71
+ // Schedule next check with adaptive interval
72
+ const nextInterval = appStateRef.current === 'active' ? 2500 : 10000;
73
+ timeoutIdRef.current = setTimeout(checkForChanges, nextInterval);
74
+ }
75
+ });
76
+ };
77
+
78
+ // Handle app state changes
79
+ const handleAppStateChange = (nextAppState) => {
80
+ appStateRef.current = nextAppState;
81
+
82
+ // If app becomes active and sync is enabled, check immediately
83
+ if (nextAppState === 'active' && isSyncEnabled && !isCheckingRef.current) {
84
+ if (timeoutIdRef.current) {
85
+ clearTimeout(timeoutIdRef.current);
86
+ }
87
+ timeoutIdRef.current = setTimeout(checkForChanges, 100);
88
+ }
89
+ };
90
+
91
+ const subscription = AppState.addEventListener('change', handleAppStateChange);
92
+
93
+ // Start checking after a small delay to let UI settle
94
+ timeoutIdRef.current = setTimeout(checkForChanges, 1000);
95
+
96
+ return () => {
97
+ if (timeoutIdRef.current) {
98
+ clearTimeout(timeoutIdRef.current);
99
+ timeoutIdRef.current = null;
100
+ }
101
+ isCheckingRef.current = false;
102
+ subscription?.remove();
103
+ };
104
+ }, [isSyncEnabled]);
105
+
106
+ return (
107
+ <SyncContext.Provider value={{ registerRefreshCallback, setSync }}>
108
+ {children}
109
+ </SyncContext.Provider>
110
+ );
111
+ };
112
+
113
+ export const useSyncContext = () => {
114
+ const context = useContext(SyncContext);
115
+ if (!context) {
116
+ throw new Error('useSyncContext must be used within a SyncProvider');
117
+ }
118
+ return context;
119
+ };
@@ -0,0 +1,103 @@
1
+ import { useState, useRef, useEffect } from "react";
2
+ import { View, Text, StyleSheet, TouchableOpacity } from "react-native";
3
+ import Icon from "react-native-vector-icons/FontAwesome";
4
+ import { Swipeable } from "react-native-gesture-handler";
5
+
6
+ export default TaskRow = ({ task, updateTask, handleDelete }) => {
7
+ const { uuid, title, isCompleted, tag_uuid, tag_name } = task;
8
+ const [checked, setChecked] = useState(isCompleted);
9
+ const swipableRef = useRef(null);
10
+
11
+ useEffect(() => {
12
+ setChecked(isCompleted);
13
+ }, [isCompleted]);
14
+
15
+ const handleIconPress = () => {
16
+ const newCompletedStatus = checked === 1 ? 0 : 1;
17
+ setChecked(newCompletedStatus);
18
+ updateTask(newCompletedStatus, uuid);
19
+ };
20
+
21
+ const renderLeftActions = () => {
22
+ return (
23
+ <TouchableOpacity
24
+ style={styles.deleteButton}
25
+ onPress={() => {
26
+ handleDelete(uuid);
27
+ if (swipableRef.current) {
28
+ swipableRef.current.close();
29
+ }
30
+ }}
31
+ >
32
+ <Text style={styles.deleteButtonText}>Delete</Text>
33
+ </TouchableOpacity>
34
+ );
35
+ };
36
+
37
+ return (
38
+ <Swipeable renderLeftActions={renderLeftActions} ref={swipableRef}>
39
+ <View style={styles.taskRow}>
40
+ <View style={styles.taskAndTag}>
41
+ {checked === 0 ? (
42
+ <Text style={styles.text}>{title}</Text>
43
+ ) : (
44
+ <Text style={styles.strikethroughText}>{title}</Text>
45
+ )}
46
+ <Text style={styles.tag}>{tag_name}</Text>
47
+ </View>
48
+ <TouchableOpacity onPress={handleIconPress}>
49
+ <Icon
50
+ name={checked === 1 ? "check-circle" : "circle-thin"}
51
+ size={20}
52
+ color={"#6BA2EA"}
53
+ />
54
+ </TouchableOpacity>
55
+ </View>
56
+ <View style={styles.dottedBox} />
57
+ </Swipeable>
58
+ );
59
+ };
60
+
61
+ const styles = StyleSheet.create({
62
+ taskRow: {
63
+ flexDirection: "row",
64
+ justifyContent: "space-between",
65
+ fontSize: 16,
66
+ padding: 10,
67
+ },
68
+ deleteButton: {
69
+ backgroundColor: "#6BA2EA",
70
+ padding: 10,
71
+ alignItems: "center",
72
+ justifyContent: "center",
73
+ },
74
+ dottedBox: {
75
+ borderWidth: 1,
76
+ borderColor: "lightgray",
77
+ borderStyle: "dashed",
78
+ },
79
+ deleteButtonText: {
80
+ color: "white",
81
+ },
82
+ text: {
83
+ fontSize: 16,
84
+ },
85
+ strikethroughText: {
86
+ textDecorationLine: "line-through",
87
+ textDecorationColor: "#6BA2EA",
88
+ fontSize: 16,
89
+ },
90
+ tag: {
91
+ color: "gray",
92
+ fontSize: 12,
93
+ marginTop: 5,
94
+ },
95
+ taskAndTag: {
96
+ flexDirection: "column",
97
+ },
98
+ actions: {
99
+ flexDirection: "row",
100
+ justifyContent: "center",
101
+ alignItems: "center"
102
+ }
103
+ });
@@ -0,0 +1,3 @@
1
+ import { open } from "@op-engineering/op-sqlite";
2
+
3
+ export const db = open({ name: 'to-do-app' });
@@ -0,0 +1,94 @@
1
+ import { useState, useEffect } from 'react'
2
+ import { Platform } from 'react-native';
3
+ import { db } from "../db/dbConnection";
4
+ import { CONNECTION_STRING } from "@env";
5
+ import { getDylibPath } from "@op-engineering/op-sqlite";
6
+ import { randomUUID } from 'expo-crypto';
7
+ import { useSyncContext } from '../components/SyncContext';
8
+
9
+ const useCategories = () => {
10
+ const [moreCategories, setMoreCategories] = useState(['Work', 'Personal'])
11
+ const { registerRefreshCallback, setSync } = useSyncContext()
12
+
13
+ const getCategories = async () => {
14
+ try {
15
+ const tags = await db.execute('SELECT * FROM tags')
16
+ const allCategories = tags.rows.map(tag => tag.name)
17
+ setMoreCategories(allCategories)
18
+ } catch (error) {
19
+ console.error('Error getting tags/categories', error)
20
+ }
21
+ }
22
+
23
+ const addCategory = async newCategory => {
24
+ try {
25
+ await db.execute('INSERT INTO tags (uuid, name) VALUES (?, ?) RETURNING *', [randomUUID(), newCategory])
26
+ db.execute('SELECT cloudsync_network_send_changes();')
27
+ setMoreCategories(prevCategories => [...prevCategories, newCategory])
28
+ } catch (error) {
29
+ console.error('Error adding category', error)
30
+ }
31
+ }
32
+
33
+ const initializeTables = async () => {
34
+ let extensionPath;
35
+
36
+ console.log('Loading extension...');
37
+ try {
38
+ if (Platform.OS == "ios") {
39
+ extensionPath = getDylibPath("ai.sqlite.cloudsync", "CloudSync");
40
+ } else {
41
+ extensionPath = 'cloudsync';
42
+ }
43
+
44
+ db.loadExtension(extensionPath);
45
+
46
+ const version = await db.execute('SELECT cloudsync_version();');
47
+ console.log(`Cloudsync extension loaded successfully, version: ${version.rows[0]['cloudsync_version()']}`);
48
+ } catch (error) {
49
+ console.error('Error loading cloudsync extension:', error);
50
+ }
51
+
52
+ console.log('Initializing tables and cloudsync...');
53
+ try {
54
+ await db.execute('CREATE TABLE IF NOT EXISTS tasks (uuid TEXT NOT NULL PRIMARY KEY, title TEXT, isCompleted INT NOT NULL DEFAULT 0);')
55
+ await db.execute('CREATE TABLE IF NOT EXISTS tags (uuid TEXT NOT NULL PRIMARY KEY, name TEXT, UNIQUE(name));')
56
+ await db.execute('CREATE TABLE IF NOT EXISTS tasks_tags (uuid TEXT NOT NULL PRIMARY KEY, task_uuid TEXT, tag_uuid TEXT, FOREIGN KEY (task_uuid) REFERENCES tasks(uuid), FOREIGN KEY (tag_uuid) REFERENCES tags(uuid));')
57
+
58
+ await db.execute(`SELECT cloudsync_init('*');`);
59
+ await db.execute('INSERT OR IGNORE INTO tags (uuid, name) VALUES (?, ?)', [randomUUID(), 'Work'])
60
+ await db.execute('INSERT OR IGNORE INTO tags (uuid, name) VALUES (?, ?)', [randomUUID(), 'Personal'])
61
+
62
+ if (CONNECTION_STRING.startsWith('sqlitecloud://')) {
63
+ await db.execute(`SELECT cloudsync_network_init('${CONNECTION_STRING}');`);
64
+ } else {
65
+ console.warn(CONNECTION_STRING)
66
+ throw new Error('No valid CONNECTION_STRING provided, cloudsync_network_init will not be called');
67
+ }
68
+
69
+ db.execute('SELECT cloudsync_network_sync(100, 10);')
70
+ getCategories()
71
+ setSync(true)
72
+ } catch (error) {
73
+ console.error('Error creating tables', error)
74
+ }
75
+ }
76
+
77
+ useEffect(() => {
78
+ initializeTables()
79
+ }, [])
80
+
81
+ useEffect(() => {
82
+ return registerRefreshCallback(() => {
83
+ getCategories();
84
+ });
85
+ }, [registerRefreshCallback])
86
+
87
+ return {
88
+ moreCategories,
89
+ addCategory,
90
+ getCategories
91
+ }
92
+ }
93
+
94
+ export default useCategories