refactor: organize per-product

now that we understand the api better,
I'm breaking the components and hooks into
per-product folders so that they're easier to read/test/maintain
This commit is contained in:
jhuleatt
2019-07-09 15:20:19 -07:00
parent d5a1543ace
commit e15c0fbcec
18 changed files with 342 additions and 316 deletions

View File

@@ -0,0 +1,70 @@
import { cleanup, render } from '@testing-library/react';
import { auth } from 'firebase/app';
import 'jest-dom/extend-expect';
import * as React from 'react';
import { AuthCheck } from '.';
import { FirebaseAppProvider } from '..';
const mockAuth = jest.fn(() => {
return {
onIdTokenChanged: jest.fn()
};
});
const mockFirebase = {
auth: mockAuth
};
const Provider = ({ children }) => (
<FirebaseAppProvider firebaseApp={mockFirebase}>
{children}
</FirebaseAppProvider>
);
describe('AuthCheck', () => {
afterEach(() => {
cleanup();
jest.clearAllMocks();
});
it('can find firebase Auth from Context', () => {
expect(() =>
render(
<Provider>
<React.Suspense fallback={'loading'}>
<AuthCheck fallback={'loading'}>{'children'}</AuthCheck>
</React.Suspense>
</Provider>
)
).not.toThrow();
});
it('can use firebase Auth from props', () => {
expect(() =>
render(
<React.Suspense fallback={'loading'}>
<AuthCheck
fallback={<h1>not signed in</h1>}
auth={(mockFirebase.auth() as unknown) as auth.Auth}
>
{'signed in'}
</AuthCheck>
</React.Suspense>
)
).not.toThrow();
});
test.todo('renders the fallback if a user is not signed in');
test.todo('renders children if a user is logged in');
test.todo('checks requiredClaims');
});
describe('useUser', () => {
test.todo('can find firebase.auth() from Context');
test.todo('throws an error if firebase.auth() is not available');
test.todo('returns the same value as firebase.auth().currentUser()');
});

View File

