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 🙏

© 2026 – Pkg Stats / Ryan Hefner

redux-lenses

v1.1.1

Published

Abstractions for interacting with Redux state by using Ramda lenses.

Downloads

2

Readme

redux-lenses v1

Abstractions that use Ramda lenses to interact with the Redux store.

Motivation

Redux is an excellent tool for state management, but using the standard pattern of action creator to reducer is tedious to do for each state variable. Often people fall back to using React state. Redux Lenses is an attempt to make Redux state management easier than React state management by using Ramda lenses to mutate the Redux state.

Install

npm install --save redux-lenses

React Example

This is the authentication example from the React Router docs, with React state changed to Redux state via Redux Lenses.

...
import authLenses from '../lenses';
import { bindLenses } from 'redux-lenses';
import { connect } from 'react-redux';


class Login extends React.Component {

  componentWillMount() {
    this.props.redirectLoginToReferrer.set(false);
  }

  login = () => {
    fakeAuth.authenticate(() => {
      this.props.redirectLoginToReferrer.set(true);
    })
  }

  render() {
    const { from } = this.props.location.state || { from: { pathname: '/' } }

    if (this.props.redirectLoginToReferrer.view()) {
      return (
        <Redirect to={from}/>
      )
    }

    return (
      <div>
        <p>You must log in to view the page at {from.pathname}</p>
        <button onClick={this.login}>Log in</button>
      </div>
    )
  }
}


export default connect(
  authLenses.connect(['redirectLoginToReferrer']),
  null,
  bindLenses
)(Login);

Piece-By-Piece

Add lens reducer

redux-thunk is required for using the request method

import { createStore, applyMiddleware } from 'redux';
import { lensReducer } from 'redux-lenses';
import thunkMiddleware from 'redux-thunk';

const store = createStore(
  lensReducer(),
  initialState,
  applyMiddleware(thunkMiddleware)
);

To use Redux Lenses with another reducer, pass that reducer to lensReducer.

...

const reducer = combineReducers({ auth, appLayout });

const store = createStore(
  lensReducer( reducer ),
  initialState,
  applyMiddleware(thunkMiddleware)
);

One area of state, such as auth, can be altered via the auth sub-reducer or Redux Lenses.

Create a Lens Group

// auth/lenses.js

import { LensGroup } from 'redux-lenses';
import R from 'ramda';

export default new LensGroup(
  { basePath: ['auth']
  , lenses:
    { loginRequest:
      { path: ['requests', 'login']
      , map: R.defaultTo({})
      }
    , user: {}
    , redirectLoginToReferrer: R.defaultTo(false)
    }
  }
);

The object passed to the LensGroup constructor accepts basePath and lenses. Let's start with loginRequest. The lens config object accepts path and map properties. The basePath of the lens group is prepended to this path, so the path of loginRequest within state is ['auth', 'requests', 'login']. The map function gives you a chance to map the value after retrieving the value from state. This is how you declare default values, use constructors, or alter the value however you'd like.

Now let's look at user. We don't want to specify a path or map function for user. The path of user in state will be ['auth', 'user'];

Instead of passing an object for lens configuration, you can just pass the map function. That's how redirectLoginToReferrer is configured. Because the path isn't spedified, it will be ['auth', 'redirectLoginToReferrer'];

Connect to Component

Use view() to get the current value. Use set(value) or set(value => nextValue) to set the value.

// I didn't show the creation of the layout lenses.
import layoutLenses from '../lenses';
import authLenses from '../../auth/lenses';
import { bindLensesAndActionCreators } from 'redux-lenses';
import { connect } from 'react-redux';
import { logout } from '../../auth/actions';


class AppLayout extends React.Component {
  componentWillMount() {
    this.props.drawerOpen.set(false);
  }
  render() {
    const { props } = this;
    const user = props.user.view();

    return (
      <div>
        <AppBar>
          {!!user &&
          <DrawerToggleButton onClick={() => props.drawerOpen.set(x => !x)} />}
          <LogoutButton onClick={props.logout} />
        </AppBar>
        <Drawer
          onRequestClose={() => props.drawerOpen.set(false)}
          open={props.drawerOpen.view()}
        />
      </div>
    );
  }
}

function mapStateToProps(state) {
  return {
    ...layoutLenses.connect(['drawerOpen'])(state),
    ...authLenses.connect(['user'])(state)
  };
}

export default connect(
  mapStateToProps
  null,
  bindLensesAndActionCreators({ logout })
)(AppLayout);

LensGroup.connect takes a list of lens IDs and returns a function which in turn accepts the state object. When you call that fn with state, you get back and object of Connected Lenses. These Connected Lenses don't yet have access to dispatch. Use bindLenses or bindLensesAndActionCreators to give dispatch to the Connected Lenses. Your mapDispatchToProps function has to put dispatch on props. This happens when you pass null, or alternatively you could explicitly put dispatch on props.

import { bindActionCreators } from 'redux';

export default connect(
  mapStateToProps,
  dispatch => ({ dispatch, ...bindActionCreators({ logout }, dispatch) }),
  bindLenses
)(AppLayout);

Action Shape

The redux actions that get created when you call "set" have information about the lens that you can use in debugging

{ type: 'SET__drawerOpen'
, path: [ 'auth', 'drawerOpen' ]
, value
, ...
}

In Action Creators

// auth/actions.js

...
import authLenses from './lenses';


