@rxfx/react
TypeScript icon, indicating that this package has built-in type declarations

1.1.1 • Public • Published

𝗥𝘅𝑓𝑥 — @rxfx/react

An insanely good utility for readable React. Part of the 𝗥𝘅𝑓𝑥 family of libraries.

TL;DR useService is analgous to Apollo Client's useQuery hook, but for any async effect, not just GraphQL calls. You don't need to write explicit loading state variables, and the service allows for cancelation and error tracking.

// Apollo style — fetches automatically, deduping fetches
const { data, loading, error } = useQuery(GET_DATA);

// RxFx style — fetches when you call dataService.request())
const { state, isActive, currentError } = useService(dataService)

Note: See @rxfx/service for more on what a service is, which you will probably use with this library.

For What?

Let's face it - effect execution in React sucks. Between the React core team advising against using useEffect (link), strict mode executing your effects twice, and the inherent complexity of managing loading states, errors and cancelation, it's enough to make you want to throw up your hands. Until now.

The useService hook from @rxfx/react lets you define a Service (from @rxfx/service) that wraps any async function, and brings all of the features you tend to need with an async function, without having to code them yourself:

  • State tracking of effect results, errors, requests and all combinations
  • Loading indicators synced to the effect itself
  • Incremental progress notifications
  • Timeouts
  • Cancelation of in-flight effects
  • Concurrency control to skip new executions or cancel old

Compare any React invocation of a Promise-returning function with useEffect with a useService call and you'll see usually half of the code, with fewer edge cases, and an easy growth path to adding new features like cancelation, or progress reporting as the needs arise.

In short, an 𝗥𝘅𝑓𝑥 service provides a separation that keeps your components simple, and your async effects testable. And useService is the way to bring the service into your component.

What's inside?

useService

useService(service: Service) - helps consume a @rxfx/service by syncing its state and isActive Observables with React component state.

Example: CodeSandbox

// A service to count asynchronously
const asyncCounter = createService(
  "count",
  bus,
  () => after(1000),
  ({ isResponse }) => (count=0, event) => {
    return isResponse(event) ? count + 1 : count
  }
);

// A component using the service for state, activity tracking
export const Counter = () => {
  const { state: count, isActive } = useService(asyncCounter);
  const buttonLabel = isActive ? "⏳" : "Increment";

  return (
    <div>
      <h1> Count is {count} </h1>
      <button onClick={() => asyncCounter.request()}>
        {buttonLabel}
      </button>
    </div>
  );
};

Basic Usage

import { useService } from '@rxfx/react';
import { after } from '@rxfx/after';
import { createService } from '@rxfx/service';

// Create a service that logs after 1 second
const loggerService = createService('logger', (message: string) => {
  return after(1000, () =>console.log(message));
});

const queuedLogService = createQueueingService('logger', (message: string) => {
  return after(1000, () =>console.log(message));
});

function LoggerComponent() {
  const { request, isActive } = useService(loggerService);
  
  return (
    <div>
      <button onClick={() => request('Hello after 1 second')} disabled={isActive}>
        {isActive ? 'Logging...' : 'Log Message'}
      </button>
    </div>
  );
}

Unmount Behavior Options

Control what happens when your component unmounts:

function SafeLogger() {
  // Cancels current request when unmounting
  const { request } = useService(loggerService, { 
    unmount: 'cancelCurrent' 
  });
  
  // ...
}

function StrictLogger() {
  // Cancels both current and queued requests when unmounting
  const { request } = useService(queuedLogService, { 
    unmount: 'cancelCurrentAndQueued' 
  });
  
  // ...
}

useWhileMounted

A readable version of useEffect(fn, []) that works with RxJS Subscriptions and Observables as well as React style.

// Canceling any service calls on unmount
useWhileMounted(() => {
  return () => countService.cancelCurrent()
})

// React style, for compatibility
useWhileMounted(() => {
  console.log('mount')
  return () => console.log('unmounted')
})

// With an RxJS Observable
useWhileMounted(() => {
  return new Observable(() => {
    console.log('mount')
    return () => console.log('unmounted')
  })
})

// With an RxJS Subscription 
useWhileMounted(() => {
  console.log('mount')
  return new Subscription(() => console.log('unmounted'))
})

useSubject

Exposes each latest value of an RxJS BehaviorSubject to React, rerendering when it changes.

let i=0;
const countSubject = new BehaviorSubject(0);

function CounterButton() {
  const count = useSubject(countSubject)
  return <Button onClick={() => countSubject.next(i++)}>
    { count }
  <Button>
}

See this CodeSandbox for how useSubject can reduce component coupling, much like Signals.

useStableValue

Equivalent to `useMemo(producer, [])``. Makes the stability more readable.

function Wordle() {
  const wordList = useStableValue(() => createWordList())
  // ... render ...
}

useStableValue should only be used in cases when you need to close over something in React. Otherwise, prefer to use a static import:

// wordList.ts
export const wordList = createWordList();

// component
import { wordList } from './wordList'
function Wordle() {
   // ... render ...
}

useStableCallback

Equivalent to useCallback(producer, []). Makes the stability more readable.

const sendAnalytics = useStableCallback(() => sendPing());

useObservable

Exposes each latest value of an RxJS Observable to React, rerendering when it changes, subscribing on mount, and unsubscribing on unmount.

useMyMountEvent

Returns a stable Promise for when a component has mounted, suitable for passing down to child components.

Readme

Keywords

none

Package Sidebar

Install

npm i @rxfx/react

Weekly Downloads

23

Version

1.1.1

License

MIT

Unpacked Size

71.3 kB

Total Files

36

Last publish

Collaborators

  • deanius