A couple of words about Ramda for React and Redux
Tags: react, ramda, redux
Published: 2023-01-05 19:08:40

I have been wanting to try something from the world of functional programming for a while. While I haven't found a use for libraries like `Immutable.js` in solving my everyday tasks, `Ramda` has been very convenient for me.

## Ramda for managing application logic
The `pipe` operator, as well as `ifElse`, `cond`, `andThen`/`otherwise`, allow creating concise and expressive functions consisting of simpler parts.

For example, we need a function that would make a GET request to the server and retrieve an array of some data (users, products, sales, etc.) - let's call them `entities`. At the same time, it is necessary to check that the response status is equal to 200.

function GET:
``` typescript
async function fetchData(entity: string) {
  return await axios.get(`http://localhost:3002/${entity}`, { validateStatus: () => true });
} 
```

Then the complete function responsible for getting and checking data from the server will look like this:
``` typescript
export const fetchEntity = async <T>(entity: string): Promise<{ data: T[]; }> => {
  return R.pipe(
    R.always(entity),
    fetchData,
    R.andThen(
      R.ifElse(
        (data) => data.status !== 200,
        () => { throw new Error(`${entity} fetch error`); },
        R.pick(['data']),
      )),
    R.otherwise(() => { throw new Error(`${entity} fetch error`); }),
  )();
};
```
The last line is actually unnecessary since the `axios` request should not throw errors, but I added it here just in case and for demonstration purposes.

- The `R.pipe` function executes the functions passed as arguments to it in sequence.
- `R.always(entity)` is a function that does nothing but returns the variable entity (as a function, since a function is required). This is equivalent to `() => entity`.
- `fetchData` takes the response from the previous function, entity, as its argument and makes a `GET` request to the server.
- `R.andThen` and `R.otherwise` are equivalent to `.then` and `.catch`. In case an error occurs, `R.otherwise` calls a function that throws the error.
- `R.ifElse` takes 3 functions as arguments: the first function should return true or false, based on which the 2nd or 3rd function is executed. The `(data) => ...` notation means that the response from the previous function, fetchData, is taken as an argument.
- `R.pick(['data'])` is the last function in the list. It receives an object and returns the value for the specified key. In this case, it is data, which should contain the array we are interested in.

The advantages of such a function notation for me are:

Firstly, the function notation is very concise, and it can be easily understood at first glance what is happening here.

Secondly, it is relatively easy to expand by adding new functions anywhere among the arguments of `R.pipe`.

n this example, the advantages may not be so obvious. However, if the request requires more complex logic and a greater number of data manipulations, the use of the `pipe` function can be noticeably more preferable.


## Ramda for Redux Slices
The recommended tool when using Redux is the `@reduxjs/toolkit` library, which takes care of some routine actions to prepare for work.

The use of Redux Toolkit does indeed make the work easier. However, personally, I am not a fan of the approach to its use in the official `Redux` documentation. How `Immer` works. `Immer` wraps the state in a proxy, so it allows us to mutate the state as if immutability doesn't matter. All mutations will be intercepted at the proxy level and we will get a new `state` at the output, while the old one will not be changed in any way.

For example, if we need to change `fetchingStatus` to `pending` when requesting data from the server, it might look like this:
``` typescript
 builder.addCase(fetchInitialData.pending, (state, _action) => {
   state.fetchingStatus = 'Fetching'
})
```

What I don't like about this is that `Immer` is so hidden under the hood here that it's not immediately clear what's happening. It looks like a bad piece of code with mutations.

The fact that the official documentation uses the word `state` to refer to the argument instead of `draft` or `stateProxy` only exacerbates the situation.

So far, I have settled on the following approach:
- get the state from the proxy using `current` from the `@reduxjs/toolkit` library.
- based on this, I get a new state, which I return (i.e., I work with it "the old-fashioned way").

And `Ramda` helps me with this due to its hiddenness of data immutability and convenient tools for working with objects.

The above example looks like this:
``` typescript
builder.addCase(fetchInitialData.pending, (stateProxy, _action) => {
  const state = current(stateProxy)
  return R.set(R.lensProp('fetchingStatus'), 'Fetching', state)
})
```
It turned out to be more verbose than the previous version, but at the same time, more clean.

Here is an example of code when we need to load additional data to the existing one:

``` typescript
builder.addCase(fetchInitialData.fulfilled, (stateProxy, action) => {
  const state: AircompanyState = current(stateProxy)
  return R.pipe(
    R.always(state),
    R.mergeLeft(action.payload),
    R.set(R.lensProp('fetchingStatus'), 'Success')
  )()
})
```

In the future, I will try to find a broader application for Ramda and also try the `fp-ts` library, which seems to be similar but has better type support for working with `Typescript` (in some cases, unfortunately, as is needed with `Ramda`). Also, don't forget about `Immer`. For example, `useImmer` can be very useful when you need to edit deeply nested values of state.