Coder Social home page Coder Social logo

re-rxjs / react-rxjs Goto Github PK

View Code? Open in Web Editor NEW
528.0 19.0 17.0 2.02 MB

React bindings for RxJS

Home Page: https://react-rxjs.org

License: MIT License

JavaScript 0.70% TypeScript 99.26% Shell 0.04%
react rxjs typescript react-hooks react-suspense

react-rxjs's Introduction

React-RxJS Logo React-RxJS

Build Status codecov version MIT License All Contributors PRs Welcome Code of Conduct

React-RxJS is a library that offers React bindings for RxJS

Please visit the website: https://react-rxjs.org

Main features

  • πŸŒ€ Truly Reactive
  • ⚑ Highly performant and free of memory-leaks
  • πŸ”€ First class support for React Suspense and ready for Concurrent Mode
  • βœ‚οΈ Decentralized and composable, thus enabling optimal code-splitting
  • πŸ”¬ Tiny and tree-shakeable
  • πŸ’ͺ Supports TypeScript

Installation

npm install @react-rxjs/core

Contributors ✨

Thanks goes to these wonderful people (emoji key):


Josep M Sobrepere

πŸ’» πŸ€” 🚧 ⚠️ πŸ‘€ πŸ“– πŸš‡

VΓ­ctor Oliva

πŸ€” πŸ‘€ πŸ’» ⚠️ πŸ“–

Ed

🎨

Pierre Grimaud

πŸ“–

Bhavesh Desai

πŸ‘€ πŸ“– ⚠️

Matt Mischuk

πŸ“–

Riko Eksteen

πŸš‡ πŸ‘€ πŸ“– πŸ’» πŸ€”

hoclun-rigsep

πŸ“– πŸ€”

Luke Shiels

πŸ› πŸ’»

This project follows the all-contributors specification. Contributions of any kind welcome!

react-rxjs's People

Contributors

allcontributors[bot] avatar dependabot[bot] avatar josepot avatar mattmischuk avatar pgrimaud avatar rikoe avatar skve avatar voliva avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

react-rxjs's Issues

<Subscribe> not working

Hi, I tried to implement this with my knowledge of how it works in Angular, that is, handle the subscription by myself. It worked right, but it was failing in RTL. So I followed the documentation and used the <Subscribe></Subscribe> component to wrap my component, but I was getting an error Uncaught [Error: Missing Subscribe!]. So I brought down my implementation and followed the docs word for word, but the error didn't go away. Below is the code and the error:

import { createSignal } from "@react-rxjs/utils"
import { map } from "rxjs/operators"

// A signal is an entry point to react-rxjs. It's equivalent to using a subject
const [textChange$, setText] = createSignal<string>();

const [useText, text$] = bind(textChange$, "")

function TextInput() {
  const text = useText()

  return (
    <div>
      <input
        type="text"
        value={text}
        placeholder="Type something..."
        onChange={(e) => setText(e.target.value)}
      />
      <br />
      Echo: {text}
    </div>
  )
}

// Previously...
// const [useText, text$] = bind(...);

const [useCharCount, charCount$] = bind(
  text$.pipe(
    map((text) => text.length)
  )
)

function CharacterCount() {
  const count = useCharCount()

  return <>Character Count: {count}</>
}

function CharacterCounter() {
    return (
      <div>
        <Subscribe>
          <TextInput />
          <CharacterCount />
        </Subscribe>
      </div>
    )
  }

export default CharacterCount

image

##Steps to Produce

  1. Create react app with typescript template
  2. Copy paste the code from the documentation
  3. Run npm start

Subscribing in render phase is not "safe"

In the same vein as my concerns in kosich/react-rxjs-elements#4 I walked through your library to see how it works, it is very clever!

However, I see that in render phase, the library is calling toPromise (essentially subscribing in render). Would it be accurate to say that the way this library works is as following:

  • User calls connectObservable causing their stream to gain a monkey patched getValue() method
  • React renders the hook for consuming your stream, internally it passes an init thunk to useReducer
  • This init thunk is invoked by react, in the render phase, which calls getValue()
  • getValue causes the stream to become subscribed (it calls toPromise())
    • this happens in render phase
    • a 200ms timeout is set to unsubscribe..
  • If React commits, the useEffect hook runs, and subscribes a second time (to the shared observable, so the source stream is only subscribed once)
  • If react doesn't commit until 201ms, the first subscription will be destroyed by the timeout, and theuseEffect hook will run causing a second subscription (to a new instance of a shared observable, so the source stream is subscribed twice as well)

Is this all correct? If subscribing to a stream triggers side effects like a network request, this could be really important to users for it to be documented as a tradeoff the library makes, so they can know this upfront instead of learning it the "hard way" after writing an app.

If I understand, there is a tradeoff here where:

  • lowering the timeout can increase the chances of duplicate side effects
  • increasing the timeout will mitigate the affordance where the library is intending to solve for "memory leaks" (if react is aborting renders fast enough there could be enough back pressure to cause memory leaks depending how high the timeout is set & how fast react is aborting renders)
  • if a render phase in react takes over 200ms before it commits, effects will be double subscribed

