--- title: 'Build a User Management App with Ionic React' description: 'Learn how to use Supabase in your Ionic React App.' --- <$Partial path="quickstart_intro.mdx" /> ![Supabase User Management example](/docs/img/ionic-demos/ionic-angular-account.png) If you get stuck while working through this guide, refer to the [full example on GitHub](https://github.com/mhartington/supabase-ionic-react). <$Partial path="project_setup.mdx" /> ## Building the app Let's start building the React app from scratch. ### Initialize an Ionic React app We can use the [Ionic CLI](https://ionicframework.com/docs/cli) to initialize an app called `supabase-ionic-react`: ```bash npm install -g @ionic/cli ionic start supabase-ionic-react blank --type react cd supabase-ionic-react ``` Then let's install the only additional dependency: [supabase-js](https://github.com/supabase/supabase-js) ```bash npm install @supabase/supabase-js ``` And finally we want to save the environment variables in a `.env`. All we need are the API URL and the `anon` key that you copied [earlier](#get-the-api-keys). <$CodeTabs> ```bash name=.env VITE_SUPABASE_URL=YOUR_SUPABASE_URL VITE_SUPABASE_PUBLISHABLE_KEY=YOUR_SUPABASE_PUBLISHABLE_KEY ``` Now that we have the API credentials in place, let's create a helper file to initialize the Supabase client. These variables will be exposed on the browser, and that's completely fine since we have [Row Level Security](/docs/guides/auth#row-level-security) enabled on our Database. <$CodeTabs> ```js name=src/supabaseClient.ts import { createClient } from '@supabase/supabase-js' const supabaseUrl = import.meta.env.VITE_SUPABASE_URL || '' const supabaseAnonKey = import.meta.env.VITE_SUPABASE_PUBLISHABLE_KEY || '' export const supabase = createClient(supabaseUrl, supabaseAnonKey) ``` ### Set up a login route Let's set up a React component to manage logins and sign ups. We'll use Magic Links, so users can sign in with their email without using passwords. <$CodeTabs> ```jsx name=/src/pages/Login.tsx import { useState } from 'react'; import { IonButton, IonContent, IonHeader, IonInput, IonItem, IonLabel, IonList, IonPage, IonTitle, IonToolbar, useIonToast, useIonLoading, } from '@ionic/react'; import {supabase} from '../supabaseClient' export function LoginPage() { const [email, setEmail] = useState(''); const [showLoading, hideLoading] = useIonLoading(); const [showToast ] = useIonToast(); const handleLogin = async (e: React.FormEvent) => { console.log() e.preventDefault(); await showLoading(); try { await supabase.auth.signInWithOtp({ "email": email }); await showToast({ message: 'Check your email for the login link!' }); } catch (e: any) { await showToast({ message: e.error_description || e.message , duration: 5000}); } finally { await hideLoading(); } }; return ( Login

Supabase + Ionic React

Sign in via magic link with your email below

Email setEmail(e.detail.value ?? '')} type="email" >
Login
); } ``` ### Account page After a user is signed in we can allow them to edit their profile details and manage their account. Let's create a new component for that called `Account.tsx`. <$CodeTabs> ```jsx name=src/pages/Account.tsx import { IonButton, IonContent, IonHeader, IonInput, IonItem, IonLabel, IonPage, IonTitle, IonToolbar, useIonLoading, useIonToast, useIonRouter } from '@ionic/react'; import { useEffect, useState } from 'react'; import { supabase } from '../supabaseClient'; import { Session } from '@supabase/supabase-js'; export function AccountPage() { const [showLoading, hideLoading] = useIonLoading(); const [showToast] = useIonToast(); const [session, setSession] = useState(null) const router = useIonRouter(); const [profile, setProfile] = useState({ username: '', website: '', avatar_url: '', }); useEffect(() => { const getSession = async () => { setSession(await supabase.auth.getSession().then((res) => res.data.session)) } getSession() supabase.auth.onAuthStateChange((_event, session) => { setSession(session) }) }, []) useEffect(() => { getProfile(); }, [session]); const getProfile = async () => { console.log('get'); await showLoading(); try { const user = await supabase.auth.getUser(); const { data, error, status } = await supabase .from('profiles') .select(`username, website, avatar_url`) .eq('id', user!.data.user?.id) .single(); if (error && status !== 406) { throw error; } if (data) { setProfile({ username: data.username, website: data.website, avatar_url: data.avatar_url, }); } } catch (error: any) { showToast({ message: error.message, duration: 5000 }); } finally { await hideLoading(); } }; const signOut = async () => { await supabase.auth.signOut(); router.push('/', 'forward', 'replace'); } const updateProfile = async (e?: any, avatar_url: string = '') => { e?.preventDefault(); console.log('update '); await showLoading(); try { const user = await supabase.auth.getUser(); const updates = { id: user!.data.user?.id, ...profile, avatar_url: avatar_url, updated_at: new Date(), }; const { error } = await supabase.from('profiles').upsert(updates); if (error) { throw error; } } catch (error: any) { showToast({ message: error.message, duration: 5000 }); } finally { await hideLoading(); } }; return ( Account

Email

{session?.user?.email}

Name setProfile({ ...profile, username: e.detail.value ?? '' }) } > Website setProfile({ ...profile, website: e.detail.value ?? '' }) } >
Update Profile
Log Out
); } ``` ### Launch! Now that we have all the components in place, let's update `App.tsx`: <$CodeTabs> ```jsx name=src/App.tsx import { Redirect, Route } from 'react-router-dom' import { IonApp, IonRouterOutlet, setupIonicReact } from '@ionic/react' import { IonReactRouter } from '@ionic/react-router' import { supabase } from './supabaseClient' import '@ionic/react/css/ionic.bundle.css' /* Theme variables */ import './theme/variables.css' import { LoginPage } from './pages/Login' import { AccountPage } from './pages/Account' import { useEffect, useState } from 'react' import { Session } from '@supabase/supabase-js' setupIonicReact() const App: React.FC = () => { const [session, setSession] = useState(null) useEffect(() => { const getSession = async () => { setSession(await supabase.auth.getSession().then((res) => res.data.session)) } getSession() supabase.auth.onAuthStateChange((_event, session) => { setSession(session) }) }, []) return ( { return session ? : }} /> ) } export default App ``` Once that's done, run this in a terminal window: ```bash ionic serve ``` And then open the browser to [localhost:3000](http://localhost:3000) and you should see the completed app. ![Supabase Ionic React](/docs/img/ionic-demos/ionic-react.png) ## Bonus: Profile photos Every Supabase project is configured with [Storage](/docs/guides/storage) for managing large files like photos and videos. ### Create an upload widget First install two packages in order to interact with the user's camera. ```bash npm install @ionic/pwa-elements @capacitor/camera ``` [Capacitor](https://capacitorjs.com) is a cross platform native runtime from Ionic that enables web apps to be deployed through the app store and provides access to native device API. Ionic PWA elements is a companion package that will polyfill certain browser APIs that provide no user interface with custom Ionic UI. With those packages installed we can update our `index.tsx` to include an additional bootstrapping call for the Ionic PWA Elements. <$CodeTabs> ```ts name=src/index.tsx import React from 'react' import ReactDOM from 'react-dom' import App from './App' import * as serviceWorkerRegistration from './serviceWorkerRegistration' import reportWebVitals from './reportWebVitals' import { defineCustomElements } from '@ionic/pwa-elements/loader' defineCustomElements(window) ReactDOM.render( , document.getElementById('root') ) serviceWorkerRegistration.unregister() reportWebVitals() ``` Then create an `AvatarComponent`. <$CodeTabs> ```jsx name=src/components/Avatar.tsx import { IonIcon } from '@ionic/react'; import { person } from 'ionicons/icons'; import { Camera, CameraResultType } from '@capacitor/camera'; import { useEffect, useState } from 'react'; import { supabase } from '../supabaseClient'; import './Avatar.css' export function Avatar({ url, onUpload, }: { url: string; onUpload: (e: any, file: string) => Promise; }) { const [avatarUrl, setAvatarUrl] = useState(); useEffect(() => { if (url) { downloadImage(url); } }, [url]); const uploadAvatar = async () => { try { const photo = await Camera.getPhoto({ resultType: CameraResultType.DataUrl, }); const file = await fetch(photo.dataUrl!) .then((res) => res.blob()) .then( (blob) => new File([blob], 'my-file', { type: `image/${photo.format}` }) ); const fileName = `${Math.random()}-${new Date().getTime()}.${ photo.format }`; const { error: uploadError } = await supabase.storage .from('avatars') .upload(fileName, file); if (uploadError) { throw uploadError; } onUpload(null, fileName); } catch (error) { console.log(error); } }; const downloadImage = async (path: string) => { try { const { data, error } = await supabase.storage .from('avatars') .download(path); if (error) { throw error; } const url = URL.createObjectURL(data!); setAvatarUrl(url); } catch (error: any) { console.log('Error downloading image: ', error.message); } }; return (
{avatarUrl ? ( ) : ( )}
); } ``` ### Add the new widget And then we can add the widget to the Account page: <$CodeTabs> ```jsx name=src/pages/Account.tsx // Import the new component import { Avatar } from '../components/Avatar'; // ... return ( Account ``` At this stage you have a fully functional application!