Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ node_modules/

reactfire/docs/reactfire-metadata.json
reactfire/firestore-debug.log
reactfire/firebase-debug.log
reactfire/pub/**
pub
yarn-error.log
Expand Down
18 changes: 12 additions & 6 deletions reactfire/auth/auth.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ class MockAuth {
this.subscriber = null;
}

app = {
name: '[DEFAULT]'
};

notifySubscriber() {
if (this.subscriber) {
this.subscriber.next(this.user);
Expand Down Expand Up @@ -44,11 +48,11 @@ const Provider = ({ children }) => (
</FirebaseAppProvider>
);

const Component = ({ children }) => (
const Component = (props?: { children?: any }) => (
<Provider>
<React.Suspense fallback={'loading'}>
<AuthCheck fallback={<h1 data-testid="signed-out">not signed in</h1>}>
{children || <h1 data-testid="signed-in">signed in</h1>}
{props?.children || <h1 data-testid="signed-in">signed in</h1>}
</AuthCheck>
</React.Suspense>
</Provider>
Expand All @@ -57,12 +61,14 @@ const Component = ({ children }) => (
describe('AuthCheck', () => {
beforeEach(() => {
// clear the signed in user
mockFirebase.auth().updateUser(null);
act(() => mockFirebase.auth().updateUser(null));
});

afterEach(() => {
cleanup();
jest.clearAllMocks();
act(() => {
cleanup();
jest.clearAllMocks();
});
});

it('can find firebase Auth from Context', () => {
Expand Down Expand Up @@ -95,7 +101,7 @@ describe('AuthCheck', () => {
});

it('renders children if a user is logged in', async () => {
mockFirebase.auth().updateUser({ uid: 'testuser' });
act(() => mockFirebase.auth().updateUser({ uid: 'testuser' }));
const { getByTestId } = render(<Component />);

await wait(() => expect(getByTestId('signed-in')).toBeInTheDocument());
Expand Down
27 changes: 12 additions & 15 deletions reactfire/auth/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ export function preloadUser(firebaseApp: firebase.app.App) {
return preloadAuth(firebaseApp).then(auth => {
const result = preloadObservable(
user(auth() as firebase.auth.Auth),
'auth: user'
`auth:preloadUser:${firebaseApp.name}`
);
return result.request.promise;
return result.toPromise();
});
}

Expand All @@ -31,18 +31,12 @@ export function useUser<T = unknown>(
options?: ReactFireOptions<T>
): User | T {
auth = auth || useAuth()();

let currentUser = undefined;

if (options && options.startWithValue !== undefined) {
currentUser = options.startWithValue;
} else if (auth.currentUser) {
// if auth.currentUser is undefined or null, we won't use it
// because null can mean "not signed in" OR "still loading"
currentUser = auth.currentUser;
}

return useObservable(user(auth), 'auth: user', currentUser);
const currentUser = auth.currentUser || options?.startWithValue;
return useObservable(
user(auth),
`auth:${auth.app.name}:useUser:${JSON.stringify(options)}`,
currentUser
);
}

export function useIdTokenResult(user: User, forceRefresh: boolean = false) {
Expand All @@ -52,7 +46,10 @@ export function useIdTokenResult(user: User, forceRefresh: boolean = false) {

const idToken$ = from(user.getIdTokenResult(forceRefresh));

return useObservable<any>(idToken$, `${user.uid}-claims`);
return useObservable<any>(
idToken$,
`auth:getIdTokenResult:${user.uid}:forceRefresh=${forceRefresh}`
);
}

export interface AuthCheckProps {
Expand Down
20 changes: 10 additions & 10 deletions reactfire/firebaseApp/sdk.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { useFirebaseApp, preloadRequest, usePreloadedRequest } from '..';
import { useFirebaseApp } from '..';
import { preloadObservable, useObservable } from '../useObservable';
import { from } from 'rxjs';

type RemoteConfig = import('firebase/app').remoteConfig.RemoteConfig;
type Storage = import('firebase/app').storage.Storage;
Expand Down Expand Up @@ -98,7 +100,7 @@ function fetchSDK(
.then(() => settingsCallback(firebaseApp[sdk]))
.then(() => firebaseApp[sdk]);
}
preloadRequest(() => sdkPromise, `firebase-sdk-${sdk}`);
preloadObservable(from(sdkPromise), `firebase-sdk-${sdk}`);

return sdkPromise;
}
Expand All @@ -107,12 +109,7 @@ function useSDK(sdk: SDK, firebaseApp?: firebase.app.App) {
firebaseApp = firebaseApp || useFirebaseApp();

// use the request cache so we don't issue multiple fetches for the sdk
const result = preloadRequest(
() => fetchSDK(sdk, firebaseApp),
`firebase-sdk-${sdk}`
);

return usePreloadedRequest(result);
return useObservable(from(fetchSDK(sdk, firebaseApp)), `firebase-sdk-${sdk}`);
}

export function preloadAuth(
Expand All @@ -123,7 +120,7 @@ export function preloadAuth(
}

export function useAuth(firebaseApp?: firebase.app.App) {
return useSDK(SDK.AUTH, firebaseApp);
return useSDK(SDK.AUTH, firebaseApp) as () => firebase.auth.Auth;
}

export function preloadAnalytics(firebaseApp: firebase.app.App) {
Expand Down Expand Up @@ -197,7 +194,10 @@ export function preloadRemoteConfig(
}

export function useRemoteConfig(firebaseApp?: firebase.app.App) {
return useSDK(SDK.REMOTE_CONFIG, firebaseApp);
return useSDK(
SDK.REMOTE_CONFIG,
firebaseApp
) as () => firebase.remoteConfig.RemoteConfig;
}

export function preloadStorage(
Expand Down
11 changes: 6 additions & 5 deletions reactfire/firestore/firestore.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ describe('Firestore', () => {
await act(() => ref.add(mockData2));

const ReadFirestoreCollection = () => {
const collection = useFirestoreCollection(ref);
const collection = useFirestoreCollection(ref) as any;

return (
<ul data-testid="readSuccess">
Expand Down Expand Up @@ -151,11 +151,12 @@ describe('Firestore', () => {
await act(() => ref.add(mockData2));

const ReadFirestoreCollection = () => {
const list = (useFirestoreCollection(ref) as firestore.QuerySnapshot)
.docs;
const filteredList = (useFirestoreCollection(
const list = ((useFirestoreCollection(
ref
) as any) as firestore.QuerySnapshot).docs;
const filteredList = ((useFirestoreCollection(
filteredRef
) as firestore.QuerySnapshot).docs;
) as any) as firestore.QuerySnapshot).docs;

// filteredList's length should be 1 since we only added one value that matches its query
expect(filteredList.length).toEqual(1);
Expand Down
15 changes: 9 additions & 6 deletions reactfire/firestore/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export function useFirestoreDoc<T = unknown>(
): T extends {} ? T : firestore.DocumentSnapshot {
return useObservable(
doc(ref),
'firestore doc: ' + ref.path,
`useFirestoreDoc:${ref.path}:${JSON.stringify(options)}`,
options ? options.startWithValue : undefined
);
}
Expand All @@ -61,7 +61,7 @@ export function useFirestoreDocData<T = unknown>(
): T {
return useObservable(
docData(ref, checkIdField(options)),
'firestore docdata: ' + ref.path,
`useFirestoreDocData:${ref.path}:${JSON.stringify(options)}`,
checkStartWithValue(options)
);
}
Expand All @@ -76,7 +76,9 @@ export function useFirestoreCollection<T = { [key: string]: unknown }>(
query: firestore.Query,
options?: ReactFireOptions<T[]>
): T extends {} ? T[] : firestore.QuerySnapshot {
const queryId = getHashFromFirestoreQuery(query);
const queryId = `useFirestoreCollection:${getHashFromFirestoreQuery(
query
)}:${JSON.stringify(options)}`;

return useObservable(
fromCollectionRef(query, checkIdField(options)),
Expand All @@ -96,8 +98,7 @@ interface _QueryWithId extends firestore.Query {
}

function getHashFromFirestoreQuery(query: firestore.Query) {
const hash = (query as _QueryWithId)._query.canonicalId();
return `firestore: ${hash}`;
return (query as _QueryWithId)._query.canonicalId();
}

/**
Expand All @@ -110,7 +111,9 @@ export function useFirestoreCollectionData<T = { [key: string]: unknown }>(
query: firestore.Query,
options?: ReactFireOptions<T[]>
): T[] {
const queryId = getHashFromFirestoreQuery(query);
const queryId = `useFirestoreCollectionData:${getHashFromFirestoreQuery(
query
)}:${JSON.stringify(options)}`;

return useObservable(
collectionData(query, checkIdField(options)),
Expand Down
2 changes: 1 addition & 1 deletion reactfire/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"build-dev": "tsc --watch",
"test-dev": "jest --verbose --watch",
"emulators": "firebase emulators:start --only firestore,database",
"test": "firebase emulators:exec --only firestore,database \"jest --no-cache --verbose --detectOpenHandles --forceExit\"",
"test": "yarn build && firebase emulators:exec --only firestore,database \"jest --rootDir pub --no-cache --verbose --detectOpenHandles --forceExit\"",
"copy-package-json": "cp package.pub.json pub/reactfire/package.json",
"watch": "yarn build && tsc --watch",
"build": "rm -rf pub && tsc && yarn copy-package-json && cp ../README.md pub/reactfire/README.md && cp ../LICENSE pub/reactfire/LICENSE && rollup -c"
Expand Down
4 changes: 2 additions & 2 deletions reactfire/performance/performance.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ describe('SuspenseWithPerf', () => {
const Fallback = () => <h1 data-testid="fallback">Fallback</h1>;

const Comp = () => {
useObservable(o$, 'test');
useObservable(o$, 'perf-test-1');

return <h1 data-testid="child">Actual</h1>;
};
Expand Down Expand Up @@ -193,7 +193,7 @@ describe('SuspenseWithPerf', () => {
const o$ = new Subject();

const Comp = () => {
const val = useObservable(o$, 'test');
const val = useObservable(o$, 'perf-test-2');

if (val === 'throw') {
throw new Promise(() => {});
Expand Down
3 changes: 1 addition & 2 deletions reactfire/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,5 @@
"outDir": "pub/reactfire",
"declaration": true,
"skipLibCheck": true
},
"files": ["index.ts"]
}
}
90 changes: 90 additions & 0 deletions reactfire/useObservable/SuspenseSubject.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { Observable, Subject, Subscription, Subscriber, empty } from 'rxjs';
import { tap, share, catchError } from 'rxjs/operators';

export class SuspenseSubject<T> extends Subject<T> {
private _value: T | undefined;
private _hasValue = false;
private _timeoutHandler: NodeJS.Timeout;
private _innerSubscriber: Subscription;
private _firstEmission: Promise<void>;
private _resolveFirstEmission: () => void;
private _error: any = undefined;
private _innerObservable: Observable<T>;
private _warmupSubscription: Subscription;

constructor(innerObservable: Observable<T>, private _timeoutWindow: number) {
super();
this._firstEmission = new Promise<void>(
resolve => (this._resolveFirstEmission = resolve)
);
this._innerObservable = innerObservable.pipe(
tap(
v => {
this._next(v);
},
e => {
// save the error, so that we can raise on subscription or .value
// resolve the promise, so suspense tries again
this._error = e;
this._resolveFirstEmission();
}
),
catchError(() => empty()),
share()
);
// warm up the observable
this._warmupSubscription = this._innerObservable.subscribe();

// set a timeout for reseting the cache, subscriptions will cancel the timeout
// and reschedule again on unsubscribe
this._timeoutHandler = setTimeout(this._reset, this._timeoutWindow);
}

get hasValue(): boolean {
// hasValue returns true if there's an error too
// so that after we resolve the promise & useObservable is called again
// we won't throw again
return this._hasValue || !!this._error;
}

get value(): T {
// TODO figure out how to reset the cache here, if I _reset() here before throwing
// it doesn't seem to work.
// As it is now, this will burn the cache entry until the timeout fires.
if (this._error) {
throw this._error;
}
return this._value;
}

get firstEmission(): Promise<void> {
return this._firstEmission;
}

private _next(value: T) {
this._hasValue = true;
this._value = value;
this._resolveFirstEmission();
}

private _reset() {
// seems to be undefined in tests?
if (this._warmupSubscription) {
this._warmupSubscription.unsubscribe();
}
this._hasValue = false;
this._value = undefined;
this._error = undefined;
this._firstEmission = new Promise<void>(
resolve => (this._resolveFirstEmission = resolve)
);
}

_subscribe(subscriber: Subscriber<T>): Subscription {
if (this._timeoutHandler) {
clearTimeout(this._timeoutHandler);
}
this._innerSubscriber = this._innerObservable.subscribe(subscriber);
return this._innerSubscriber.add(this._reset);
}
}
Loading