diff --git a/README.md b/README.md index 5c454cc..92b67f6 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -Welcome to your new TanStack app! +Welcome to your new TanStack app! # Getting Started @@ -6,7 +6,7 @@ To run this application: ```bash npm install -npm run start +npm run start ``` # Building For Production @@ -29,10 +29,8 @@ npm run test This project uses [Tailwind CSS](https://tailwindcss.com/) for styling. - - - ## Routing + This project uses [TanStack Router](https://tanstack.com/router). The initial setup is a file based router. Which means that the routes are managed as files in `src/routes`. ### Adding A Route @@ -68,8 +66,8 @@ In the File Based Routing setup the layout is located in `src/routes/__root.tsx` Here is an example layout that includes a header: ```tsx -import { Outlet, createRootRoute } from '@tanstack/react-router' -import { TanStackRouterDevtools } from '@tanstack/react-router-devtools' +import { Outlet, createRootRoute } from "@tanstack/react-router"; +import { TanStackRouterDevtools } from "@tanstack/react-router-devtools"; import { Link } from "@tanstack/react-router"; @@ -86,127 +84,18 @@ export const Route = createRootRoute({ ), -}) +}); ``` The `` component is not required so you can remove it if you don't want it in your layout. More information on layouts can be found in the [Layouts documentation](https://tanstack.com/router/latest/docs/framework/react/guide/routing-concepts#layouts). - ## Data Fetching -There are multiple ways to fetch data in your application. You can use TanStack Query to fetch data from a server. But you can also use the `loader` functionality built into TanStack Router to load the data for a route before it's rendered. +### Pocketbase -For example: - -```tsx -const peopleRoute = createRoute({ - getParentRoute: () => rootRoute, - path: "/people", - loader: async () => { - const response = await fetch("https://swapi.dev/api/people"); - return response.json() as Promise<{ - results: { - name: string; - }[]; - }>; - }, - component: () => { - const data = peopleRoute.useLoaderData(); - return ( - - ); - }, -}); -``` - -Loaders simplify your data fetching logic dramatically. Check out more information in the [Loader documentation](https://tanstack.com/router/latest/docs/framework/react/guide/data-loading#loader-parameters). - -### React-Query - -React-Query is an excellent addition or alternative to route loading and integrating it into you application is a breeze. - -First add your dependencies: - -```bash -npm install @tanstack/react-query @tanstack/react-query-devtools -``` - -Next we'll need to create a query client and provider. We recommend putting those in `main.tsx`. - -```tsx -import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; - -// ... - -const queryClient = new QueryClient(); - -// ... - -if (!rootElement.innerHTML) { - const root = ReactDOM.createRoot(rootElement); - - root.render( - - - - ); -} -``` - -You can also add TanStack Query Devtools to the root route (optional). - -```tsx -import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; - -const rootRoute = createRootRoute({ - component: () => ( - <> - - - - - ), -}); -``` - -Now you can use `useQuery` to fetch your data. - -```tsx -import { useQuery } from "@tanstack/react-query"; - -import "./App.css"; - -function App() { - const { data } = useQuery({ - queryKey: ["people"], - queryFn: () => - fetch("https://swapi.dev/api/people") - .then((res) => res.json()) - .then((data) => data.results as { name: string }[]), - initialData: [], - }); - - return ( -
-
    - {data.map((person) => ( -
  • {person.name}
  • - ))} -