Final API for the first stable release

After thinking a lot about it, there are 2 important API changes that I would like to make before the initial release:

  • Merge connectObservable and connectFactoryObservable into connect. I find the current names very verbose. At the beginning I didn't want to do this because I thought that it was going to be too annoying for those developers that want to start using this library in a redux project... However, I've now changed my mind. I think that it's unlikely that the 2 connects will be used in the same file. I'm willing to consider other names in order to avoid the collision with react-redux. Also, I'm of the opinion that greenfield projects that want to use this library should avoid using redux in the first place.

  • I'm getting rid of subjectFactory in favor of groupInMap. The reason being that anything that can be done using subjectFactory can also do it using groupInMap, also the second one offers a means to access the whole dictionary (it's keys it's length, etc)... It's a lot less "magical" and I think that it's a much better primitive. So, let's get rid of the fluffy one.

I'm also considering another small change:

  • switchMapSuspended should not immediately start emitting SUSPENSE... I think that it should wait for a few milliseconds and only if nothing has been emitted for those few millisecs, then it should emit SUSPENSE... IMO that creates a much nicer user experience... Therefore, I think that the operator should take a timeoutMs optional argument (similar to the one provided by useTransition). I think that a good default value should be ~1500ms.

Thoughts, comments, ideas... πŸ™

hook returned by bind(functionAcceptingArrayReturningAsyncObservable$) should not cause infinite render loop

Observed Behavior

Given

  1. a function getAsyncObservableFromArray$(words: string[]) that returns an asynchronous observable
  2. a use... hook that is derived from aforementioned function
  3. a React component that invokes the aforementioned hook function

... the component causes an infinite render loop.

Expected Behavior

There should be no infinite render loop, the behavior should be no different from a similar scenario with

  1. a function getObservableFromArray$(words: string[]) that returns a synchronous observable
  2. a function getAsyncObservableFromScalar$(word: string) that returns an asynchronous observable

Steps to Reproduce

Uncomment line 35 in App.tsx in this code sandbox

ESLint rule to prevent `connectObservable` and `connectFactoryObservable` being used inside components

I think that a common pitfall that new-comers will experience is that they may inadvertently use connectObservable/connectFactoryObservable from inside a functional component.

I think that we should create a ESLint rule so that developers don't fall for that mistake. Any volunteers? cough @voliva cough

I think that as a first implementation something that just checks that the connector functions are being called from the "root scope" (not inside a function) should be enough? πŸ€”

Also, I think that the ESLint rule should be an error, not a warning.

Webpack 4 Build Error

I included react-rxjs in a Next.js POC and it threw an error during build because it ships an esm module with optional chaining:

