DEV Community

Cover image for Pocketbase with react and react-query
Dennis kinuthia
Dennis kinuthia

Posted on • Edited on

Pocketbase with react and react-query

pocket base

Open Source backend
for your next SaaS and Mobile app
in 1 file

setup docs

Download for Linux (11MB zip)

Download for Windows (11MB zip)

Download for macOS x64 (11MB zip)

Download for macOS ARM64 (11MB zip)

download a the zipped foledr, exctract it's contents and you'll have a binary execute it in the command line

./pocketbase serve.
Enter fullscreen mode Exit fullscreen mode

in powershell it would look something like this

  .\pocketbase.exe serve
Enter fullscreen mode Exit fullscreen mode

to serve it on your LAN
on windows run

ipconfig
Enter fullscreen mode Exit fullscreen mode

and something lke this in linux

this doesn't work on mac or wsl, but there a lot of other ways

hostname -I
Enter fullscreen mode Exit fullscreen mode
.\pocketbase.exe serve --http="192.168.0.101:8090"
Enter fullscreen mode Exit fullscreen mode

once it's up and running ctrl + click on one of the urls in the terminal

Server started at: http://127.0.0.1:8090
  - REST API: http://127.0.0.1:8090/api/
  - Admin UI: http://127.0.0.1:8090/_/
Enter fullscreen mode Exit fullscreen mode

admin dashboard

this will be where you do everything from creating and managing new collections , users , viewing logs , changige settings ...

admin dashborad

next we deal with the front-edn by using the provided javascript sdk

npm install pocketbase
Enter fullscreen mode Exit fullscreen mode

then create a config.ts file (optional, you can put all the logic in a component)

import PocketBase, { Record } from 'pocketbase';
import { QueryClient } from "react-query";


export interface PeepResponse {
  id: string;
  created: string;
  updated: string;
  "@collectionId": string;
  "@collectionName": string;
  age: number;
  bio: string;
  name: string;
  "@expand": {};
}

// export const client = new PocketBase("http://192.168.43.238:8090");
export const client = new PocketBase("http://127.0.0.1:8090");
export const realTime = async (
  index: [string],
  queryClient: QueryClient,
 ) => {
  return await client.realtime.subscribe("peeps", function (e) {
    console.log("real time peeps", e.record);
 });
};

export const allPeeps=async():Promise<PeepResponse[]|Record[]>=>{
 return await client.records.getFullList("peeps", 200 /* batch size */, {
   sort: "-created",
 });
}

Enter fullscreen mode Exit fullscreen mode

in the above example i created a peeps collection and have a function allPeeps that fetches all records from it , the client sdk also has a paginated variaint

query

we can the use this in out cpmponent with react-query

const peepsQuery = useQuery(["peeps"], allPeeps);
Enter fullscreen mode Exit fullscreen mode

and map over the data array inside the query

 peepQuery.data?.map((item)=>{
  return <TheRows list={peepsQuery.data} />
 })
Enter fullscreen mode Exit fullscreen mode

mutation

we can add a new peep by using the sdk too

 const mutation = useMutation(
({ data, index }: MutationVars) => {
    return client.records.create(index, data);
},
// react-query options , in order to append new peeps by using the data returned after the mutation instaed of having to run the query again to update our list of peeps
// the index will be the react-query index and also the sdk client index
{
//print error if mutation fails
        onError: (err, { data: newData, index }, context) => {
           console.log("error saving the item === ", err)
        },
        //update the list with created record  
        onSuccess: (data, { index }) => {
            console.log("vars === ", data, index)
            queryClient.setQueryData(index, (old: any) => {
                old.unshift(data);
                return old;
            });
            console.log("successfull save of item ", data);
        },

    }
);

Enter fullscreen mode Exit fullscreen mode

Then we'll use that inside a function that'll be passed to our form's onsubmit prop

const createPeep = async (data: any) => {
        mutation.mutate({ data, index: "peeps" })
};
Enter fullscreen mode Exit fullscreen mode

real time listeners

the client sdk has support for real time data from collections
which we'll wrap it with our function

