React state management with Lenses

We're going to take a look at Lenses and how we use them in our UIs at Football Radar to keep our state immutable and improve how we manage it.


I'm going to assume knowledge of React & Redux/Flux.

Immutability in React is important for controlling the renders of your components, often via implementing a PureComponent. If you’re not familiar with this construct I recommend reading the documentation on before reading on;

Lenses are used for “focusing” in on a particular piece of a complex data whilst remaining in the context of that data. An acronym with types:

function setSomeDeepProperty :: (data: T, lensToSomeDeepProp: Lens) => T;  
function updateSomeOtherDeepProperty :: (data: T, lensToSomeDeepProp: Lens) => T;  

For more detail, I recommend the excellent series "Thinking in Ramda";
A more indepth explaination of lenses can be found here;


  1. Introduce our state and the RequestLifecycle object.
  2. Write some utility functions for RequestLifecycle using Lenses.
  3. Use these Lenses in our reducer.
  4. "Computed properties" after our reducer.
  5. Disadvantages (lazy data structure, learning curve).
  6. Conclusion.

Introducing State & RequestLifecycle

The scenario we're modelling here is a small application which lists & assigns users to watch recorded games of football.

type State = {  
    requests: {
        //GET /games
        getGames: RequestLifeCycle<Game[]>;
        //POST /watcher {gameId, watcherId}
        postWatcher: RequestLifeCycle<Watcher>;
    games: Game[];
    views: {
        myGames: Game[]

interface Game {  
    id: number;
    homeTeam: Team;
    awayTeam: Team;
    watchers: Watcher[];

interface Watcher {  
   watchedAt: Date;
   ...others, probably

A Watcher represents the instance of a user watching the game. In reality they are watching this game to record all the data necessary for our analysis. Multiple users can watch a single game, if the game is recorded the watchedAt date may differ between each one of them.

We have the following scenario: when the postWatcher request succeeds we want to add that watcher to the game.watchers array. We must do this whilst treating our State as an immutable object.

interface RequestLifeCycle<T> {  
    request: SomeXHRObj;
    status: "NOT_ASKED" | "LOADING" | "SUCCESS" | "FAILURE";
    error?: Error;
    data?: T;

Introduction to RequestLifeCycle

RequestLifeCycle is a pattern we use often here at Football Radar. It's job is to describe the lifecycle of a HTTP request in our state. This is proved very useful for many little situations such as not allowing a modal to close whilst a related request is in flight, preventing double submits, knowing when to display network notifications, the list goes on. This is heavily inspired by

Making RequestLifecycle useful

In the Flux world we're going to be dispatching actions when our source request does something. We want these actions to result in our reducer returning a new State with an updated request such that (for example) state.requests.getGames !== prevState.requests.getGames.

const statusL = R.lensProp("status");  
const dataL = R.lensProp("data");  
const errorL = R.lensProp("error");  

We define a lens for each property we will want to update in some way (we can and will use deeper paths most of the time) . You'll notice we use the convention propL for the variable names, this is similar to jQuery's $element or Rx's stream$.

const setStatus = R.set(statusL);  
const setData = R.set(dataL);  
const setError = R.set(errorL);  
const resetError = setError(null);  

Next we take advantage of Ramda’s currying on R.set, a function which takes a lens and sets the given value the property which the lens points to. This function will return a new copy of the object with the new value assign to the property. This is key for our immutability and is our primary driver for using lens.

export const setToLoading = R.compose(  

export const setToSuccess = (payload, requestLifecycle) =>  R.compose(  

export const setToFailed = (error, requestLifecycle) =>  R.compose(  

Lastly we have our 3 public functions. Whilst this isn't a very complex example, we can see how lenses aid composition of functions over objects. They let us go as deep as we need to into the object and then come all the way back again.

Composition types: A ---------------> A ---------------> A  
Functions:         change A.1         change A.2.a     change A.3  

You'll notice that all three of these functions could be achieved by with a simple Object.assign. In this example, there is a strong argument for that, but once the data shape gets deeper this approach really starts to shine.

Usage in our reducer

Now we have our pure function updates for RequestLifecycle we can use them in our reducer:

const postWatcherL = R.lensPath(["requests", "postWatcher"]);

function reducer(state, action) {  

        return R.over(postWatcherL, RequestLifecycle.setToLoading, state);

        return R.over(postWatcherL, RequestLifecycle.setToFailed(action.payload), state);

        return R.compose(
            R.over(postWatcherL, RequestLifecycle.setToSuccess(action.payload)),
            ...//todo add the watcher to the game.watchers array


Let's look at a few individual LOC:

// :: (state: State) => RequestLifeCycle<Watcher> 
const postWatcherL = R.lensPath(["requests", "postWatcher"]);  

Same as our previous lens but we're looking deeper into the state object to reach our RequestLifecycle.

//R.over :: (lens: R.Lens<S, T>, updateT: (T => T), state: S) => S
return R.over(postWatcherL, RequestLifecycle.setToFailed(action.payload), state);  

This line here, at least for me, really illustrates the power of lenses. R.over is a great function: it takes a lens and an update function. This function will be passed the current value of the lens over the state and returns the new value. This change is then propagated upwards to ensure each level is a new object:

state               //shallow copy, new .requests (R.over)  
  .requests         //shallow copy, new .postWatcher (R.over)
    .postWatcher    //shallow copy, new .status & .data (setToSuccess)
      .status       //new value
      .data         //new value

state !== prevState  
state.requests !== prevState.requests  
state.requests.postWatcher !== prevState.requests.postWatcher  
state.requests.getGames === prevState.requests.getGames ===  

Our state is perfectly updated, with the minimal number of changes so our React renders can optimise. Another thing to note here is how this update function is completely independent of the shape of our state. This is another win in projects which you must maintain as it's less to learn when you inevitably come back to it.

Note, due to Ramda creating shallow copies we're actually achieving a basic level of structural sharing.

Handling the postWatcher response

We have a Game[] in our state which we'll assume has been populated already. When the postWatcher request has succeeded we need to add the watcher to the array in the game, such that[x].watchers[y] has a new reference at each level. The main difference in this example is that we're dealing with arrays ( game.watchers and

Let's extend the action handler above (I've extended action.payload for this example too):

    return R.compose(
        R.over(postWatcherL, RequestLifecycle.setToSuccess(action.payload)),
        R.over(R.lensPath(["games", action.payload.index, "watchers"]), R.append(action.payload.watcher))

We're creating a lens at the point we're using it depends on a dynamic index:

// :: (state: State) => Watcher[] 
R.lensPath(["games", action.payload.index, "watchers"])  

Once again we take advantage of Ramda's currying. This function now only takes an array and will add our watcher to it:

// :: (watchers: Watcher[]) => Watcher[]  
const appendWatcher = R.append(action.payload.watcher)  

As before, combining the two with R.over adds our watcher to our game whilst updating the full path in the state:

state               //shallow copy, new games prop (R.over)  
  games             //new array                    (R.over)
    targetGame(n)   //shallow copy                 (R.over)
      watchers      //new array                    (R.over)
         newWatcher //new object                   (action.payload)
      ...originals  //same watcher objects
  ...originalGames  //same game objects   

state !== prevState  
state.requests !== prevState.requests !==[n] !==[n][n].homeTeam|awayTeam ===[n].homeTeam|awayTeam[n].watchers !==[n].watchers  

Going a bit further with post-reducer updates

state.views.myGames is a subset of We can compute state.views.myGames only when has changed with a nice function taking advantage of lenses:

type PostReduceUpdate = {  
    lenses: R.Lens[]
    fn: (state: State, previousState: State) => State

const updateMyGames: PostReduceUpdate = {  
    lenses: [gamesL],
    fn: (state) => R.set(myGamesL,, state)

We're going to loop over updateMyGames.lenses and R.view them for both the oldState and the nextState, straight after our reducer has run. If the results are not referentially equivalent then updateMyGames.fn will be run.

function haveLensesChanged(nextState: State, previousState: State, lenses: R.Lens[]): boolean {  
    //foreach lens, pair up the next and previous values
    const stateToCheck = => [R.view(lens, nextState), R.view(lens, previousState)]);

    //foreach (next,prev) pair, equality compare them to see if they've changed
    const statesAreEqual =[next, prev]) => next === prev);

    //conclude if any of our pairs have changed
    return R.any(R.equals(false), statesAreEqual);

function runPostReduceUpdates(postReduceUpdates: PostReduceUpdate[], oldState: State, nextState: State): State {  
    return postReduceUpdates
         //filter out updates which do not need to run
        .filter({lenses}) => haveLensesChanged(nextState, oldState, lenses) === true)
        //run each update
        .reduce((state, {fn}) => fn(state), nextState);

//At the end of our reducer
nextState = runPostReduceUpdates([updateMyGames], state, nextState);  

So now we have a function which can check the data in any location in our State for changes and only run another function if the data at that location has changed. Lenses allow us to write this running function without any knowledge of the shape of State and defers that to the implementation of each PostReduceUpdate.

Disadvantages of lenses

You may have thought to yourself in a few of these examples "Well I could have easily done that with Object.assign or .slice(0)". Hopefully the other examples demonstrate when that approach doesn't make things easier but it does highlight the possibility of potentially over solving the problem.

Another issue which we glossed over is the shape of State itself. Lenses let us abstract away the shape of the data but if we're not disciplined we can end up with poorly shaped data because "don't worry, we'll just use a lens to get there". Shallow copying objects over and over again isn't free either so if you can mould the data you should definitely consider that first.

Lenses are also tricky to type in TypeScript (I can't speak for Flow). You're relying on strings for property names when creating them, something which TypeScript cannot (yet) help you with.

Lastly there is a learning curve. I look back at functions I wrote before using lenses and could see how I would do them again if that project had lenses (I'm not saying lenses always superior). This thought applies backwards; how do people who haven't used lenses understand your function which is heavily influenced by them? This is the same with any new piece of technology and requires careful rollout and knowledge sharing.

In this post we've only used lenses in our flux style reducer and that is also the case in our code bases in Football Radar. Once we're out of the reducer and in the React world we just read the state directly; this.state.views.myGames etc. This limitation and consistency has helped with the learning curve.


  1. We introduced our N-levels deep State.
  2. Acknowledged the need to treat our State as an immutable object (though we never technically made it immutable!).
  3. Shown how we can achieve this with lenses and functions such as R.over for updating deep in State
  4. Taken this a step further to inspect our State for relevant changes.

Additional resources

  1. have a great video on using lenses with React's setState;
  2. The "Thinking in Ramda" series has an introduction to lenses;
  3. In the same series there's a nice post on immutability and arrays;
  4. Further explanation of lenses in JavaScript;
  5. School of Haskell on lenses;
  6. Pure components and the need for immutability;

Want to work with Max and the team? We are currently on the lookout for a frontend engineer to join the team based in our London office - read more and apply here today.