Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ ui-debug.log
# npm pack artifacts
reactfire-*.tgz
reactfire.tgz
# ...except the demo's pinned build, which is its reactfire dependency.
# Regenerate with `npx tsc && npx vite build && npm pack` at the commit in the filename.
!recipe-demo/reactfire-*.tgz
package/
publish.sh
unpack.sh
Expand Down
49 changes: 48 additions & 1 deletion recipe-demo/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion recipe-demo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
"firebase": "^12.17.1",
"next": "^16.3.1",
"react": "^19.2.8",
"react-dom": "^19.2.8"
"react-dom": "^19.2.8",
"reactfire": "file:reactfire-4.2.6-ac3ccf9.tgz"
},
"devDependencies": {
"@types/node": "^24.3.0",
Expand Down
Binary file added recipe-demo/reactfire-4.2.6-ac3ccf9.tgz
Binary file not shown.
6 changes: 3 additions & 3 deletions recipe-demo/src/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { Metadata } from 'next';
import Link from 'next/link';
import '@picocss/pico/css/pico.min.css';
import { SessionProvider } from '@/lib/session-context';
import { Providers } from '@/lib/providers';
import { SessionNav } from '@/components/SessionNav';

export const metadata: Metadata = {
Expand All @@ -13,7 +13,7 @@ export default function RootLayout({ children }: { children: React.ReactNode })
return (
<html lang="en">
<body>
<SessionProvider>
<Providers>
<header className="container">
<nav>
<ul>
Expand All @@ -32,7 +32,7 @@ export default function RootLayout({ children }: { children: React.ReactNode })
</nav>
</header>
<main className="container">{children}</main>
</SessionProvider>
</Providers>
</body>
</html>
);
Expand Down
15 changes: 8 additions & 7 deletions recipe-demo/src/components/RecipeBrowser.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
'use client';

import { useState } from 'react';
import { useFirestoreCollectionData } from 'reactfire';
import { RecipeList } from './RecipeList';
import { useRecipes } from '@/lib/use-recipes';
import { recipeQuery } from '@/lib/recipes';
import { CUISINES, type Cuisine, type Recipe } from '@/lib/types';

export function RecipeBrowser({ initialRecipes }: { initialRecipes: Recipe[] }) {
const [cuisine, setCuisine] = useState<Cuisine | 'all'>('all');
const { recipes, status, error } = useRecipes(cuisine, initialRecipes);
const { data, status } = useFirestoreCollectionData(recipeQuery(cuisine), {
idField: 'id',
initialData: initialRecipes,
});
const recipes = data as Recipe[];

return (
<>
Expand All @@ -23,11 +28,7 @@ export function RecipeBrowser({ initialRecipes }: { initialRecipes: Recipe[] })
</select>
</label>

{error ? (
<article aria-invalid="true">Could not load recipes: {error.message}</article>
) : (
<RecipeList recipes={recipes} loading={status === 'loading'} />
)}
<RecipeList recipes={recipes} loading={status === 'loading'} />
</>
);
}
4 changes: 2 additions & 2 deletions recipe-demo/src/components/RecipeCard.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
'use client';

import { useState } from 'react';
import { useUser } from 'reactfire';
import { toggleLike } from '@/lib/recipes';
import { useSession } from '@/lib/session-context';
import type { Recipe } from '@/lib/types';

export function RecipeCard({ recipe }: { recipe: Recipe }) {
const { user } = useSession();
const { data: user } = useUser();
const [pending, setPending] = useState(false);
const liked = user ? recipe.likedBy.includes(user.uid) : false;

Expand Down
15 changes: 8 additions & 7 deletions recipe-demo/src/components/RequireAuth.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,27 @@

import { usePathname, useRouter } from 'next/navigation';
import { useEffect, type ReactNode } from 'react';
import { useSession } from '@/lib/session-context';
import { useSigninCheck } from 'reactfire';

export function RequireAuth({ children }: { children: ReactNode }) {
const { user, status } = useSession();
const { status, data: signinResult } = useSigninCheck();
const signedIn = signinResult?.signedIn ?? false;
const router = useRouter();
const pathname = usePathname();

useEffect(() => {
// Waiting for 'ready' is what stops a signed-in user being bounced to
// /signin on every hard reload, before onAuthStateChanged has fired.
if (status === 'ready' && !user) {
// Waiting for 'success' is what stops a signed-in user being bounced to
// /signin on every hard reload, before the auth state has resolved.
if (status === 'success' && !signedIn) {
router.replace(`/signin?next=${encodeURIComponent(pathname)}`);
}
}, [status, user, router, pathname]);
}, [status, signedIn, router, pathname]);

if (status === 'loading') {
return <article aria-busy="true">Checking your session</article>;
}

if (!user) {
if (!signedIn) {
return null;
}

Expand Down
4 changes: 2 additions & 2 deletions recipe-demo/src/components/SessionNav.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
'use client';

import Link from 'next/link';
import { useUser } from 'reactfire';
import { logOut } from '@/lib/session';
import { useSession } from '@/lib/session-context';

export function SessionNav() {
const { user, status } = useSession();
const { data: user, status } = useUser();

if (status === 'loading') {
return (
Expand Down
16 changes: 16 additions & 0 deletions recipe-demo/src/lib/providers.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
'use client';

import { type ReactNode } from 'react';
import { AuthProvider, FirebaseAppProvider, FirestoreProvider } from 'reactfire';
import { app, firestore } from './firebase';
import { auth } from './session';

export function Providers({ children }: { children: ReactNode }) {
return (
<FirebaseAppProvider firebaseApp={app}>
<FirestoreProvider sdk={firestore}>
<AuthProvider sdk={auth}>{children}</AuthProvider>
</FirestoreProvider>
</FirebaseAppProvider>
);
}
18 changes: 0 additions & 18 deletions recipe-demo/src/lib/session-context.tsx

This file was deleted.

11 changes: 1 addition & 10 deletions recipe-demo/src/lib/session.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use client';

import { getAuth, connectAuthEmulator, onAuthStateChanged, signInWithEmailAndPassword, signOut, type User } from 'firebase/auth';
import { getAuth, connectAuthEmulator, signInWithEmailAndPassword, signOut } from 'firebase/auth';
import { app, useEmulators } from './firebase';

export const auth = getAuth(app);
Expand All @@ -12,15 +12,6 @@ if (useEmulators && !(EMULATOR_SENTINEL in globalThis)) {
connectAuthEmulator(auth, 'http://127.0.0.1:9099', { disableWarnings: true });
}

export interface Session {
user: User | null;
status: 'loading' | 'ready';
}

export function subscribeToSession(onChange: (session: Session) => void) {
return onAuthStateChanged(auth, (user) => onChange({ user, status: 'ready' }));
}

export function signIn(email: string, password: string) {
return signInWithEmailAndPassword(auth, email, password);
}
Expand Down
44 changes: 0 additions & 44 deletions recipe-demo/src/lib/use-recipes.ts

This file was deleted.

Loading