export const realTime = async (
  index: [string],
  queryClient: QueryClient,
 ) => {
  // sdk realtime listener 
  return await client.realtime.subscribe("peeps", function (e) {
    console.log("real time peeps", e.record);
      appendToCache(index,queryClient,e.record);

  });
};
Enter fullscreen mode Exit fullscreen mode

you can use the data directly , but because we already have react-query managing things we might as well append any new changes to the te existing ['peeps'] query cache

export const appendToCache=async(index:[string],queryClient:QueryClient,newData:any)=>{
 queryClient.setQueryData(index, (old:any) => {
  old.unshift(newData)
  return old
 });
}
Enter fullscreen mode Exit fullscreen mode

tip when using react-query QueryClient() is that you should not do it like this

const queryClient = new QueryClient()
Enter fullscreen mode Exit fullscreen mode

since this will create a new instance on every component render
instead use the provided hook

const queryClient = usQueryClient()
Enter fullscreen mode Exit fullscreen mode

which returns the current instance of the QueryClient

if the function is in an external fie pass it in as a prop.
and now calling this inside the app will update the ui automaitcally when any of the data changes

authentication

i skipped straight to Oauth providers since the password one looked pretty straight forward

in this case i wanted the google auth because i had already configured a service account for Google Ouath 2 project

you'll need a client id and client secret Google Ouath 2 project

then you'll enable it in the admin dashboard
Image description

tips , when setting up the service account you'll need
allowed javascript origin and a redirectUrl
you can use http://localhost:3000 and http://localhost:3000/redirect respectively , this is assuming that's where your react app will be running

after that setup the login page

import React from 'react'
import { TheButton } from '../Shared/TheButton';
import { client, providers } from './../../pocket/config';


interface LoginProps {

}

export const Login: React.FC<LoginProps> = ({}) => {
let provs = providers.authProviders
    let redirectUrl = 'http://localhost:3000/redirect'

const startLogin = (prov:any)=>{
localStorage.setItem("provider", JSON.stringify(prov));
    const url = provs[0].authUrl + redirectUrl
    if (typeof window !== 'undefined') {
        window.location.href = url;
    }
}




return (
 <div className='w-full h-full flex-col-center'>
        <div className='text-3xl font-bold '>LOGIN</div>
        {
           provs&&provs?.map((item,index)=>{
            return (
                <TheButton
                key={item.name}
                label={item.name}
                border={'1px solid'}
                padding={'2%'}
                textSize={'1.2 rem'}
                onClick={()=>startLogin(item)}
                />
            )
        })
        }
 </div>
);
}

Enter fullscreen mode Exit fullscreen mode

and the redirect page as so

import React from 'react'
import { useSearchParams,useNavigate,Navigate } from 'react-router-dom';
import { client } from './../../pocket/config';
import { useQueryClient } from 'react-query';
import { UserType } from './types';


interface RedirectProps {
user?: UserType | null
}

export const Redirect: React.FC<RedirectProps> = ({ user }) => {
    //@ts-ignore
    const local_prov = JSON.parse(localStorage.getItem('provider'))
    const [searchParams] = useSearchParams();
    const code = searchParams.get('code') as string
    // compare the redirect's state param and the stored provider's one
    const queryClient= useQueryClient()
   let redirectUrl = 'http://localhost:3000/redirect'
    const [loading,setLoading]= React.useState(true)
   if (local_prov.state !== searchParams.get("state")) {
        let url = 'http://localhost:3000/login'
        if (typeof window !== 'undefined') {
            window.location.href = url;
        }
    } else {
        client.users.authViaOAuth2(
            local_prov.name,
            code,
            local_prov.codeVerifier,
            redirectUrl)
            .then((response) => {
              console.log("authentication data === ", response)
             client.records.update('profiles', response.user.profile?.id as string, {
                    name:response.meta.name,
                    avatarUrl:response.meta.avatarUrl
                }).then((res)=>{
                    console.log(" successfully updated profi;e",res)
                    }).catch((e) => {
                    console.log("error updating profile  == ", e)
                })
                setLoading(false)
                console.log("client modal after logg   == ",client.authStore.model)
                queryClient.setQueryData(['user'], client.authStore.model)

            }).catch((e) => {
                console.log("error logging in with provider  == ", e)
            })
    }
    if(user){
        return <Navigate to="/" replace />;
    }

    return (
        <div>
            {
                loading?(
                    <div className='w-full h-full flex-center'>loading .... </div>) : 
                    (
                        <div className='w-full h-full flex-center'>success</div>

                    )}
        </div>
    );
}