export function setUser(user) {
  return authLenses.set({ user, redirectLoginToReferrer: !!user }));
}


export function login(credentials) {
  return dispatch => {
    const promise = authService.login(credentials).then(user => {
      dispatch(setUser(user));
    });
    const loginRq = authLenses.get('loginRequest');
    return dispatch(loginRq.request(promise));
  }
}


export function logout() {
  return (dispatch, getState) => {
    const { user } = authLenses.view(['user'], getState());
    alert(`Goodbye ${user.name}`);

    return dispatch(authLenses.get('logoutRequest').request(
      authService.logout().then(() => dispatch(setUser()))
    ));
  }
}

Async Requests

Redux-lenses offers a method called 'request' for managing the state around async requests, The request method accepts a promise as it's only argument. It helps to use an empty object as the default when you create the lens. See the above code for an example.

Request tracks the state of an async request. There's no need to catch the errors. Errors and results are captured in state.

// right after the request method is called, the state is set to:
{ inProgress: true, completed: false }

// if the promise is resolved, the state is set to:
{ inProgress: false, completed: true, result }

// if the promise is rejected, the error is caught, and state is set to:
{ inProgress: false, completed: true, error }

Then in your components, it's trivial to show results and errors. Here, ErrorText is a component that will only show a message if error has a value.

function LoginForm(props) {
  return (
    <form>
      <input name="email" />
      <input name="password" type="password" />
      <button onClick={props.login}>Login</button>
      <ErrorText error={props.loginRequest.view().error} />
    </div>
  );
}

Computed Props and Reselect

Reselect is the recommended way for deriving computed props from your Redux state. The EnhancedLens.view method is what Reselect refers to as an input-selector. Here is a reselect example rewritten with Redux Lenses.

import { createSelector } from 'reselect'
import todoLenses from '../lenses';

export const getVisibleTodos = createSelector(
  [ todoLenses.get('visibilityFilter').view, todoLenses.get('todos').view ],
  (visibilityFilter, todos) => {
    switch (visibilityFilter) {
      case 'SHOW_ALL':
        return todos
      case 'SHOW_COMPLETED':
        return todos.filter(t => t.completed)
      case 'SHOW_ACTIVE':
        return todos.filter(t => !t.completed)
    }
  }
)

Framework compatibility

Redux Lenses, like Redux, isn't specific to React. Redux Lenses should work anywhere Redux works. The included bindLenses function is built to match the API of React-Redux's mergeProps function. Redux Lenses might not be compatible with the bindings for other frameworks, so you may have to write custom connect code to connect Redux Lenses with the components of frameworks other than React.

API

LensGroup class

get :: String -> EnhancedLens

authLenses.get('user')

pick :: [ key:String ] -> { key: EnhancedLens }

authLenses.pick(['user', 'loginRequest'])

set :: { key: value|updateFunction } -> Redux Action

dispatch( authLenses.set({ user }));
dispatch( authLenses.set({ clickCount: x => x + 1 }));

view :: [ key:String ] -> state:Object -> { key: value }

authLenses.view(['user'], state)
// { user: { name: 'Bob' } }

viewAll :: state:Object -> { key: value }

Same as view but it gives you values for all the lenses in the group.

connect :: [ key:String ] -> state:Object -> { key: ConnectedLens }

Returns an object of Connected Lenses. Before you can dispatch actions from the Connected Lens, it needs access to the dispatch function via ConnectedLens.setDispatch(dispatch). This is what happens inside of the bindLenses function in the examples.

EnhancedLens class

Class for interacting with one lens that hasn't yet been connected. Mostly this is inside of action creators.

view :: Object -> Any

const userEnhancedLens = authLenses.get('user');
const user = userEnhancedLens.view(state);

set :: value|updateFunction -> Redux Action

const userEnhancedLens = authLenses.get('user');
dispatch(userEnhancedLens.set(user));

request :: Promise -> ReduxThunkFunction(Action)

The result of dispatching the thunk action is the result of the promise. If there is an error, the error is caught, and the result is { error };

const promiseResult = dispatch(
  authLenses.get('loginRequest').request(loginPromise)
);

resetRequest :: _ -> Redux Action

Resets request state to:

{ inProgress: false, completed: false }

applyMap :: value -> mappedValue

This allows you to transform a value via the EnhancedLens's map function, which you specify when creating the LensGroup. This is used by the ConnectedLens and not something you'll likely ever use.

ConnectedLens class

Very similar API as EnhancedLens, except for connected lenses. This is the API inside of your components.

view :: _ -> Any

const user = props.user.view()

set :: value|updateFunction -> (dispatches action to set state)

onClick: () => props.modalIsOpen.set(true)
onClick: () => props.modalIsOpen.set(x => !x)

papp :: value|updateFunction -> _ -> (dispatches action to set state)

Papp stands for "partially apply". It's an alternative to creating anonymous functions.

onClick: props.modalIsOpen.papp(true)
onClick: props.modalIsOpen.papp(x => !x)

request :: Promise -> (dispatches actions to set state)

Will set state twice, once initially, and once when the promise resolves or rejects. Request does not dispatch the promise. In this example, we're assuming login was already bound to dispatch.

props.loginRequest.request( login(credentials) );

resetRequest :: _ -> (dispatches action to set state)

Resets request state to:

{ inProgress: false, completed: false }
onClick: () => props.loginRequest.resetRequest()