About

reactjrx

reactjrx is a javascript library which provides a simple and efficient API for handling global state, flow control, and queries in React applications using RxJS. With a small footprint and scalability to suit any project size, it is a great alternative to other popular libraries such as Recoil, Redux, React Query, Zustand, etc.

There are two layers to this library, a lower level of pure binding between rxjs and react and a higher level layer to help you deal with state and queries.

Instead of having to rely on several separate libraries, you have all the essential tools in one package. You don't need the state management? No problem, tree shaking will make sure you don't embed extra code in your project. Just use what you need.

TLDNR;

  • If you want to handle global state visit state.

  • If you want to make queries or connect observables visit queries.

  • If you want a simple and quick way to interact with observables visit bindings.

Low level API: RxJS bindings

Before using the low level api you may want to ensure whether signals or queries are not better suited for your needs. Although there is nothing wrong with it (we actually use it under the hood for the higher level api) they generally don't provide the optimisation or convenient API you will find with signals or queries. Beside they can create confusion since they are quite far from the React philosophy

The low level API provide all the building blocks to bind rxjs to react and the higher API. For example let's assume you have a package that expose observables, you can bind them easily:

const interval$ = interval(100)

const App = () => {
  // observe the returned value of an observable
  const counter = useObserve(interval$)

  const [vote$, vote] = useObserveCallback()

  // subscribe to an observable. Similar to observe but
  // does not sync nor return the observable output with react.
  useSubscribe(
    () =>
      interval$.pipe(
        tap((value) => {
          console.log("counter update", value)
        })
      ),
    []
  )

  /**
   * Here we handle API request and its status on user click.
   * Note that queries are more appropriate for such things.
   * Again this is to show how the low level API can be used.
   */
  const { data, error, status } = useObserve(
    () =>
      vote$.pipe(
        switchMap(() =>
          merge(
            of({ error: undefined, status: "fetching" }),
            from(fetch("/api/vote", { method: "post" }))
          )
        ),
        map((response) => ({
          data: response.numberOfVotes,
          status: "success"
        })),
        catchError((error) => of({ data: undefined, error, status: "error" })),
        scan((acc, state) => ({ ...acc, ...state }), {
          data: undefined,
          error: undefined,
          status: "idle"
        })
      ),
    {
      defaultValue: { data: undefined, error: undefined, status: "idle" }
    },
    [vote$]
  )

  return (
    <>
      Counter {counter}
      <button onClick={() => vote()} disabled={status === "fetching"}>
        Vote!
      </button>
      <div>You voted {data ?? 0} times!</div>
    </>
  )
}

State management (signals)

Signals are a higher level API which lets you manage your global state and react to it:

const voteSignal = signal({
  default: 0,
  // key is optional but required if you want to persist your signal
  key: "vote"
})

const App = () => {
  const vote = useSignalValue(voteSignal)

  // persist and hydrate your signals easily
  const { isHydrated } = usePersistSignals({
    entries: [{ signal: voteSignal, version: 0 }]
  })

  // You can easily delay your app until your state is hydrated
  if (!isHydrated) return null

  return (
    <>
      <button
        onClick={() =>
          // setValue can take a single value or a function that returns
          // previous state
          voteSignal.setValue((previousVotes) => previousVotes + 1)
        }
      >
        Vote!
      </button>
      <div>You voted {vote} times!</div>
    </>
  )
}

Queries

Queries are a drop in replacement for https://tanstack.com/query and help you connect outside world with react. They should preferably be used instead of useObserve or other low level API since they provide a more convenient and optimised way of querying data.

const App = () => {
  const { data: users, status, error } = useQuery({
    queryKey: ["users"],
    queryFn: () => {
      // returns an observable that emit users
      // we can stop at the first result but we can
      // also keep listening to the observable and return
      // new value whenever they arrive
      return db.users$.pipe(first())
    }
  })

  const {
    mutate: addNewUser,
    status: addUserStatus,
    data: newUser
  } = useMutation({
    mutationFn: (user) => db.users.add(user)
  })

  return (
    <div>
      {status === "pending" && <div>Fetching users...</div>}
      {!!error && <div>An error occured while fetching users</div>}
      <div>You have {users?.length} users</div>
      <button
        onClick={() => {
          addNewUser({ id: "new id", name: "new user" })
        }}
      >
        add new user
      </button>
      {addUserStatus === "success" && (
        <div>New user {newUser.name} added with success</div>
      )}
    </div>
  )
}

Author

Maxime Bret (bret.maxime@gmail.com).

License

Open source and available under the MIT License.

Last updated