reaxjs
TypeScript icon, indicating that this package has built-in type declarations

1.0.0 • Public • Published

ReaxJS

npm version

React + rxjs

Installation

Install using npm/yarn

npm install --save reaxjs

Basic example

Here is a small example using ReaxJS

import React from 'react';
 
import reax from 'reaxjs';
 
const Demo = reax({
  input: event => event.target.value
},
({input}) => ({
  output: input.pipe(
    map(x => x.toUpperCase()).
    startWith('')
  )
}),
(values, events) => (
  <div>
    <input onChange={events.input} />
    <span>{values.output}</span>
  </div>
));
 
ReactDOM.render(<Demo>, document.body);

Ok, lets break it down. The example above renders as an input and a span, where the span always shows the upper-case text from the input. This can be made into a pure component. This pure component takes a prop object with a values object and an events object. The values object has one property, output, while the events object has one function property, input.

//PureDemo.js
export default function PureDemo(props){
  return (
    <div>
      <input onChange={props.events.input} />
      <span>{props.values.output}</span>
    </div>
  );
}

This pure component can be wrapped with a stateful react component or Redux. Writing pure state-less components is a good habit. But we need to store the state someplace, and we can use rxjs to implicitly store the state:

function toUpperCase(inputObservable){
  return inputObservable.pipe(
    map(x => x.toUpperCase()).
    startWith('')
  );
}

This function takes an observable as parameter and returns an observable. It will upper-case the values it receives from the inputObservable, and it will ensure it always has a vaule by returning the empty string.

We can use reax to combine the pure component and the toUpperCase function.

const Component = reax(eventsToValues, observablesFactory, (values, events) => <PureDemo values={values} events={events} />);

The first parameter to reax is an object of functions, each function mapping from an event to a value. The events object used in the PureDemo component has the same keys as this object. When you call one of the functions on the events, then the corresponding function on eventsToValues will be called. These functions are used to convert react events to values. For the PureDemo component it should look like this:

const events = {
  'input': event => event.target.value,
  // add more events here with the format name: event => value
};

The second parameter is a factory function that produces an object of observables. It should have a parameter called for example events. The events parameter to the factory has the same keys as the events object we created earlier, but the values are observables, not functions. The stream of values the observable produces is whatever the function with the same name in the events object returns. The observablesFactory should return an object, where each property is an observable. Since we have already made the toUpperCase function (which, remember, returns an observable), we can define it like this:

function observablesFactory(events){
  return {
    output: toUpperCase(events.input),
    // add more observables here
  };
}

Each returned observable can use one or more events, and can use the props for initial values and parameters.

Now that we have events and the observablesFactory we can combine them with PureDemo using reax:

reax(events, observablesFactory, (values, events) => <PureDemo values={values} events={events} />);
 
//which is the same as
 
reax({
  'input': event => event.target.value
},
(events, props) => ({
  output: toUpperCase(events.input)
}),
(values, events) => (
  <div>
    <input onChange={events.input} />
    <span>{values.output}</span>
  </div>
));

reax can be partially applied by ommiting the last parameter (the component), so it can also be used as a decorator:

@reax(events, observablesFactory)
class PureDemo extends React.Component{
  render(){
    return (
      <div>
        <input onChange={this.props.events.input} />
        <span>{this.props.values.output}</span>
      </div>
    );
  }
}

Props

Props passed to the reaxed component will be forwarded to the pure component, and will be passed into the observablesFactory method as an observable. This way you can react to prop changes using RxJS. As an example, consider a component that can be incremented/decremented, with a prop to control how much to increment/decrement by:

function observablesFactory(events, props){
  return {
    sum: Rx.Observable.merge(
      events.increment,
      events.decrement
    ).pipe(
      withLatestFrom(props, (dir, {delta}) => dir*delta),
      scan((sum, delta) => sum+delta, 0)
    )
  };
}
 
//...
 
<Counter delta={1} />

If you are only interested in the initial props values (ie, they should never change), then you can use the third parameter:

function observablesFactory(events, props, initalProps){
  return {
    sum: Rx.Observable.merge(
      events.increment,
      events.decrement
    ).pipe(
      scan((sum, delta) => sum+delta, initialProps.initial)
    )
  }
}
 
//...
 
<Counter initial={0} />

TypeScript

ReaxJS is written in TypeScript, and works very well with type inference. Specify the input type of each eventMappings parameter (and optionally the Props, if you need them), and TypeScript will figure out the rest:

import * as React from 'react';
import * as Rx from 'rxjs/Rx';
import reax, {constant} from 'reaxjs';
 
export interface Props {
  readonly initalValue : number
}
 
export default reax({
  increment: constant(+1),
  decrement: constant(-1),
}, (events, props, initalProps : Props) => ({
  sum: Rx.Observable.merge(
    events.increment,
    events.decrement
  ).pipe(
    scan((sum, delta) => sum+delta, initalProps.initalValue)
  )
}), (values, events, props) => (
  <div>
    <button onClick={events.decrement}>-</button>
    <span>{values.sum}</span>
    <button onClick={events.increment}>+</button>
  </div>
));

In this example the constant(value) function is used. It lets you disregard the type of the event and set it to always return a constant value. This is useful for example for button clicks, where the contents of the event isn't useful. In the above example increment will always return +1 and decrement will always return -1. You can also use constant() without passing in any argument, in which case it will always return true.

Inspiration

This is based on my experience working with react, redux and react-most. There are, in my opinion, a few problems with them:

  • Redux is not async, so doing anything with network requires other libraries.
  • Redux stores everything in a global shared store, so having multiple similar components requires more work.
  • React-most doesn't use rxjs by default.
  • React-most pushes every event through the same observable, and uses stringly typed keys and switch statements, instead of the power of observables.

ReaxJS is my attempt to solve all of these issues.

Readme

Keywords

Package Sidebar

Install

npm i reaxjs

Weekly Downloads

3

Version

1.0.0

License

MIT

Unpacked Size

60.6 kB

Total Files

35

Last publish

Collaborators

  • mariusgundersen