React Native / Supabase + React Native

Supabase + React Native

Өгөгдлийн сан, нэвтрэлт, файл хадгалалт — backend шаардагдах бүх зүйлийг Supabase үнэгүй хангадаг. React Native апп-д Supabase холбохоос эхлээд өгөгдөл унших, бичих хүртэл энэ хичээлд бүгдийг сурна. PostgreSQL мэддэг хэрэггүй, JavaScript мэдлэгтэй л хангалттай.

Суулгах ба тохиргоо

bash
npx expo install @supabase/supabase-js @supabase/ssr
npx expo install expo-secure-store

expo-secure-store нь Supabase session-г аюулгүй хадгалахад хэрэгтэй — AsyncStorage-с найдвартай.

lib/supabase.ts файл үүсгэнэ:

typescript
import { createClient } from "@supabase/supabase-js";
import * as SecureStore from "expo-secure-store";
import { Platform } from "react-native";

// SecureStore нь вэб дээр ажиллахгүй тул platform шалгана
const ExpoSecureStoreAdapter = {
  getItem: (key: string) => {
    if (Platform.OS === "web") return localStorage.getItem(key);
    return SecureStore.getItemAsync(key);
  },
  setItem: (key: string, value: string) => {
    if (Platform.OS === "web") return localStorage.setItem(key, value);
    return SecureStore.setItemAsync(key, value);
  },
  removeItem: (key: string) => {
    if (Platform.OS === "web") return localStorage.removeItem(key);
    return SecureStore.deleteItemAsync(key);
  },
};

const SUPABASE_URL = process.env.EXPO_PUBLIC_SUPABASE_URL!;
const SUPABASE_ANON_KEY = process.env.EXPO_PUBLIC_SUPABASE_ANON_KEY!;

export const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY, {
  auth: {
    storage: ExpoSecureStoreAdapter,
    autoRefreshToken: true,
    persistSession: true,
    detectSessionInUrl: false, // React Native-д false байх ёстой
  },
});

.env файлд Supabase холболтын мэдээлэл нэмнэ:

код
EXPO_PUBLIC_SUPABASE_URL=https://xxxxxxxx.supabase.co
EXPO_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

EXPO_PUBLIC_ угтвартай орчны хувьсагч нь client талд харагдана. Нууц мэдээллийг хэзээ ч EXPO_PUBLIC_ угтвартайгаар нэрлэж болохгүй.

Өгөгдөл унших ба харуулах

Supabase холбоосон бол .from().select() дарааллаар өгөгдлийн сангаас мэдээлэл татна:

jsx
import { useState, useEffect } from "react";
import { supabase } from "../lib/supabase";
import {
  View,
  Text,
  FlatList,
  ActivityIndicator,
  StyleSheet,
} from "react-native";

export default function PostsList() {
  const [posts, setPosts] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    async function fetchPosts() {
      const { data, error } = await supabase
        .from("posts")
        .select("id, title, body, created_at")
        .order("created_at", { ascending: false })
        .limit(20);

      if (error) {
        setError(error.message);
      } else {
        setPosts(data);
      }
      setLoading(false);
    }

    fetchPosts();
  }, []);

  if (loading) {
    return (
      <View style={styles.center}>
        <ActivityIndicator size="large" color="#22d3ee" />
      </View>
    );
  }

  if (error) {
    return (
      <View style={styles.center}>
        <Text style={styles.errorText}>⚠️ {error}</Text>
      </View>
    );
  }

  return (
    <FlatList
      data={posts}
      keyExtractor={(item) => item.id.toString()}
      renderItem={({ item }) => (
        <View style={styles.card}>
          <Text style={styles.title}>{item.title}</Text>
          <Text style={styles.body} numberOfLines={3}>
            {item.body}
          </Text>
          <Text style={styles.date}>
            {new Date(item.created_at).toLocaleDateString("mn-MN")}
          </Text>
        </View>
      )}
      contentContainerStyle={styles.list}
    />
  );
}