-
- ); -} - -export default App; -``` - -You can find out everything you need to know on how to use React-Query in the [React-Query documentation](https://tanstack.com/query/latest/docs/framework/react/overview). +TODO ## State Management diff --git a/package-lock.json b/package-lock.json index 8c1b360..3a15b57 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,6 @@ "@atlaskit/pragmatic-drag-and-drop": "^1.7.4", "@headlessui/react": "^2.2.4", "@tailwindcss/vite": "^4.0.6", - "@tanstack/react-query": "^5.79.0", "@tanstack/react-query-devtools": "^5.79.0", "@tanstack/react-router": "^1.114.3", "@tanstack/react-router-devtools": "^1.114.3", @@ -1722,6 +1721,7 @@ "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.79.0.tgz", "integrity": "sha512-s+epTqqLM0/TbJzMAK7OEhZIzh63P9sWz5HEFc5XHL4FvKQXQkcjI8F3nee+H/xVVn7mrP610nVXwOytTSYd0w==", "license": "MIT", + "peer": true, "funding": { "type": "github", "url": "https://github.com/sponsors/tannerlinsley" @@ -1742,6 +1742,7 @@ "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.79.0.tgz", "integrity": "sha512-DjC4JIYZnYzxaTzbg3osOU63VNLP67dOrWet2cZvXgmgwAXNxfS52AMq86M5++ILuzW+BqTUEVMTjhrZ7/XBuA==", "license": "MIT", + "peer": true, "dependencies": { "@tanstack/query-core": "5.79.0" }, diff --git a/package.json b/package.json index ca706ce..57e6a4a 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,6 @@ "@atlaskit/pragmatic-drag-and-drop": "^1.7.4", "@headlessui/react": "^2.2.4", "@tailwindcss/vite": "^4.0.6", - "@tanstack/react-query": "^5.79.0", "@tanstack/react-query-devtools": "^5.79.0", "@tanstack/react-router": "^1.114.3", "@tanstack/react-router-devtools": "^1.114.3", diff --git a/src/context/auth/AuthContext.tsx b/src/context/auth/AuthContext.tsx index e903d97..811fcb4 100644 --- a/src/context/auth/AuthContext.tsx +++ b/src/context/auth/AuthContext.tsx @@ -1,5 +1,4 @@ -import { createContext, useContext, useCallback } from "react"; -import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; +import { createContext, useContext, useCallback, useState } from "react"; import type { ReactNode } from "react"; import { pb } from "@/lib/pocketbase"; import type { AuthRecord } from "pocketbase"; @@ -26,91 +25,47 @@ export interface AuthContextValue { const AuthContext = createContext(undefined); /** - * Fetches the currently authenticated user from PocketBase. - */ -async function fetchUser(): Promise { - if (pb.authStore.isValid) { - return pb.authStore.record; - } - return null; -} - -/** - * Provider for authentication context, using TanStack Query for state management. + * Provider for authentication context. */ export function AuthProvider({ children }: { children: ReactNode }) { - const queryClient = useQueryClient(); - const { data: user, isLoading } = useQuery({ - queryKey: ["auth", "user"], - queryFn: fetchUser, - }); + const [isLoading, setIsLoading] = useState(false); + const [user, setUser] = useState(pb.authStore.record); + const navigate = useNavigate(); - const loginMutation = useMutation({ - mutationFn: async ({ - email, - password, - }: { - email: string; - password: string; - }) => { - await pb.collection("users").authWithPassword(email, password); - return fetchUser(); - }, - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ["auth", "user"] }); - }, - }); + function updateUser() { + if (pb.authStore.isValid) { + setUser(pb.authStore.record); + } + setIsLoading(false); + } - const signupMutation = useMutation({ - mutationFn: async ({ - email, - password, - passwordConfirm, - }: { - email: string; - password: string; - passwordConfirm: string; - }) => { - await pb.collection("users").create({ email, password, passwordConfirm }); - await pb.collection("users").authWithPassword(email, password); - return fetchUser(); - }, - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ["auth", "user"] }); - }, - }); - - const logoutMutation = useMutation({ - mutationFn: async () => { - pb.authStore.clear(); - return null; - }, - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ["auth", "user"] }); - }, - }); - - const login = useCallback( - async (email: string, password: string) => { - await loginMutation.mutateAsync({ email, password }); - navigate({ to: "/campaigns" }); - }, - [loginMutation], - ); + const login = useCallback(async (email: string, password: string) => { + console.log("login"); + setIsLoading(true); + await pb.collection("users").authWithPassword(email, password); + updateUser(); + navigate({ to: "/campaigns" }); + }, []); const signup = useCallback( async (email: string, password: string, passwordConfirm: string) => { - await signupMutation.mutateAsync({ email, password, passwordConfirm }); + console.log("signup"); + setIsLoading(true); + await pb.collection("users").create({ email, password, passwordConfirm }); + await pb.collection("users").authWithPassword(email, password); + updateUser(); navigate({ to: "/campaigns" }); }, - [signupMutation], + [], ); const logout = useCallback(async () => { - await logoutMutation.mutateAsync(); + console.log("logout"); + pb.authStore.clear(); + setUser(null); navigate({ to: "/" }); - }, [logoutMutation]); + }, []); return ( - - - + , ); } diff --git a/src/routes/__root.tsx b/src/routes/__root.tsx index 5815606..a79bd34 100644 --- a/src/routes/__root.tsx +++ b/src/routes/__root.tsx @@ -1,6 +1,5 @@ import { AuthProvider } from "@/context/auth/AuthContext"; import { DocumentProvider } from "@/context/document/DocumentContext"; -import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; import { Outlet, createRootRoute } from "@tanstack/react-router"; import { TanStackRouterDevtools } from "@tanstack/react-router-devtools"; @@ -13,7 +12,6 @@ export const Route = createRootRoute({ - ), }); diff --git a/src/routes/_app/_authenticated/campaigns.$campaignId.tsx b/src/routes/_app/_authenticated/campaigns.$campaignId.tsx index 5517c0e..1704303 100644 --- a/src/routes/_app/_authenticated/campaigns.$campaignId.tsx +++ b/src/routes/_app/_authenticated/campaigns.$campaignId.tsx @@ -1,11 +1,10 @@ -import { useCallback } from "react"; +import { useCallback, useEffect, useState } from "react"; import { createFileRoute, Link } from "@tanstack/react-router"; import { pb } from "@/lib/pocketbase"; import { SessionRow } from "@/components/documents/session/SessionRow"; import { Button } from "@headlessui/react"; -import { useQueryClient, useSuspenseQuery } from "@tanstack/react-query"; import { Loader } from "@/components/Loader"; -import type { Relationship } from "@/lib/types"; +import type { Campaign, Relationship, Session } from "@/lib/types"; export const Route = createFileRoute( "/_app/_authenticated/campaigns/$campaignId", @@ -15,14 +14,15 @@ export const Route = createFileRoute( }); function RouteComponent() { - const queryClient = useQueryClient(); const params = Route.useParams(); - const { - data: { campaign, sessions }, - } = useSuspenseQuery({ - queryKey: ["campaign"], - queryFn: async () => { + const [loading, setLoading] = useState(true); + const [campaign, setCampaign] = useState(null); + const [sessions, setSessions] = useState([]); + + useEffect(() => { + async function fetchData() { + setLoading(true); const campaign = await pb .collection("campaigns") .getOne(params.campaignId); @@ -31,14 +31,17 @@ function RouteComponent() { filter: `campaign = "${params.campaignId}" && type = 'session'`, sort: "-created", }); - return { - campaign, - sessions, - }; - }, - }); + setSessions(sessions as Session[]); + setCampaign(campaign as Campaign); + setLoading(false); + } + fetchData(); + }, [setCampaign, setSessions, setLoading]); const createNewSession = useCallback(async () => { + if (campaign === null) { + return; + } // Check for a previous session const prevSession = await pb .collection("documents") @@ -70,10 +73,12 @@ function RouteComponent() { }); } } - - queryClient.invalidateQueries({ queryKey: ["campaign"] }); }, [campaign]); + if (loading || campaign === null) { + return ; + } + return (
diff --git a/src/routes/_app_._authenticated.document_.$documentId.print.tsx b/src/routes/_app_._authenticated.document_.$documentId.print.tsx index 091d946..9f2dc96 100644 --- a/src/routes/_app_._authenticated.document_.$documentId.print.tsx +++ b/src/routes/_app_._authenticated.document_.$documentId.print.tsx @@ -1,9 +1,8 @@ import { DocumentPrintRow } from "@/components/documents/DocumentPrintRow"; import { SessionPrintRow } from "@/components/documents/session/SessionPrintRow"; import { Loader } from "@/components/Loader"; -import { pb } from "@/lib/pocketbase"; -import { RelationshipType, type Relationship, type Session } from "@/lib/types"; -import { useSuspenseQuery } from "@tanstack/react-query"; +import { useDocument, useDocumentCache } from "@/context/document/hooks"; +import { RelationshipType, type DocumentId, type Session } from "@/lib/types"; import { createFileRoute } from "@tanstack/react-router"; import _ from "lodash"; @@ -16,32 +15,28 @@ export const Route = createFileRoute( function RouteComponent() { const params = Route.useParams(); - const { - data: { session, relationships }, - } = useSuspenseQuery({ - queryKey: ["session", "relationships"], - queryFn: async () => { - const session = await pb - .collection("documents") - .getOne(params.documentId); - const relationships: Relationship[] = await pb - .collection("relationships") - .getFullList({ - filter: `primary = "${params.documentId}"`, - expand: "secondary", - }); - console.log("Fetched data: ", relationships); - return { - session: session as Session, - relationships: _.mapValues( - _.groupBy(relationships, (r) => r.type), - (rs: Relationship[]) => rs.flatMap((r) => r.expand?.secondary), - ), - }; - }, - }); - console.log("Parsed data: ", relationships); + const { cache } = useDocumentCache(); + const { docResult } = useDocument(params.documentId as DocumentId); + + if (docResult.type !== "ready") { + return ; + } + + const session = docResult.value.doc as Session; + const relationships = _.mapValues( + docResult.value.relationships, + (relResult) => { + if (relResult.type != "ready") { + return []; + } + return relResult.value.secondary + .map((id) => cache.documents[id]) + .flatMap((docResult) => + docResult.type === "ready" ? [docResult.value.doc] : [], + ); + }, + ); return (