Pocketbase with react and react-query
Dennis kinuthia
Posted on September 18, 2022
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.
in powershell it would look something like this
.\pocketbase.exe serve
to serve it on your LAN
on windows run
ipconfig
and something lke this in linux
this doesn't work on mac or wsl, but there a lot of other ways
hostname -I
.\pocketbase.exe serve --http="192.168.0.101:8090"
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/_/
admin dashboard
this will be where you do everything from creating and managing new collections , users , viewing logs , changige settings ...
next we deal with the front-edn by using the provided javascript sdk
npm install pocketbase
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",
});
}
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);
and map over the data array inside the query
peepQuery.data?.map((item)=>{
return <TheRows list={peepsQuery.data} />
})
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);
},
}
);
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" })
};
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);
});
};
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
});
}
tip when using react-query
QueryClient()
is that you should not do it like this
const queryClient = new QueryClient()
since this will create a new instance on every component render
instead use the provided hook
const queryClient = usQueryClient()
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
tips , when setting up the service account you'll need
allowed javascript origin
and aredirectUrl
you can usehttp://localhost:3000
andhttp://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>
);
}
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>
);
}
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
*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*
Posted on September 18, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.