const styles = StyleSheet.create({
  center: {
    flex: 1,
    justifyContent: "center",
    alignItems: "center",
    backgroundColor: "#0b1120",
  },
  list: {
    padding: 16,
    backgroundColor: "#0b1120",
  },
  card: {
    backgroundColor: "#0f172a",
    borderRadius: 12,
    padding: 16,
    marginBottom: 12,
    borderWidth: 1,
    borderColor: "#1e293b",
    gap: 6,
  },
  title: {
    color: "#f1f5f9",
    fontSize: 16,
    fontWeight: "600",
  },
  body: {
    color: "#94a3b8",
    fontSize: 13,
    lineHeight: 18,
  },
  date: {
    color: "#475569",
    fontSize: 11,
    marginTop: 4,
  },
  errorText: {
    color: "#fb7185",
    fontSize: 15,
    textAlign: "center",
    padding: 24,
  },
});

Өгөгдөл бичих ба устгах

insert, update, delete — CRUD үйлдлүүд адилхан хялбар:

jsx
import { useState } from "react";
import { supabase } from "../lib/supabase";
import {
  View,
  Text,
  TextInput,
  TouchableOpacity,
  Alert,
  StyleSheet,
} from "react-native";

export default function NewPost() {
  const [title, setTitle] = useState("");
  const [body, setBody] = useState("");
  const [saving, setSaving] = useState(false);

  const createPost = async () => {
    if (!title.trim() || !body.trim()) {
      Alert.alert("Анхааруулга", "Гарчиг болон агуулгыг оруулна уу");
      return;
    }

    setSaving(true);
    const { error } = await supabase
      .from("posts")
      .insert({ title: title.trim(), body: body.trim() });

    if (error) {
      Alert.alert("Алдаа", error.message);
    } else {
      Alert.alert("Амжилттай", "Нийтлэл хадгалагдлаа");
      setTitle("");
      setBody("");
    }
    setSaving(false);
  };

  const deletePost = async (id) => {
    const { error } = await supabase.from("posts").delete().eq("id", id); // WHERE id = ?

    if (error) {
      Alert.alert("Алдаа", error.message);
    }
  };

  const updatePost = async (id, newTitle) => {
    const { error } = await supabase
      .from("posts")
      .update({ title: newTitle })
      .eq("id", id);

    if (error) {
      Alert.alert("Алдаа", error.message);
    }
  };

  return (
    <View style={styles.container}>
      <Text style={styles.heading}>Шинэ нийтлэл</Text>

      <TextInput
        style={styles.input}
        value={title}
        onChangeText={setTitle}
        placeholder="Гарчиг"
        placeholderTextColor="#475569"
      />
      <TextInput
        style={[styles.input, styles.textarea]}
        value={body}
        onChangeText={setBody}
        placeholder="Агуулга..."
        placeholderTextColor="#475569"
        multiline
        numberOfLines={5}
        textAlignVertical="top"
      />

      <TouchableOpacity
        style={[styles.btn, saving && styles.btnDisabled]}
        onPress={createPost}
        disabled={saving}
      >
        <Text style={styles.btnText}>
          {saving ? "Хадгалж байна..." : "Нийтлэх"}
        </Text>
      </TouchableOpacity>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    padding: 24,
    backgroundColor: "#0b1120",
  },
  heading: {
    fontSize: 22,
    fontWeight: "bold",
    color: "#f1f5f9",
    marginBottom: 20,
  },
  input: {
    backgroundColor: "#0f172a",
    borderWidth: 1,
    borderColor: "#1e293b",
    borderRadius: 10,
    padding: 14,
    color: "#f1f5f9",
    fontSize: 15,
    marginBottom: 12,
  },
  textarea: {
    height: 120,
  },
  btn: {
    backgroundColor: "#22d3ee",
    borderRadius: 10,
    padding: 16,
    alignItems: "center",
    marginTop: 4,
  },
  btnDisabled: { opacity: 0.5 },
  btnText: {
    color: "#0b1120",
    fontWeight: "700",
    fontSize: 16,
  },
});

Supabase-г React Native-тай холбосноор дутуу байсан backend бүхэлдээ бэлэн болно. Мэдээллийн сан, файл хадгалалт, нэвтрэлт — бүгдийг нэг package-р, үнэгүй, JavaScript-аар л удирдана. Дараагийн хичээлд authentication нэмэх замаар апп-г бүрэн болгоно.

Дараагийн хичээлд:

Supabase Auth React Native-д — бүртгэл, нэвтрэлт, session удирдалт, хамгаалагдсан дэлгэц хэрхэн хийх талаар сурна.