@@ -1,16 +1,9 @@
import { auth, User } from 'firebase/app';
import * as React from 'react';
import { auth, performance, User } from 'firebase/app';
import { useUser, useFirebaseApp } from './index';
const { Suspense, useState, useLayoutEffect } = React;
import { user } from 'rxfire/auth';
import { useObservable, useFirebaseApp, ReactFireOptions } from '..';
export interface SuspensePerfProps {
children: React.ReactNode;
traceId: string;
fallback: React.ReactNode;
firePerf?: performance.Performance; // TODO(jeff): Add firePerf here when it's available
}
function getPerfFromContext(): performance.Performance {
function getAuthFromContext(): auth.Auth {
const firebaseApp = useFirebaseApp();
if (!firebaseApp) {
@@ -19,39 +12,34 @@ function getPerfFromContext(): performance.Performance {
);
}
const perfFunc = firebaseApp.performance;
const authFunc = firebaseApp.auth;
if (!perfFunc || !perfFunc()) {
if (!authFunc || !authFunc()) {
throw new Error(
"No perf object off of Firebase. Did you forget to import 'firebase/performance' in a component?"
"No auth object off of Firebase. Did you forget to import 'firebase/auth' in a component?"
);
}
return perfFunc();
return authFunc();
}
export function SuspenseWithPerf({
children,
traceId,
fallback,
firePerf
}: SuspensePerfProps) {
firePerf = firePerf || getPerfFromContext();
const trace = React.useMemo(() => firePerf.trace(traceId), [traceId]);
/**
* Subscribe to Firebase auth state changes, including token refresh
*
* @param auth - the [firebase.auth](https://firebase.google.com/docs/reference/js/firebase.auth) object
* @param options
*/
export function useUser<T = unknown>(
auth?: auth.Auth,
options?: ReactFireOptions<T>
): User | T {
auth = auth || getAuthFromContext();
const Fallback = () => {
useLayoutEffect(() => {
trace.start();
return () => {
trace.stop();
};
}, []);
return <>{fallback}</>;
};
return <Suspense fallback={<Fallback />}>{children}</Suspense>;
return useObservable(
user(auth),
'user',
options ? options.startWithValue : undefined
);
}
export interface AuthCheckProps {
@@ -69,7 +57,7 @@ export function AuthCheck({
}: AuthCheckProps): React.ReactNode {
const user = useUser<User>(auth);
useLayoutEffect(() => {
React.useLayoutEffect(() => {
// TODO(jeff) see if this actually works
if (requiredClaims) {
throw user.getIdTokenResult().then(idTokenResult => {

View File

@@ -0,0 +1,11 @@
import 'jest-dom/extend-expect';
describe('Realtime Database (RTDB)', () => {
describe('useDatabaseObject', () => {
test.todo("returns the same value as ref.on('value')");
});
describe('useDatabaseList', () => {
test.todo("returns the same value as ref.on('value')");
});
});

View File

@@ -0,0 +1,37 @@
import { database } from 'firebase/app';
import { list, object, QueryChange } from 'rxfire/database';
import { ReactFireOptions, useObservable } from '..';
/**
* Subscribe to a Realtime Database object
*
* @param ref - Reference to the DB object you want to listen to
* @param options
*/
export function useDatabaseObject<T = unknown>(
ref: database.Reference,
options?: ReactFireOptions<T>
): QueryChange | T {
return useObservable(
object(ref),
ref.toString(),
options ? options.startWithValue : undefined
);
}
/**
* Subscribe to a Realtime Database list
*
* @param ref - Reference to the DB List you want to listen to
* @param options
*/
export function useDatabaseList<T = { [key: string]: unknown }>(
ref: database.Reference | database.Query,
options?: ReactFireOptions<T[]>
): QueryChange[] | T[] {
return useObservable(
list(ref),
ref.toString(),
options ? options.startWithValue : undefined
);
}

View File

@@ -1,10 +1,10 @@
import { renderHook, act } from '@testing-library/react-hooks';
import { render, cleanup } from '@testing-library/react';
import * as React from 'react';
import 'jest-dom/extend-expect';
import { FirebaseAppProvider } from './index';
import { cleanup, render } from '@testing-library/react';
import { renderHook } from '@testing-library/react-hooks';
import * as firebase from 'firebase/app';
import { useFirebaseApp } from './firebaseContext';
import 'jest-dom/extend-expect';
import * as React from 'react';
import { useFirebaseApp } from '.';
import { FirebaseAppProvider } from './index';
afterEach(cleanup);

View File

@@ -0,0 +1,13 @@
import { renderHook, act } from '@testing-library/react-hooks';
import * as React from 'react';
import 'jest-dom/extend-expect';
describe('Firestore', () => {
describe('useFirestoreDoc', () => {
test.todo('returns the same value as ref.onSnapshot()');
});
describe('useFirestoreCollection', () => {
test.todo('returns the same value as ref.onSnapshot()');
});
});

View File

@@ -0,0 +1,37 @@
import { firestore } from 'firebase/app';
import { doc, fromCollectionRef } from 'rxfire/firestore';
import { ReactFireOptions, useObservable } from '..';
/**
* Suscribe to Firestore Document changes
*
* @param ref - Reference to the document you want to listen to
* @param options
*/
export function useFirestoreDoc<T = unknown>(
ref: firestore.DocumentReference,
options?: ReactFireOptions<T>
): firestore.DocumentSnapshot | T {
return useObservable(
doc(ref),
ref.path,
options ? options.startWithValue : undefined
);
}
/**
* Subscribe to a Firestore collection
*
* @param ref - Reference to the collection you want to listen to
* @param options
*/
export function useFirestoreCollection<T = { [key: string]: unknown }>(
ref: firestore.CollectionReference,
options?: ReactFireOptions<T[]>
): firestore.QuerySnapshot | T[] {
return useObservable(
fromCollectionRef(ref),
ref.path,
options ? options.startWithValue : undefined
);
}

View File

@@ -1,41 +0,0 @@
import { renderHook, act } from '@testing-library/react-hooks';
import * as React from 'react';
import 'jest-dom/extend-expect';
describe('useUser', () => {
test.todo('can find firebase.auth() from Context');
test.todo('throws an error if firebase.auth() is not available');
test.todo('returns the same value as firebase.auth().currentUser()');
});
describe('Firestore', () => {
describe('useFirestoreDoc', () => {
test.todo('returns the same value as ref.onSnapshot()');
});
describe('useFirestoreCollection', () => {
test.todo('returns the same value as ref.onSnapshot()');
});
});
describe('Realtime Database (RTDB)', () => {
describe('useDatabaseObject', () => {
test.todo("returns the same value as ref.on('value')");
});
describe('useDatabaseList', () => {
test.todo("returns the same value as ref.on('value')");
});
});
describe('Storage', () => {
describe('useStorageTask', () => {
test.todo('returns the same value as uploadTask');
});
describe('useStorageDownloadURL', () => {
test.todo('returns the same value as getDownloadURL');
});
});

View File

@@ -1,179 +1,11 @@
import { auth, firestore, User, database, storage } from 'firebase/app';
import { user } from 'rxfire/auth';
import { fromCollectionRef, doc } from 'rxfire/firestore';
import { object, list, QueryChange } from 'rxfire/database';
import { useObservable } from './util/use-observable';
import { getDownloadURL } from 'rxfire/storage';
import { Observable, from } from 'rxjs';
import { useFirebaseApp } from './firebaseContext';
export interface ReactFireOptions<T = unknown> {
startWithValue: T;
}
function getAuthFromContext(): auth.Auth {
const firebaseApp = useFirebaseApp();
if (!firebaseApp) {
throw new Error(
'Firebase not found in context. Either pass it directly to a reactfire hook, or wrap your component in a FirebaseAppProvider'
);
}
const authFunc = firebaseApp.auth;
if (!authFunc || !authFunc()) {
throw new Error(
"No auth object off of Firebase. Did you forget to import 'firebase/auth' in a component?"
);
}
return authFunc();
}
/**
* Subscribe to Firebase auth state changes, including token refresh
*
* @param auth - the [firebase.auth](https://firebase.google.com/docs/reference/js/firebase.auth) object
* @param options
*/
export function useUser<T = unknown>(
auth?: auth.Auth,
options?: ReactFireOptions<T>
): User | T {
auth = auth || getAuthFromContext();
return useObservable(
user(auth),
'user',
options ? options.startWithValue : undefined
);
}
/**
* Suscribe to Firestore Document changes
*
* @param ref - Reference to the document you want to listen to
* @param options
*/
export function useFirestoreDoc<T = unknown>(
ref: firestore.DocumentReference,
options?: ReactFireOptions<T>
): firestore.DocumentSnapshot | T {
return useObservable(
doc(ref),
ref.path,
options ? options.startWithValue : undefined
);
}
/**
* Subscribe to a Firestore collection
*
* @param ref - Reference to the collection you want to listen to
* @param options
*/
export function useFirestoreCollection<T = { [key: string]: unknown }>(
ref: firestore.CollectionReference,
options?: ReactFireOptions<T[]>
): firestore.QuerySnapshot | T[] {
return useObservable(
fromCollectionRef(ref),
ref.path,
options ? options.startWithValue : undefined
);
}
/**
* Subscribe to a Realtime Database object
*
* @param ref - Reference to the DB object you want to listen to
* @param options
*/
export function useDatabaseObject<T = unknown>(
ref: database.Reference,
options?: ReactFireOptions<T>
): QueryChange | T {
return useObservable(
object(ref),
ref.toString(),
options ? options.startWithValue : undefined
);
}
/**
* Subscribe to a Realtime Database list
*
* @param ref - Reference to the DB List you want to listen to
* @param options
*/
export function useDatabaseList<T = { [key: string]: unknown }>(
ref: database.Reference | database.Query,
options?: ReactFireOptions<T[]>
): QueryChange[] | T[] {
return useObservable(
list(ref),
ref.toString(),
options ? options.startWithValue : undefined
);
}
/**
* modified version of rxFire's _fromTask
*
* @param task
*/
function _fromTask(task: storage.UploadTask) {
return new Observable<storage.UploadTaskSnapshot>(subscriber => {
const progress = (snap: storage.UploadTaskSnapshot) => {
return subscriber.next(snap);
};
const error = e => subscriber.error(e);
const complete = () => {
return subscriber.complete();
};
task.on('state_changed', progress, error, complete);
// I REMOVED THE UNSUBSCRIBE RETURN BECAUSE IT CANCELS THE UPLOAD
// https://github.com/firebase/firebase-js-sdk/issues/1659
});
}
/**
* Subscribe to the progress of a storage task
*
* @param task - the task you want to listen to
* @param ref - reference to the blob the task is acting on
* @param options
*/
export function useStorageTask<T = unknown>(
task: storage.UploadTask,
ref: storage.Reference,
options?: ReactFireOptions<T>
): storage.UploadTaskSnapshot | T {
return useObservable(
_fromTask(task),
'upload' + ref.toString(),
options ? options.startWithValue : undefined
);
}
/**
* Subscribe to a storage ref's download URL
*
* @param ref - reference to the blob you want to download
* @param options
*/
export function useStorageDownloadURL<T = string>(
ref: storage.Reference,
options?: ReactFireOptions<T>
): string | T {
return useObservable(
getDownloadURL(ref),
'download' + ref.toString(),
options ? options.startWithValue : undefined
);
}
export { SuspenseWithPerf, AuthCheck } from './components';
export { FirebaseAppProvider, useFirebaseApp } from './firebaseContext';
export * from './auth';
export * from './database';
export * from './firebaseApp';
export * from './firestore';
export * from './performance';
export * from './storage';
export * from './useObservable';

View File

@@ -8,7 +8,7 @@
"build-dev": "tsc",
"test-dev": "jest --verbose --watch",
"test": "jest --no-cache",
"watch": "tsc index.ts --lib DOM,ES2018 --watch --sourceMap --declaration --jsx react",
"watch": "tsc --watch",
"build": "rm -rf pub && tsc && cp package.pub.json pub/reactfire/package.json && cp ../README.md pub/reactfire/README.md"
},
"repository": {

View File

@@ -0,0 +1,54 @@
import { performance } from 'firebase/app';
import * as React from 'react';
import { useFirebaseApp } from '..';
export interface SuspensePerfProps {
children: React.ReactNode;
traceId: string;
fallback: React.ReactNode;
firePerf?: performance.Performance;
}
function getPerfFromContext(): performance.Performance {
const firebaseApp = useFirebaseApp();
if (!firebaseApp) {
throw new Error(
'Firebase not found in context. Either pass it directly to a reactfire hook, or wrap your component in a FirebaseAppProvider'
);
}
const perfFunc = firebaseApp.performance;
if (!perfFunc || !perfFunc()) {
throw new Error(
"No perf object off of Firebase. Did you forget to import 'firebase/performance' in a component?"
);
}
return perfFunc();
}
export function SuspenseWithPerf({
children,
traceId,
fallback,
firePerf
}: SuspensePerfProps) {
firePerf = firePerf || getPerfFromContext();
const trace = React.useMemo(() => firePerf.trace(traceId), [traceId]);
const Fallback = () => {
React.useLayoutEffect(() => {
trace.start();
return () => {
trace.stop();
};
}, []);
return <>{fallback}</>;
};
return <React.Suspense fallback={<Fallback />}>{children}</React.Suspense>;
}

View File

@@ -1,10 +1,10 @@
import { of, Subject, Observable, observable } from 'rxjs';
import { render, waitForElement, cleanup, act } from '@testing-library/react';
import * as React from 'react';
import { act, cleanup, render, waitForElement } from '@testing-library/react';
import { performance } from 'firebase/app';
import 'jest-dom/extend-expect';
import { SuspenseWithPerf, AuthCheck } from './components';
import { FirebaseAppProvider } from './firebaseContext';
import { auth, performance, User } from 'firebase/app';
import * as React from 'react';
import { Subject } from 'rxjs';
import { SuspenseWithPerf } from '.';
import { FirebaseAppProvider } from '../firebaseApp';
const traceStart = jest.fn();
const traceEnd = jest.fn();
@@ -18,15 +18,8 @@ const mockPerf = jest.fn(() => {
return { trace: createTrace };
});
const mockAuth = jest.fn(() => {
return {
onIdTokenChanged: jest.fn()
};
});
const mockFirebase = {
performance: mockPerf,
auth: mockAuth
performance: mockPerf
};
const Provider = ({ children }) => (
@@ -180,43 +173,3 @@ describe('SuspenseWithPerf', () => {
expect(createTrace).toHaveBeenCalled();
});
});
describe('AuthCheck', () => {
afterEach(() => {
cleanup();
jest.clearAllMocks();
});
it('can find firebase Auth from Context', () => {
expect(() =>
render(
<Provider>
<React.Suspense fallback={'loading'}>
<AuthCheck fallback={'loading'}>{'children'}</AuthCheck>
</React.Suspense>
</Provider>
)
).not.toThrow();
});
it('can use firebase Auth from props', () => {
expect(() =>
render(
<React.Suspense fallback={'loading'}>
<AuthCheck
fallback={<h1>not signed in</h1>}
auth={(mockFirebase.auth() as unknown) as auth.Auth}
>
{'signed in'}
</AuthCheck>
</React.Suspense>
)
).not.toThrow();
});
test.todo('renders the fallback if a user is not signed in');
test.todo('renders children if a user is logged in');
test.todo('checks requiredClaims');
});

View File

@@ -0,0 +1,61 @@
import { storage } from 'firebase/app';
import { getDownloadURL } from 'rxfire/storage';
import { Observable } from 'rxjs';
import { ReactFireOptions, useObservable } from '..';
/**
* modified version of rxFire's _fromTask
*
* @param task
*/
function _fromTask(task: storage.UploadTask) {
return new Observable<storage.UploadTaskSnapshot>(subscriber => {
const progress = (snap: storage.UploadTaskSnapshot) => {
return subscriber.next(snap);
};
const error = e => subscriber.error(e);
const complete = () => {
return subscriber.complete();
};
task.on('state_changed', progress, error, complete);
// I REMOVED THE UNSUBSCRIBE RETURN BECAUSE IT CANCELS THE UPLOAD
// https://github.com/firebase/firebase-js-sdk/issues/1659
});
}
/**
* Subscribe to the progress of a storage task
*
* @param task - the task you want to listen to
* @param ref - reference to the blob the task is acting on
* @param options
*/
export function useStorageTask<T = unknown>(
task: storage.UploadTask,
ref: storage.Reference,
options?: ReactFireOptions<T>
): storage.UploadTaskSnapshot | T {
return useObservable(
_fromTask(task),
'upload' + ref.toString(),
options ? options.startWithValue : undefined
);
}
/**
* Subscribe to a storage ref's download URL
*
* @param ref - reference to the blob you want to download
* @param options
*/
export function useStorageDownloadURL<T = string>(
ref: storage.Reference,
options?: ReactFireOptions<T>
): string | T {
return useObservable(
getDownloadURL(ref),
'download' + ref.toString(),
options ? options.startWithValue : undefined
);
}

View File

@@ -0,0 +1,11 @@
import 'jest-dom/extend-expect';
describe('Storage', () => {
describe('useStorageTask', () => {
test.todo('returns the same value as uploadTask');
});
describe('useStorageDownloadURL', () => {
test.todo('returns the same value as getDownloadURL');
});
});

View File

@@ -1,7 +1,7 @@
import * as React from 'react';
import { Observable } from 'rxjs';
import { startWith } from 'rxjs/operators';
import { ObservablePromiseCache } from './request-cache';
import { ObservablePromiseCache } from './requestCache';
const requestCache = new ObservablePromiseCache();

View File

@@ -1,4 +1,4 @@
import { useObservable } from './use-observable';
import { useObservable } from '.';
import { renderHook, act } from '@testing-library/react-hooks';
import { of, Subject, Observable, observable } from 'rxjs';
import { delay } from 'rxjs/operators';