error - ./node_modules/@react-rxjs/utils/dist/utils.esm.js 66:40
Module parse failed: Unexpected token (66:40)
You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file. See https://webpack.js.org/concepts#loaders
|     let innerSub;
|     let outterSub = source$.subscribe((n) => {
>       innerSub = innerSub || n.get(key)?.subscribe(observer);
|     }, (e) => {
|       observer.error(e);

The underlying problem seems to be the JS parser in webpack 4, and it was solved by changing the configuration of Next.js's build system to use webpack 5.

Do you plan to support building with webpack 4 or generally running the library on older engines?

I failed to create a counter

ReactDOM.unstable_createRoot(
  document.getElementById('root')
).render(<Suspense fallback={<>wait</>}><App /></Suspense>);

function App() {
  const [useCounter] = connectObservable(interval(1000));
  const count = useCounter();
  return <div className="App">count {count}</div>;
}

It just says "wait". I can't get anything to work, except an initial value or initial suspend... Am I doing something wrong?

Test improvements

Most of the work is already done, but there's still margin for improvement on the tests. I focused on those methods exposed as part of the API.

connectObservable

  • It's missing a test for updates on the source stream.
  • Q: I understand re-rxjs doesn't do any sort of batching. Is there any way to show how to make react batch updates?
  • Maybe the test it works with suspense can be broken down. I haven't taken a deep look and I'm a bit out of the loop from suspense, but it will be interesting to see if we can break that test down.

connectFactoryObservable

  • On test it returns the latest emitted value it only checks for the initial value. Needs to add in some updates on the source stream.
  • I think it would be good to have a test it shares the subscription among all of the subscribers of the same key. It is superseeded by another test, but this will document a bit better how the key works.
    /**
     * - one hook subscribes to key1
     * - it receives the initial value
     * - perform a change on key1 subject
     * - another hook subscribes to key1
     * - both receive the change made on key1 subject
     * - another hook subscribes to key2, and receives the initial value
     * - all hooks unmount
     */
  • The test observable > if the source observable completes it keeps emitting the latest value until there are no more subscriptions has an expect I found confusing:
  let diff = -1
  const [, getShared] = connectFactoryObservable((_: number) => {
    diff++
    return from([1, 2, 3, 4].map(val => val + diff))
  })

  let latestValue1: number = 0
  let nUpdates = 0
  const sub1 = getShared(0).subscribe(x => {
    latestValue1 = x
    nUpdates += 1
  })
  expect(latestValue1).toBe(4)
  expect(nUpdates).toBe(1) // <--- this one
  /**
   * Feels like it shouldn't be expected to receive only the last value from the source
   * as it seems like it's sort of debouncing the updates.
   * I've checked and it's not, it's only on this specific case where you have a synchronous
   * source that you will get the last value of that synchronous batch on the first subscription.
   * Following "synchronous batches" are fine (they will emit every single item), it's just
   * the initial one.
   * Maybe it's worth having a test just to have this case documented, even if the behaviour
   * changes later on if we consider this needs to be fixed.
   */
  • The error handling ones we mentioned how having a test working with react would be cool. I also wonder how to replicate this test also for connectObservable (as it applies as well)

createInput

I found the test cover everything I could think of πŸ‘

rxjs operators

  • Maybe we also need a set of small tests for suspended and switchMapSuspended

Uncaught Error: Missing Subscribe

Hello, I am getting this error whenever I use bind() without the second parameter (default value).

Even a simple

const [useFoo] = bind(of(1))

const Foo = () => {
  useFoo()
}

triggers the error.

const [useFoo] = bind(of(1), any)

this works however

bind hook only firing once with scan and aggregated map

Hi,

I'm trying to implement a stream that collect items from other streams into a map / object.
But when I then bind the stream for the collected map I do not receive all updates to the stream.

I made a simple example to showcase that:

const $counter = interval(1000);

export const [useCounterMap, counterMap$] = bind(
  $counter.pipe(
    scan((map, b) => {
      map[String(b)] = b;
      return map;
    }, {})),
  {}
);

Somewhere in a React component:

const counter = useCounterMap();

useEffect(() => {
  console.log(counter, '(counter)')
}, [counter]);

In the logs I receive the following:

{} (counter)
{0: 0} (counter)

And that's it ..
So the init value and the first timer event are received successfully, but then it stops.
I'm pretty new to rxjs, so maybe I'm doing sth wrong or overlooking sth,
but shouldn't the effect keep logging the counter-map with each second?
I do see the full map with every update when I directly subscribe to counterMap$

handling subscriptions reactively

I've been thinking a lot about #101 lately and I really think that we should do a better job at being explicit about how we think that those subscriptions should be handled.

It's easy for me and for @voliva to know what's safe and what's not, because we understand what are the internals and the implications of misusing bind. However, we need to find a better way to setup our users for success.

That's why I think that we should promote a pattern of reactive top-level subscriptions (kinda like a router) that ensures that when a hook generated from bind is consumed, the subscription is already there. And also, that the subscription will be disposed once it's no longer needed. Also, this practice would be aligned with what the React team proposes for data-fetching: the requests should start before render.

In order to do that, though. It's necessary to make an important change to the "shared-observable" that's exposed from bind: that observable should not complete, otherwise it will be too complicated for the consumers to use this library safely.

I will be posting updates on this issue, but I don't think that we can go life with react-rxjs.org until we haven't figured this out properly.

Unable to find package.json for @react-rxjs/*

Hi @josepot πŸ‘‹

I hope you are doing great!

I noticed while running yarn storybook the following warning:

WARN unable to find package.json for @react-rxjs/core
WARN unable to find package.json for @react-rxjs/utils

I know is just a warning and probably does not affect anything, I am also unsure how to address it because I did check your source files and I found the package.json for utils.

Nonetheless I thought you should know about this warning!

Take care,
πŸ™‡

Updating an element in a observable list

Thank you for this awesome library.

Reviewing your example https://react-rxjs.org/docs/tutorial/github-issues I can see that there's a good example of requesting a list of items form https://api.github.com/repos/${org}/${repo}/issues?per_page=25&page=${page}.

currentRepoAndPage$ is a merge of 2 source observables currentRepo$ and pageSelected$. When any of these source Subjects emit a new value

 currentRepoAndPage$.pipe(
    switchMap(({ page, repo, org }) =>
      getIssues(org, repo, page).pipe(startWith(SUSPENSE))
    )
  )

will re-execute.

The question; if I had an observable with a list of issues and I wanted to update one of the issues in the list, what would this look like? Would I update the item in the list from the component and call a function to push the new list into the Subject?

I also wondered if we could use BehaviorSubject instead of Subject?

Create lower-level bindings that are "ECMAScript Observable" compliant

I've been thinking about this and I felt in love with the idea of creating a lower-level library that's not coupled to RxJS and that exposes a subset of the current Re-RxJS API, concretely it would expose:

  • connectObservable
  • connectFactoryObservable
  • shareLatest
  • SUSPENSE

Those bindings would be compatible with any implementation of the ECMAScript Observable spec, then Re-RxJS could use that library internally to provide the same API that it currently has.

Its API would also have a setObservable method that would receive an Observable implementation that it's compliant with the ECMAScript Observable spec.

Thoughts?

When an Observable emits a function

Upgrading from @react-rxjs/[email protected] -> @react-rxjs/[email protected] came across this regression.

When an observable returns a function, we get a stringified (plus evaluated!) version of the function. This worked in the prior version.

const counter$ = timer(0, 1000).pipe(startWith(0), shareReplay(1));

const [useBoundMethodThatDoesntWork] = bind(() =>
  counter$.pipe(
    map((count) => {
      return (s) => `${s}: ${count}`;
    })
  )
);

// .....


const method = useBoundMethod();
method('a') // -> Not a function
typeof method // -> 'stirng'
return ({method})
  // -> function (s) { return "".concat(s, ": ").concat(count); }: 0:

Code

Question - Todo App Demo

Hi guys, first off, I want to say I love this library and the way it integrates with React.

I was reading and doing some tests using the Todo App provided by the docs, and I'm trying to make the application start with some todos loaded from an API call, but I don't think the way I did here is it right.

It doesn't fell right calling .subscribe mannualy from state file.

The getUsers function is inside api folder.

Can you guys take a look at this or suggest to me what would be the better way of doing this?

https://codesandbox.io/s/react-rxjs-basic-todos-9snq9w?file=/src/api/getTodos.ts

thanks

Component receiving and empty object when remounting

Steps to reproduce:

  1. Call bind with an observable that emits asynchronously (e.g. 1000ms)
  2. Create a component that uses the hook from (1)
  3. Use this component in two different routes, each with its own <Subscribe> boundary
  4. Switch from one route to the other

Expected behaviour:

  • The hook returns the actual value after suspending.

Current behaviour (0.6.1):

  • The hook returns {} (EMPTY_VALUE) without suspending.

Sandbox: https://codesandbox.io/s/react-rxjs-empty-value-0n7zj?file=/src/App.tsx

react-rxjs hook not executing correctly

I'm trying to get an idea from your GitHub example of how I might use your library to manage some data streams to represent an API request including data, isLoading and error. I know I could have 3 separate Subjects and maybe this is the better way to achieve this. However I do think having collating related data makes sense for organising code or accessing related data streams through a single react-rxjs hook on a component. I have many API requests in this app, and would like to re-use this pattern throughout the app.

The state

import { merge, Subject } from "rxjs"
import { shareLatest, bind } from "@react-rxjs/core"
import API from "../api/api"
import ROUTES from "../api/routes"

interface EntityField {
  id: string;
  entityId: string;
  name: string;
  type: "Text" | "Number" | "Checkbox";
  size: "Large" | "Medium" | "Small";
  searchable: boolean;
  required: boolean;
  position: number;
}

const isLoading = new Subject<boolean>();
const toggleLoading = (loading: boolean) => isLoading.next(loading);

const error = new Subject<string>();
const setError = (errorMessage: string) => error.next(errorMessage);

const fields = new Subject<EntityField[]>();
const loadEntityFields = async (entityId: string) => {
  toggleLoading(true);
  try {
    const { data } = await API.get(`${ROUTES.ENTITIES_FIELDS}/${entityId}/fields`);
    fields.next(data);
  } catch (e) {
    setError(e);
    console.error(e);
  } finally {
    toggleLoading(false);
  }
}

const entityFieldsMerge = merge(
  isLoading,
  error,
  fields
).pipe(shareLatest())

const [useEntityFields, entityFields$] = bind(entityFieldsMerge);

const entityFieldsStore = {
  useEntityFields,
  entityFields$,
  loadEntityFields
}

export default entityFieldsStore;

This doesn't seem to work - In the component I have:

const EntityFields: React.FC<EntityFieldsProps> = ({ entityId }) => {
  console.log('entityId ', entityId );
  const classes = useEntityFieldsStyles();

  const fields = entityFieldStore.useEntityFields();
  console.log("fields", fields);    
....

For the subscription I've done:

const provider$ = merge(entityFieldStore.entityFields$, layoutStore.profileSidePanelOpen$, layoutStore.sideMenuOpen$);

function App() {
  return (
    <Subscribe source$={provider$}>
      <ThemeProvider theme={theme}>
        <CssBaseline />
        <Router>
          <Routes />
        </Router>
      </ThemeProvider>
    </Subscribe>
  );
}

The layoutStore related hooks work fine, of course they've very simple boolean observables without any merges, but depending on your response I might refactor this code too.

The console.log("fields", fields); doesn't execute and with the hook useEntityFields the app appears to behave in a peculiar way - it appears to interfere with the current MobX store (but that's probably just a false flag).

Am I approaching this solution in the wrong way?

I'm hoping to change from MobX to the react-rxjs in the event that I can successfully demonstrate a PoC with react-rxjs.

v0.2.0

I think that we've almost at a point where I would feel good releasing v0.2.0 of this library.

Things that I would like to discuss before v0.2.0 is released:

  • I've got used to the name distinctShareReplay, but I hate it. I think that it's a horrible name... I'm actually considering the idea of coining a term for the kind of Observable that it generates (like BehaviorObservable or ReactObservable or WHATEVER) and then renaming the operator to something like asReactObservable or asBehaviorObservable.

I'm also starting to wonder whether this operator should be exposed in the first place... But I think that it should. Because we know from experience that if we don't expose it, then most of the users of this library will have to create their own custom shareReplay, and the worst part is that it's not even possible to create the custom shareReplay that would work best with this library by using any of the overloads of the current RxJS shareReplay... So, yeah, I think that it should be exposed, but not with that name... Also, maybe we should only export the "customShareReplay" behavior without the distinct? and then we could simply call it shareLatest? πŸ€” Or maybe we could just name it shareLatest and that's it (with the distinct behavior still on it)?

I won't release v0.2.0 until I'm not happy with this.

There are 2 other things that I would like to happen before we release v0.2.0:

  1. use docosaurus to generate a site for https://re-rxjs.org or for https://rerxjs.org (I bought both domains). Ideally I would like to have in that site similar to the one that recoil.js has, at least with the following sections:
  • Introduction
  • Basic Tutorial
  • API Reference
  1. A blog-post announcing the release of this library.

I've been working on those 2 items, but I will need help... I will create separate issues for those 2. Oh, the docs-site would live under a completely different repo.

connectFactoryObservable: Object parameters will cause infinite loops

Hey, thanks for the project. You saved me a lot of headaches.

The following code will lead to an infinite loop when used in a component:

export type ArticleQuery = {
	searchTerm?: string,
	author?: string
}

const [useArticleQuery] = bind((query: ArticleQuery) => searchForArticles$(query))

This is likely, because the NestedMap only expects an array of arguments and doesn't deeply compare objects.

I would suggest taking a look at the system that react-query uses for key generation. They sort the entries of an object before converting it to json. (See the function stableStringify)

Maybe it is also somehow possible to detect when a loop occurs and throw an error instead of potentially sending many requests to a server.

should we expose `defaultStart`?

Inside the internal-utils of @react-rxjs/utils, we have this operator named defaultStart which IMO can be very useful when using these bindings. It's very simiar to startWith, the main difference is that defaultStart only emits the provided starting value if its source doesn't synchronously emit anything.

The questions is: should we expose it from @react-rxjs/utils? and if we did, can you think of a better name?

When is <Subscribe /> actually necessary?

Gentlemen,

I really like what you are doing here. However I am having trouble with this part of the documentation:

Something to note is that a subscription on the underlying observable must be present before the hook is executed. We can use Subscribe to help us with it:

    function CharacterCounter() {
      return (
        <div>
          <Subscribe source$={charCount$}>
            <TextInput />
            <CharacterCount />
          </Subscribe>
        </div>
      )
    }

When I remove the Subscribe component, nothing bad seems to happen. I've spent a while reading your delightfully succinct code but thought it would be easier to ask for an example where Subscribe is clearly necessary than try to work one out for myself. Thanks in advance.

Typing question with createSignal()

The following code produces this complaint from the compiler: Expected 0 arguments, but got 1. TS2554

const [resetTo$, doResetTo] = createSignal();
doResetTo(5);

whereas this code will compile:

const [resetTo$, doResetTo] = createSignal(x => x);
doResetTo(5);

and so will this:

const [resetTo$, doResetTo] = createSignal<number>();
doResetTo(5);

I'm not sure I find this intuitive, or understand why that overload at

export function createSignal(): [Observable<void>, () => void]
is necessary. Assuming this is all as intended, do you wish me to document it?

Rest parameters on >0.4.3 and <0.6.1

This is fixed in 0.6.1 so just looking for advice on this, and whether a fix can be patched into 0.5.x
Affects versions >0.4.3 <0.6.1

const [useRequiresTheArray] = bind((s: string, a: string[]) => {
  return of(a.join(s));
});

is fine, but

const [useRequiresTheArray] = bind((s: string, ...a: string[]) => {
  return of(a.join(s));
});
Warning: The final argument passed to useEffect changed size between renders. The order and size of this array must remain constant.

Example Code

Uncaught Error: Missing Subscribe

Unfortunately, I don't know what to do because I am very new to the React world and have only worked with Angular until now. Unfortunately, I cannot assign this error. What does "Missing Subscribe" mean? Do I have to subscribe to it myself? If so, what steps are missing here? I have orientated myself on the tests and also on the React page how to build custom hooks.

Sources:
https://github.com/re-rxjs/react-rxjs/blob/main/packages/core/src/bind/connectObservable.test.tsx
https://react-rxjs.org/docs/api/core/bind
https://reactjs.org/docs/hooks-custom.html

Where are my misconceptions of using of react-rxjs?

log://error

Error
Missing Subscribe!
Call Stack
 gv
  vendor.js:1142:15
 mountSyncExternalStore
  vendor.js:23762:20
 Object.useSyncExternalStore
  vendor.js:24701:14
 useSyncExternalStore
  vendor.js:40731:21
 useStateObservable
  vendor.js:1175:101
 useStaticObservable
  vendor.js:1190:37
 App
  main.js:102:30
 renderWithHooks
  vendor.js:23268:18
 mountIndeterminateComponent
  vendor.js:27960:13
 beginWork
  vendor.js:29464:16

file://main.tsx

root.render(
  <StrictMode>
    <App />
  </StrictMode>,
);

file://app.tsx

export function App() {
  const element = useWatchSelector('video', 0, 250);
  return <div>{radarElement ? 'Found' : 'notFound'}</div>;;
}

file:// use-watch-selector.ts

import { bind } from '@react-rxjs/core';
import { watchSelector } from './radar.observable';
import { useEffect, useState } from 'react';

export function useWatchSelector<Element extends HTMLElement>(
  selector: string,
  startWithInterval = 0,
  interval = 250,
  context: Document | HTMLElement = document,
) {
  const [watchElement, setWatchElement] = useState<Element | null>(null);
  const [useBindValue] = bind(
    watchSelector<Element>(selector, context, interval, startWithInterval), // Obervable<HTMLElement>
  );

  const watchSelectorValue = useBindValue();
  useEffect(() => setWatchElement(watchSelectorValue), [watchSelectorValue]);

  return watchElement;
}

Is @react-rxjs/[email protected] a significant breaking change from 0.5.0?

Upgrading to @react-rxjs/[email protected] I can't get the most basic PoC to work.

import React from "react";
import { bind, shareLatest } from "@react-rxjs/core";
import { timer } from "rxjs";
import { startWith } from 'rxjs/operators';

const [useTimer] = bind(() => timer(0, 1000).pipe(startWith(0), shareLatest()));

export default function App() {
  const timer = useTimer();
  return <div className="App">{timer}</div>;
}
Error
Missing subscription

Code

If this is due to breaking changes, a suggestion would be to expand this exception message to give more detail. Plus, to add a demo package with a few working examples to provide end users with a sanity check.

If it is a regression, perhaps there can be some integration tests added.

Proposal to support event handlers

React's basic data flow is, roughly:

User interaction --(A)--> State change --(B)--> View update

Currently, react-rxjs only handles part B. This proposal discusses part A.


When using React and RxJS, there are two ways of handling events.

  1. Use regular React event handlers, forgoing the benefits of Observables
  2. Use fromEvent directly on DOM elements, working outside React's event system

Neither is clearly desirable. Instead, what if react-rxjs provided a helper that creates event handlers using RxJS operators?

Here is a tentative type signature for such a helper:

function useHandler<E = React.Event>(
  factory: (event$: Observable<E>) => Observable<T>,
  dependencyArray: unknown[],
): [(event: E) => void, Observable<T>]

E.g.

import {useHandler} from '@react-rxjs/utils'

const MyComponent = () => {
  const [handler, observable$] = useHandler<React.MouseEvent>(
    // This callback creates an observable
    // When the component mounts, the observable is subscribed automatically.
    // When the component unmounts, the observable is unsubscribed.
    (event$) => event$.pipe(/* ... */),
    [/* dependency array */]
  )

  // Here, we can compose observable$ to create another observable

  return (
    <button onClick={handler}>
      Click me
    </button>
  )
}