Enter fullscreen mode Exit fullscreen mode

this also assumes that you're using react-router-dom v6

import './App.css'
import { useTheme } from './utils/hooks/themeHook'
import { BsSunFill, BsFillMoonFill } from "react-icons/bs";;
import { TheIcon } from './components/Shared/TheIcon';

import { Query, useQuery } from 'react-query';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import { Home } from './components/home/Home';
import { Redirect } from './components/login/Redirect';
import { Login } from './components/login/Login';
import { useEffect, useInsertionEffect } from 'react';
import { client } from './pocket/config';
import { ProtectedRoute } from './components/login/PrivateRoutes';
import { UserType } from './components/login/types';


type AppProps = {
  // queryClient:QueryClient
};
type MutationVars = {
  data: any;
  index: string;
}
export interface FormOptions {
  field_name: string;
  field_type: string;
  default_value: string | number
  options?: { name: string; value: string }[]
}
function App({ }: AppProps) {
  const { colorTheme, setTheme } = useTheme();
 const mode = colorTheme === "light" ? BsSunFill : BsFillMoonFill;
  const toggle = () => {
    setTheme(colorTheme);
  };

  const getUser = async()=>{
    return client.authStore.model
  }




  const userQuery = useQuery(["user"],getUser); 

   console.log("user query ======  ",userQuery)


if(userQuery.isLoading){
  return(
    <div className="w-full min-h-screen text-5xl font-bold flex-col-center">
     LOADING....
    </div>
  )
}
  if (userQuery.isError) {
    return (
      <div className="w-full min-h-screen text-5xl font-bold flex-col-center">
        {/* @ts-ignore */}
        {userQuery?.error?.message}
      </div>
    )
  }
const user = userQuery.data as UserType|null|undefined
  return (
    <div
      className="w-full min-h-screen  flex-col-center scroll-bar
   dark:bg-black dark:text-white "
    >

      <BrowserRouter>
        <div className="fixed top-[0px] w-[100%] z-50">
          <div className="w-fit p-1  flex-center">
            <TheIcon Icon={mode} size={"25"} color={""} iconAction={toggle} />
          </div>
        </div>
        <div className="w-full h-[90%] mt-16  ">
          <Routes>
            <Route
              path="/"
              element={
                <ProtectedRoute user={user}> 
                  <Home user={user} />
                  </ProtectedRoute>
           }
            />
            {/* @ts-ignore */}
            <Route path="/login" element={<Login />} />

            <Route path="/redirect" element={<Redirect  user={user}/>} />

          </Routes>
        </div>
      </BrowserRouter>

    </div>
  );
}

export default App
Enter fullscreen mode Exit fullscreen mode

*and that's it .
i plan to port over an exsting app to pocketbase and write about all the quirky things one might run into

for the complete code github repo

recommend resources

Community curated tools for pocketbase

Fireship next13 pocketbase tutorial

pardon the messy code i was more focused on getting it to work and putting the word out there hopefully you'll make your way around this awesome tool much easier*

Top comments (2)

Collapse
 
bgw8 profile image
Emil Rydén

Great post!
How would one go about to use pocketbase in production?
Could you just attach it to the repo and then deploy it via e.g vercel?

Collapse
 
tigawanna profile image
Dennis kinuthia

No , pocketbase needs persistent storage. If you had a server you could just copy the executable there and place it under a subdomain and configure something like ngnix to forward all requests to your endpoint to the pocketbase instance.
Something like gcp cloud engine or even better some free solutions recommended by the community awesome pocketbase could work for you.

Also if i have a repo using pocket base with about more refined code GitHub repo
Plus this gem was put out on how to use it with Nextjs 13

Great tool , you might need to learn a thing or two about bare metal hosting or docker containers, it's a worth while investment
Also sorry for the late response