Repository: streamich/react-use
Pull request #410 (2019-06-23 10:48:16)

Use observable


Patch:
diff --git a/src/useIsomorphicLayoutEffect.ts b/src/useIsomorphicLayoutEffect.ts
new file mode 100644
index 00000000..3e079ab1
--- /dev/null
+++ b/src/useIsomorphicLayoutEffect.ts
@@ -0,0 +1,9 @@
+import { useEffect, useLayoutEffect } from 'react';
+
+/**
+ * `useLayoutEffect` that does not show warning on server.
+ * See: https://medium.com/@alexandereardon/uselayouteffect-and-ssr-192986cdcf7a
+ */
+const useIsomorphicLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect;
+
+export default useIsomorphicLayoutEffect;
diff --git a/src/useObservable.ts b/src/useObservable.ts
index 49832492..a329aa16 100644
--- a/src/useObservable.ts
+++ b/src/useObservable.ts
@@ -1,4 +1,5 @@
-import { useEffect, useState } from 'react';
+import { useState } from 'react';
+import useIsomorphicLayoutEffect from './useIsomorphicLayoutEffect';
 
 export interface Observable<T> {
   subscribe: (
@@ -13,7 +14,7 @@ function useObservable<T>(observable$: Observable<T>, initialValue: T): T;
 function useObservable<T>(observable$: Observable<T>, initialValue?: T): T | undefined {
   const [value, update] = useState<T | undefined>(initialValue);
 
-  useEffect(() => {
+  useIsomorphicLayoutEffect(() => {
     const s = observable$.subscribe(update);
     return () => s.unsubscribe();
   }, [observable$]);


Test patch:
diff --git a/src/__tests__/useObservable-layout.spec.ts b/src/__tests__/useObservable-layout.spec.ts
new file mode 100644
index 00000000..28dd0c77
--- /dev/null
+++ b/src/__tests__/useObservable-layout.spec.ts
@@ -0,0 +1,21 @@
+import * as React from 'react';
+import * as ReactDOM from 'react-dom';
+import { Subject } from 'rxjs';
+import { useObservable } from '..';
+import useIsomorphicLayoutEffect from '../useIsomorphicLayoutEffect';
+
+jest.mock('../useIsomorphicLayoutEffect');
+
+test('uses', async () => {
+  const subject = new Subject();
+  const container = document.createElement('div');
+
+  const Demo = ({ obs }) => {
+    const value = useObservable(obs);
+    return React.createElement(React.Fragment, {}, value);
+  };
+
+  expect(useIsomorphicLayoutEffect).toHaveBeenCalledTimes(0);
+  ReactDOM.render(React.createElement(Demo, { obs: subject }), container);
+  expect(useIsomorphicLayoutEffect).toHaveBeenCalledTimes(1);
+});
diff --git a/src/__tests__/useObservable.test.tsx b/src/__tests__/useObservable.test.tsx
index 6bba8c87..4166c986 100644
--- a/src/__tests__/useObservable.test.tsx
+++ b/src/__tests__/useObservable.test.tsx
@@ -1,7 +1,21 @@
+import * as React from 'react';
+import * as ReactDOM from 'react-dom';
 import { act, renderHook } from 'react-hooks-testing-library';
 import { Subject } from 'rxjs';
 import { useObservable } from '..';
 
+let container: HTMLDivElement | null;
+
+beforeEach(() => {
+  container = document.createElement('div');
+  document.body.appendChild(container);
+});
+
+afterEach(() => {
+  document.body.removeChild(container!);
+  container = null;
+});
+
 test('default initial value is undefined', () => {
   const subject$ = new Subject();
   const { result } = renderHook(() => useObservable(subject$));
@@ -32,4 +46,62 @@ test('returns the latest value of observables', () => {
   expect(result.current).toBe(400);
 });
 
-xtest('subscribes to observable only once', () => {});
+test('subscribes to observable only once', async () => {
+  const subject = new Subject();
+  const spy = jest.spyOn(subject, 'subscribe');
+
+  expect(spy).toHaveBeenCalledTimes(0);
+
+  const Demo = ({ obs }) => {
+    const value = useObservable(obs);
+    return <>{value}</>;
+  };
+
+  ReactDOM.render(<Demo obs={subject} />, container);
+
+  expect(spy).toHaveBeenCalledTimes(1);
+
+  await new Promise(r => setTimeout(r, 1));
+  act(() => {
+    subject.next('a');
+  });
+  await new Promise(r => setTimeout(r, 1));
+  act(() => {
+    subject.next('b');
+  });
+
+  expect(spy).toHaveBeenCalledTimes(1);
+});
+
+test('re-renders component as obsevable changes', async () => {
+  const subject = new Subject();
+
+  let cnt = 0;
+  const Demo = ({ obs }) => {
+    cnt++;
+    const value = useObservable(obs);
+    return <>{value}</>;
+  };
+
+  ReactDOM.render(<Demo obs={subject} />, container);
+
+  await new Promise(r => setTimeout(r, 1));
+  expect(cnt).toBe(1);
+  expect(container.innerHTML).toBe('');
+
+  act(() => {
+    subject.next('a');
+  });
+
+  await new Promise(r => setTimeout(r, 1));
+  expect(cnt).toBe(2);
+  expect(container.innerHTML).toBe('a');
+
+  act(() => {
+    subject.next('b');
+  });
+
+  await new Promise(r => setTimeout(r, 1));
+  expect(cnt).toBe(3);
+  expect(container.innerHTML).toBe('b');
+});