(We could flesh out the details of useHandler() later)

This would provide a better story for React devs who want to use RxJS (or vice versa).

import error when using Subscribe

../node_modules/@react-rxjs/utils/dist/utils.es2017.mjs
Attempted import error: 'SUSPENSE' is not exported from '@react-rxjs/core'.

new util: newInput

Admittedly, creating input streams is a lot of boilerplate. I remember that @rikoe mentioned it once and I agree: it's quite tedious. So, I'm thinking about adding the following util:

const newInput = <A extends unknown[], T>(                  
  mapper: (...args: A) => T,                                   
): [Observable<T>, (...args: A) => void] => {                  
  const subject = new Subject<T>()                             
  const onEvent = (...args: A) => subject.next(mapper(...args))
  return [subject.asObservable(), onEvent]                     
}                                                              

either that, or:

const newInput = <A extends unknown[], T>(                  
  mapper: (...args: A) => T,                                   
): [Subject<T>, (...args: A) => void] => {                  
  const subject = new Subject<T>()                             
  const onEvent = (...args: A) => subject.next(mapper(...args))
  return [subject, onEvent]                     
}                                                              

Should we add it? And if we do, which one do you prefer: the one that exposes the Subject or the one that just exposes an Observable?

An example:

before:

const add$ = new Subject<string>()                                
const onAdd = (text: string) => text && userAdd$.next(text)

