Skip to content

Commit

Permalink
feat(react): add useState React hook
Browse files Browse the repository at this point in the history
  • Loading branch information
tlaundal committed Mar 19, 2020
1 parent 8e31277 commit 417fc48
Show file tree
Hide file tree
Showing 4 changed files with 186 additions and 3 deletions.
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,15 @@
"@ava/typescript": "1.1.1",
"@commitlint/cli": "8.3.5",
"@commitlint/config-conventional": "8.3.4",
"@testing-library/react-hooks": "^3.2.1",
"@types/react": "16.9.23",
"@types/sinon": "7.5.2",
"ava": "3.5.0",
"conditional-type-checks": "1.0.5",
"husky": "4.2.3",
"nyc": "15.0.0",
"react": "16.13.0",
"react-test-renderer": "16.13.0",
"rimraf": "3.0.2",
"rxjs": "6.5.4",
"rxjs-marbles": "5.0.4",
Expand All @@ -47,6 +51,7 @@
"rxjs-spy": "7.5.1"
},
"peerDependencies": {
"react": "^16.13.0",
"rxjs": "^6.5.0"
},
"ava": {
Expand Down
75 changes: 75 additions & 0 deletions src/react/useStream.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import test from 'ava';
import { renderHook } from '@testing-library/react-hooks';
import { useStream } from './useStream';
import { empty, Subject } from 'rxjs';

const initial = 'x';
const second = 'A';

test('useStream return initial value right away', t => {
const { result } = renderHook(() => useStream(empty(), initial));

t.deepEqual(result.current, 'x');
});

test('useStream return value from stream', t => {
const source$ = new Subject<string>();
const { result } = renderHook(() => useStream(source$, initial));

source$.next(second);

t.deepEqual(result.current, second);
});

test('useStream unsubscribes on unmount', t => {
const source$ = new Subject<string>();
const { unmount } = renderHook(() => useStream(source$, initial));

unmount();

t.deepEqual(source$.observers, []);
});

test('useStream does not resubscribe on rerender', t => {
const source$ = new Subject<string>();
const { rerender } = renderHook(() => useStream(source$, initial));

const subscription = source$.observers[0];
rerender();

t.assert(source$.observers[0] === subscription);
});

test('useStream does not update for changed initial value', t => {
const source$ = new Subject<string>();
const { rerender, result } = renderHook(
props => useStream(source$, props.initial),
{
initialProps: { initial },
}
);

source$.next(second);
rerender({ initial: 'something' });

t.deepEqual(result.current, second);
});

test('useStream unsubscribes, keeps latest value and subscribes new stream', t => {
const source1$ = new Subject<string>();
const source2$ = new Subject<string>();

const { rerender, result } = renderHook(
props => useStream(props.source$, initial),
{
initialProps: { source$: source1$ },
}
);

source1$.next(second);
rerender({ source$: source2$ });

t.deepEqual(result.current, second);
t.deepEqual(source1$.observers, []);
t.deepEqual(source2$.observers.length, 1);
});
30 changes: 30 additions & 0 deletions src/react/useStream.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { useState, useEffect } from 'react';
import { Observable } from 'rxjs';

/**
* React hook to subscribe to a stream
*
* Each emit from the stream will make the component re-render with the new
* value. Initially the `initial` argument is returned, because an `Observable`
* has no guarantee for when the first emit will happen.
*
* This hook passes the `source$` argument as a dependency to `useEffect`, which
* means you will need to take care that it is referentially equal between each
* render (unless you want to resubscribe, of course). Generally, you should
* only use this hook for static/global streams.
*
* @param source$ Stream that provides the needed values
* @param initial Initial value to return
* @see useEffect
*/
export const useStream = <T, I>(source$: Observable<T>, initial: I): T | I => {
const [value, setValue] = useState<T | I>(initial);

useEffect(() => {
const subscription = source$.subscribe(setValue);

return () => subscription.unsubscribe();
}, [source$]);

return value;
};
79 changes: 76 additions & 3 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.7.7.tgz#1b886595419cf92d811316d5b715a53ff38b4937"
integrity sha512-WtTZMZAZLbeymhkd/sEaPD8IQyGAhmuTuvTzLiCFM7iXiVdY0gc0IaI+cW0fh1BnSMbJSzXX6/fHllgHKwHhXw==

"@babel/runtime@^7.6.3":
"@babel/runtime@^7.5.4", "@babel/runtime@^7.6.3":
version "7.8.7"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.8.7.tgz#8fefce9802db54881ba59f90bb28719b4996324d"
integrity sha512-+AATMUFppJDw6aiR5NVPHqIQBlV/Pj8wY/EZH+lmvRdUo9xBaz/rF3alAwFJQavvKfeOlPE7oaaDHVbcySbCsg==
Expand Down Expand Up @@ -409,6 +409,14 @@
dependencies:
defer-to-connect "^1.0.1"

"@testing-library/react-hooks@^3.2.1":
version "3.2.1"
resolved "https://registry.yarnpkg.com/@testing-library/react-hooks/-/react-hooks-3.2.1.tgz#19b6caa048ef15faa69d439c469033873ea01294"
integrity sha512-1OB6Ksvlk6BCJA1xpj8/WWz0XVd1qRcgqdaFAq+xeC6l61Ucj0P6QpA5u+Db/x9gU4DCX8ziR5b66Mlfg0M2RA==
dependencies:
"@babel/runtime" "^7.5.4"
"@types/testing-library__react-hooks" "^3.0.0"

"@types/circular-json@^0.4.0":
version "0.4.0"
resolved "https://registry.yarnpkg.com/@types/circular-json/-/circular-json-0.4.0.tgz#7401f7e218cfe87ad4c43690da5658b9acaf51be"
Expand Down Expand Up @@ -463,6 +471,26 @@
resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0"
integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==

"@types/prop-types@*":
version "15.7.3"
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.3.tgz#2ab0d5da2e5815f94b0b9d4b95d1e5f243ab2ca7"
integrity sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw==

"@types/react-test-renderer@*":
version "16.9.2"
resolved "https://registry.yarnpkg.com/@types/react-test-renderer/-/react-test-renderer-16.9.2.tgz#e1c408831e8183e5ad748fdece02214a7c2ab6c5"
integrity sha512-4eJr1JFLIAlWhzDkBCkhrOIWOvOxcCAfQh+jiKg7l/nNZcCIL2MHl2dZhogIFKyHzedVWHaVP1Yydq/Ruu4agw==
dependencies:
"@types/react" "*"

"@types/react@*", "@types/[email protected]":
version "16.9.23"
resolved "https://registry.yarnpkg.com/@types/react/-/react-16.9.23.tgz#1a66c6d468ba11a8943ad958a8cb3e737568271c"
integrity sha512-SsGVT4E7L2wLN3tPYLiF20hmZTPGuzaayVunfgXzUn1x4uHVsKH6QDJQ/TdpHqwsTLd4CwrmQ2vOgxN7gE24gw==
dependencies:
"@types/prop-types" "*"
csstype "^2.2.0"

"@types/[email protected]":
version "7.5.2"
resolved "https://registry.yarnpkg.com/@types/sinon/-/sinon-7.5.2.tgz#5e2f1d120f07b9cda07e5dedd4f3bf8888fccdb9"
Expand All @@ -473,6 +501,14 @@
resolved "https://registry.yarnpkg.com/@types/stacktrace-js/-/stacktrace-js-0.0.32.tgz#d23e4a36a5073d39487fbea8234cc6186862d389"
integrity sha512-SdxmlrHfO0BxgbBP9HZWMUo2rima8lwMjPiWm6S0dyKkDa5CseamktFhXg8umu3TPVBkSlX6ZoB5uUDJK89yvg==

"@types/testing-library__react-hooks@^3.0.0":
version "3.2.0"
resolved "https://registry.yarnpkg.com/@types/testing-library__react-hooks/-/testing-library__react-hooks-3.2.0.tgz#52f3a109bef06080e3b1e3ae7ea1c014ce859897"
integrity sha512-dE8iMTuR5lzB+MqnxlzORlXzXyCL0EKfzH0w/lau20OpkHD37EaWjZDz0iNG8b71iEtxT4XKGmSKAGVEqk46mw==
dependencies:
"@types/react" "*"
"@types/react-test-renderer" "*"

"@typescript-eslint/eslint-plugin@^2.18.0":
version "2.18.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.18.0.tgz#f8cf272dfb057ecf1ea000fea1e0b3f06a32f9cb"
Expand Down Expand Up @@ -1384,6 +1420,11 @@ crypto-random-string@^2.0.0:
resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-2.0.0.tgz#ef2a7a966ec11083388369baa02ebead229b30d5"
integrity sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==

csstype@^2.2.0:
version "2.6.9"
resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.9.tgz#05141d0cd557a56b8891394c1911c40c8a98d098"
integrity sha512-xz39Sb4+OaTsULgUERcCk+TJj8ylkL4aSVDQiX/ksxbELSqwkgt4d4RD7fovIdgJGSuNYqwZEiVjYY5l0ask+Q==

currently-unhandled@^0.4.1:
version "0.4.1"
resolved "https://registry.yarnpkg.com/currently-unhandled/-/currently-unhandled-0.4.1.tgz#988df33feab191ef799a61369dd76c17adf957ea"
Expand Down Expand Up @@ -2883,7 +2924,7 @@ log-symbols@^3.0.0:
dependencies:
chalk "^2.4.2"

loose-envify@^1.4.0:
loose-envify@^1.1.0, loose-envify@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==
Expand Down Expand Up @@ -3598,7 +3639,7 @@ progress@^2.0.0:
resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8"
integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==

prop-types@^15.7.2:
prop-types@^15.6.2, prop-types@^15.7.2:
version "15.7.2"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5"
integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==
Expand Down Expand Up @@ -3652,6 +3693,30 @@ react-is@^16.8.1:
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.12.0.tgz#2cc0fe0fba742d97fd527c42a13bec4eeb06241c"
integrity sha512-rPCkf/mWBtKc97aLL9/txD8DZdemK0vkA3JMLShjlJB3Pj3s+lpf1KaBzMfQrAmhMQB0n1cU/SUGgKKBCe837Q==

react-is@^16.8.6:
version "16.13.0"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.0.tgz#0f37c3613c34fe6b37cd7f763a0d6293ab15c527"
integrity sha512-GFMtL0vHkiBv9HluwNZTggSn/sCyEt9n02aM0dSAjGGyqyNlAyftYm4phPxdvCigG15JreC5biwxCgTAJZ7yAA==

[email protected]:
version "16.13.0"
resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-16.13.0.tgz#39ba3bf72cedc8210c3f81983f0bb061b14a3014"
integrity sha512-NQ2S9gdMUa7rgPGpKGyMcwl1d6D9MCF0lftdI3kts6kkiX+qvpC955jNjAZXlIDTjnN9jwFI8A8XhRh/9v0spA==
dependencies:
object-assign "^4.1.1"
prop-types "^15.6.2"
react-is "^16.8.6"
scheduler "^0.19.0"

[email protected]:
version "16.13.0"
resolved "https://registry.yarnpkg.com/react/-/react-16.13.0.tgz#d046eabcdf64e457bbeed1e792e235e1b9934cf7"
integrity sha512-TSavZz2iSLkq5/oiE7gnFzmURKZMltmi193rm5HEoUDAXpzT9Kzw6oNZnGoai/4+fUnm7FqS5dwgUL34TujcWQ==
dependencies:
loose-envify "^1.1.0"
object-assign "^4.1.1"
prop-types "^15.6.2"

read-pkg-up@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-1.0.1.tgz#9d63c13276c065918d57f002a57f40a1b643fb02"
Expand Down Expand Up @@ -3965,6 +4030,14 @@ safe-buffer@~5.1.0, safe-buffer@~5.1.1:
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==

scheduler@^0.19.0:
version "0.19.0"
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.19.0.tgz#a715d56302de403df742f4a9be11975b32f5698d"
integrity sha512-xowbVaTPe9r7y7RUejcK73/j8tt2jfiyTednOvHbA8JoClvMYCp+r8QegLwK/n8zWQAtZb1fFnER4XLBZXrCxA==
dependencies:
loose-envify "^1.1.0"
object-assign "^4.1.1"

semver-compare@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/semver-compare/-/semver-compare-1.0.0.tgz#0dee216a1c941ab37e9efb1788f6afc5ff5537fc"
Expand Down

0 comments on commit 417fc48

Please sign in to comment.