npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

r2s

v0.3.4

Published

A Rxjs state management library for React

Downloads

10

Readme

r2s

Based on the work of Michal Zalecki. I updated the code to support the React 16 and Rxjs 6 and made a few changes to make easier to create the store.

The name stands for Reactive React Store. This package provides an easy to implement and very powerful redux like state management using Rxjs operators and pipes.

Getting started

  • Install packages:
npm install rxjs r2s

Create the actions:

//store/counter/actions.js
import { createActions } from 'r2s'

const counterActions = createActions(['increase', 'decrease', 'reset'])
export default counterActions

The createActions functions returns an object with new Subjects. So it's the same thing as:

//store/counter/actions.js
import { Subject } from "rxjs";

const counterActions = {
	increase: new Subject(),
	decrease: new Subject(),
	reset: new Subject()
}

export default counterActions

Create Reducers

//store/counter/reducer.js
import { merge } from 'rxjs';
import { map } from 'rxjs/operators';

import counterActions from './actions';

import {createReducers} from '@/r2s'

const initialState = 0;

const reducers$ = merge (
	counterActions.increase.pipe(map(() => state => state + 1)),
	counterActions.decrease.pipe(map(() => state => state - 1)),
	counterActions.reset.pipe(map(() => () => initialState))
)

export default createReducers(initialState, reducers$);

The createReducers function is an helper that creates an Observable subscribed by the exported Behavior Subject. It needs to be this way if there's a chance that an action will be called before the first component subscribes. The Following code is equivalent to the above:

//store/counter/reducer.js
import { BehaviorSubject, of } from 'rxjs';
import { map, merge, mergeScan } from 'rxjs/operators';

import counterActions from './actions';

const reducers$ = merge (
	counterActions.increase.pipe(map(() => state => state + 1)),
	counterActions.decrease.pipe(map(() => state => state - 1)),
	counterActions.reset.pipe(map(() => () => initialState))
)

const initialState = 0;

const observable = of(() => initialState)
.pipe(
	merge(
		reducers$
	),
	mergeScan((state, reducer) => {
		return of(reducer(state))
	}, initialState),
)

const subject = new BehaviorSubject(initialState)
observable.subscribe(a => subject.next(a))

export default subject

What we do is create a new observable from the initial state with of and merge all streams into a single Observable. Each stream's content is mapped to a function that receives the current state so we can use it to return the new one. This function is where the Reducers pure functions act, always returning a new state.

If our Subject had a payload, it would be the first argument of the function, as an example:

counterActions.setValue.pipe(map(payload => () => payload))
//or
counterActions.addValue.pipe(map(payload => state => state + payload))

Create a component and subscribe to the Observable

//Counter.js
import React from 'react';
import {subscribe} from 'r2s';
import counterActions from './store/counter/actions'
import counter$ from './store/counter/reducers'

function Counter(props) {
  return (
    <div>
      <h1>{props.counter}</h1>
      <button onClick={props.increase}>Increase</button>
      <button onClick={props.decrease}>Decrease</button>
      <button onClick={props.reset}>Reset</button>
    </div>
  )
}

	const observables = {
    counter: counter$
  }


export default subscribe(observables, counterActions)(Counter)

The first argument of the subscribe function is an object which the properties are the names passed down as Props to the component. The second argument is an actions object (with Subjects as properties values) which will be passed as props functions. And the Third object is for passing anything else you want as props.

The subscribe can receive n actions objects as arguments, as follows:

export default subscribe(
	({counter, user}) =>  ({counter, user}),
	{...userActions, ...counterActions},
	{otherProp: 'foo'}
)(Counter)

Please note the you are not required to use the actions on the connect, the following works too:

//Counter.js
import React from 'react';
import {subscribe} from 'r2s';
import counterActions from './store/counter/actions'

function Counter({counter, increase, decrease, reset}) {
  return (
    <div>
      <h1>{this.props.counter}</h1>
      <button onClick={() => counterActions.increase.next()}>Increase</button>
      <button onClick={() => counterActions.decrease.next()}>Decrease</button>
      <button onClick={() => counterActions.reset.next()}>Reset</button>>
    </div>
  )
}

export default subscribe(({counter}) =>  ({counter}))(Counter)

Async Actions

For the async actions, while the code can be added anywhere between the Subject and the last Reducer Map, I found it easier to maintain in a middle file, similar to actions itself. I called it effects for my experience with Ngrx

So an effects file would be:

//store/counter/effects.js
import authActions from "./auth.actions";
import { from, of} from "rxjs";
import { switchMap, map, catchError} from 'rxjs/operators';

export default {
	login:authActions.login
	.pipe(
		switchMap(payload => {
			return from(axios.post('/api/login', payload))
				.pipe(
					map(response => ({user:response.user, status:'fetched'})),
					catchError(err => of({error:err, status:'error'}))
				)
	    })
	)
}

So, we take the action Subject, use the switchMap operator to map it to new Observable. This new observable is created with from, which creates an Observable from an Observable-like object, such as the axios promise. Then we map the response from the promise to our data, and catch it with catchError if it rejects.

One import thing to keep in mind is that observables complete once they have an error, so if the inner pipe operators were placed in the outer one, we would not be able to retry the login action, because our Subject would be complete and stop emitting new data down the stream.

This is the code for such case, and would not work after the first login fail:

//store/counter/effects.js

// THIS WILL NOT WORK PROPERLY
export default {
  login: authActions.login
  .pipe(
    switchMap(payload => from(axios.post('/api/login', payload))),
    map(response => ({user:response.user, status:'fetched'})),
    catchError(err => of({error:err, status:'error'}))    
  )
 }

After that we use our effects observable in the reducer file:

//store/counter/reducer.js
import authActions from "./actions";
import authEffects from "./effects";

const initialState = {user: {}, status:'started', error:null};

const AuthReducer$ = of(state => ({...initialState, ...state}))
  .pipe(
    merge(      
	  // first the action itself, so we let the app know we are fetching data and show a loader
      authActions.login.pipe(map(() => state => ({...state, status:'fetching', error:null}))),
      // and the result from our effects
      authEffects.login.pipe(map(payload => state => ({...state, ...payload})))
    )
)

Actions Side Effects

The Tap operator makes easy to handle side effects within the application.

After a successfully login we can redirect the user to the dashboard. Creating a history object with history, we can use it along the tap operator in the login effect pipe:

create a history.js:

//history.js
import createHistory from "history/createBrowserHistory"

export default createHistory();

auth/effects.js:

//store/auth/effects.js
import authActions from "./auth.actions";
import { from, of} from "rxjs";
import { switchMap, tap, map, catchError} from 'rxjs/operators'; // <- add tap

import history from '../../history'

export default {
	login:authActions.login
	.pipe(
		switchMap(payload => {
			return from(axios.post('/api/login', payload))
				.pipe(
					map(response => ({user:response.user, status:'fetched'})),
					catchError(err => of({error:err, status:'error'}))
				)
	    }),
	    // NEW CODE
	    tap(payload => {
          if(!payload.error) {
            history.push('/dashboard')
          }
        })
	)
}

or dispatch an action:

import anotherActions from '../another/actions'
...
tap(payload =>  {  if(!payload.error)  { anotherActions.doStuff.next(payload)  }  })

A diagram of the entire flux

diagram

Of course it is just an example, you can add any operator to the pipes and change the data as you need.