const edit$ = new Subject<{ id: number; text: string }>()
const onEdit = (id: number, text: string) => userEdit$.next({ id, text })                               

after:

const [add$, onAdd] = newInput((text: string) => text)
const [edit$, onEdit] = newInput((id: number, text: string) => ({ id, text }))

Typescript integration issues

Hi @josepot , how you doing? I hope you are doing great πŸ˜„

This is somewhat related to #228.

While doing minor release updates:
Screen Shot 2021-11-15 at 7 50 26 PM

My Typescript app broke:
Screen Shot 2021-11-15 at 7 50 54 PM

Apparently Typescript does not support mjs yet so webpack customization is required in this case: formatjs/formatjs#1395

Was that intentional? I.e. the fact that the library now breaks with Typescript?

Ideas, suggestions on how to bypass that? I think writing a custom rule saying mjs is still js is the most promising solution: formatjs/formatjs#1395 (comment)

Have a good one,

React-RxJS release roadmap

These are the things that I think should happen before we publish the first stable release:

  • Split the library between core, utils and dom.

    • @react-rxjs/core should have everything that react-rxjs already has except for the utils section
    • @react-rxjs/utils I would open a separate issue to discuss what we should put in there, but in a nutshell it will have the APIs that are currently under the utils section plus some useful operators from @josepot/rxjs-utils (like groupInMap, mergeWithKey, recursiveObservable... We will see)
    • @react-rxjs/dom will expose an operator named batchUpdates that won't be necessary with React 17. It's basically a observeOn(asapScheduler) that does the next on the observer wrapped on a ReactDom unstable_batchedupdates.
  • Docs site. I'm the owner of the domain react-rxjs.org and I would like to have a site very similar to the one that recoil has. These are the sections that I would like to have in the initial docs site:

    • Motivation: The main idea of this section would be to explain why it's so limiting and frustrating to use React as a state-management library, the main reason is that it's not Reactive. Then we would explain why it's so desirable to use a truly reactive state-management system with React: code-navigability, avoid indirection, automatic code-splitting, being able to leverage React-Suspense and React-Error-Boundaries and the coming features of React Concurrent Mode, while having top-notch performance because with a forward-referenced based architecture we would only trigger the strictly necessary re-renders.

    • Core concepts: In this section I would explain that thanks to the great improvements that React has made during these last years, it's now possible to have a set of RxJS bindings for React that address the impedance mismatch that happens when trying to use RxJS directly in React. It does so by making streams first-class citizens through hooks that leverage React Suspense and React Error-Boundaries. It also provides a means of multicasting the streams that are being used by React in a way that avoids memory-leaks, that makes composition easier while also avoids premature unsubscriptions because a component gets mounted/unmoutned. In this section why the "shareLatest" operator is a key and core piece for coveting this impedance mismatch.

    • Installation: It would be almost identical to the installation section of Recoil

    • Getting Started: We have to think of an example that showcases the power of the library, that's easy to understand for RxJS newcomers and that's not too trivial... I don't think that we would want to use the same example that they use in the Getting Started section of Recoil. I think that example makes sense for their library, but not so much for ours... I'm still not sure.

    • TODOs App: I mean, I guess that it's almost mandatory to show an example of how to build the freaking todos app... That's what everybody does, and most developers have seen it implemented in so many different ways that I guess that we should have it.

    • Real-life example: Let's not be shy, let's use this https://github.com/josepot/rtk-github-issues-example/pull/1/files It's a great example to explain how this library integrates so nicely with React Suspense and ErrorBoundaries, while making code-splitting a walk in the park.

    • API Reference: Move and improve the API reference that we have in the README of the Repo to the docs website.

cc: @voliva @bhavesh-desai-scratch @rikoe @egillh210 thoughts?

Testing a signal

Overview

I'm trying to test a signal which is exported from a JS module. The problem is that between tests it's changed and the tests become interdependent.

Recreation

export const [launchedPressed$, pressLaunch] = createSignal<Launch>();

Which has the following binding

export const [useLaunchRocket, launchRocket$] = bind(
  launchedPressed$.pipe(
    switchMap(({ delaySeconds, url, status, timeoutSeconds }) => {
      return status === LaunchStatus.WORKING
        ? of(LaunchStatus.ABORTED)
        : ajax.getJSON(`${url}${delaySeconds}`).pipe(
            map(() => LaunchStatus.STANDBY),
            timeoutWhen(!!timeoutSeconds, (timeoutSeconds ?? 10) * 1000),
            catchError(() => of(LaunchStatus.ABORTED)),
          );
    }),
    map((status) => ({ status })),
    startWith({ status: LaunchStatus.STANDBY }),
  ),
);

Because the signal is exported from this module it's akin to a global variable and the value is changed between tests within the test runner. This makes the values less predictable and the tests more flaky and interdependent.

it("should emit aborted when status is working and pressLaunch is called", () => {
    testScheduler.run(async (helpers) => {
      const { cold, expectObservable } = helpers;
      const payloadA: { a: Launch } = {
        a: {
          delaySeconds: 1,
          status: "working",
          url: "www.google.com",
          timeoutSeconds: 2,
        },
      };
      const expectedA = { a: { status: "standby" }, b: { status: "aborted" } };

      const source$ = cold("a", payloadA).pipe(tap((a) => pressLaunch(a)));

      expectObservable(source$).toBe("a", payloadA);
      expectObservable(launchRocket$).toBe("(ab)", expectedA);
    });
  });

  it("should abort when the delay exceeds the timeout", () => {
    (ajax.getJSON as jest.Mock).mockImplementation(() => of("test").pipe(delay(3000)));

    testScheduler.run(async (helpers) => {
      const { cold, expectObservable } = helpers;
      const payloadA: { a: Launch } = {
        a: {
          delaySeconds: 3,
          status: "standby",
          url: "www.google.com",
          timeoutSeconds: 2,
        },
      };
      const expectedA = { a: { status: "standby" }, b: { status: "aborted" } };

      const source$ = cold("a", payloadA).pipe(tap((a) => pressLaunch(a)));

      expectObservable(source$).toBe("a", payloadA);
      expectObservable(launchRocket$).toBe("a 2000ms b", expectedA);
    });
  })

Is my analysis correct?

What's the best approach to testing signals?

Are there any examples on the https://react-rxjs.org/ ?

Are there any projects that use React-RxJS?

Hi! I am impressed by your library. I'm looking to use React and RxJS in my project, and I want to learn by examples.

I checked the example repos under re-rxjs: react-rxjs-basic-todos, react-rxjs-github-issues-example, and eleccions-2021. But these were fairly basic CRUD projects. I'm looking for apps that use non-trivial observable chains, and that handle errors in a sensible way.

Are there any (hopefully large and mature) projects that use React-RxJS?

Edit: I forgot there was a Discussions tab--please move this issue over there if need be.

[BUG] bind is returning an array when an Object is passed through the observer.

If an object is send through an observer, and the observer is binded, the hook of the bind within React returns an array (as in Object.entries(obj)).

Expected: the actual object.

Example:

const [useObj] = bind(observer$.pipe(
    tap(console.log) // <-- returns an object as expected. e.g. { key: "value"} 
), {});

export default function useDependency(dependency) {
    const obj = Obj(); // <--- returns an array as if Object.entries(obj) has been executed. Expecting an object, but returns: [key, value].

Incompatible with tsx

getting start example works in jsx but I cannot convert it to work with tsx for the compiler reports issue with function bind overload. I think this is necessary for nowadays, react and typescript pair is widely use. I guess the problem is about js loose function declaration, but I can be wrong for my inexperience in the code so I would like you guys to consider. The error message is as follow:

Failed to compile
project/investing-2021/modules/asset-monitoring/src/components/CharacterCounter/index.tsx
TypeScript error in project/investing-2021/modules/asset-monitoring/src/components/CharacterCounter/index.tsx(7,26):
No overload matches this call.
  Overload 1 of 2, '(observable: Observable<void>, defaultValue?: void): [() => void, Observable<void>]', gave the following error.
    Argument of type 'string' is not assignable to parameter of type 'void'.
  Overload 2 of 2, '(getObservable: (...args: unknown[]) => Observable<string>, defaultValue?: string | ((...args: unknown[]) => string)): [(...args: unknown[]) => string, (...args: unknown[]) => Observable<string>]', gave the following error.
    Argument of type 'Observable<void>' is not assignable to parameter of type '(...args: unknown[]) => Observable<string>'.
      Type 'Observable<void>' provides no match for the signature '(...args: unknown[]): Observable<string>'.  TS2769

     5 | 
     6 | const [textChange$, setText] = createSignal();
  >  7 | const [useText, text$] = bind(textChange$, "")
       |                          ^
     8 | 
     9 | function TextInput() {
    10 |   const text = useText()

Move suspense utilities to utils package?

While writing the documentation I'm wondering about the suspense utility functions (suspend, suspended and switchMapSuspended) if they should be moved to @react-rxjs/utils instead of core.

I remember we discussed this and I think I was in favour of keeping them into @react-rxjs/core. However, now I see that maybe core should only export functions/values which can't be replaced externally (as the case of SUSPEND - that's a core part of react-rxjs). But these operators are basically sugar to have something more declarative and ready-to-use.

Observable emitting error should not cause infinite loop on Subscribe

Under certain circumstances, a <Subscribe> with a nested component that uses a hook returned from bind(), where the observable emits an error, causes an infinite loop.

In the code example below, executable here, uncommenting the throw() on line 6 results in an infinite loop with the error being swallowed. Per discussion with @voliva who investigated this further, it seems that if and only if all of these conditions apply:

  1. The bind doesn't have a default value
  2. The shared observable is not passed to a <Subscribe>
  3. The observable emits the error asynchronously
  4. The observable doesn't emit any value before emitting the error (promises can either emit a value or error)

then the error is swallowed, and the component retries getting the value again

import { defer } from "rxjs";
import { bind, Subscribe } from "@react-rxjs/core";

async function mockFetch() {
  console.log("mockFetch()");
  // throw new Error("ouch");
  return new Promise((resolve) =>
    setTimeout(() => {
      resolve(42);
    }, 1000)
  );
}

const [useSomeData] = bind(defer(mockFetch));

function SomeData() {
  const data = useSomeData();
  return <>{data}</>;
}
export default function App() {
  return (
    <Subscribe fallback="Loading...">
      <SomeData />
    </Subscribe>
  );
}

Packaging for React Native

Got another packaging issue for you guys. Using expo and npm:

warn Package @react-rxjs/core has been ignored because it contains invalid configuration. Reason: Package subpath './package.json' is not defined by "exports" in /Users/jim/proj/MDT/node_modules/@react-rxjs/core/package.json

Use TS Doc on public API

Use JSDoc for public API methods. Great for discoverability with editors that support the syntax

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    πŸ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. πŸ“ŠπŸ“ˆπŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❀️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.