ch20-TheBoys-ramda-with-react

Before starting this section, make sure to go through the prerequisite where were mirror Heroes to Boys.

Why Functional Programming, why Ramda?

If we take a look at the recent proposals to EcmaScript, we will realize that FP is the way and that new JS features will closely resemble RamdaJS utilities. Check out So what's new in ES2025 for a premier. We believe that learning FP and Ramda today will build a future-proof JS skillset, at least for the next decade.
We have a React app with solid tests built by TDD at all levels and 100% code coverage. In the prerequisites we mirrored Heroes group of entities to Boys. Now we can try out daring refactors and see if things still work.
While the literature on FP is plenty, there is a lack of real world examples of using FP and Ramda, especially with modern React and TypeScript. We are hopeful that this section addresses that gap.

Functional Programming JS resources

Here are the resources that inspired this work. You do not have to be fluent in FP to benefit from this section, but if you choose to back-fill your knowledge at a later time, these resources are highly recommended.

Install the Ramda package

yarn add ramda ramda-adjunct
yarn add -D @types/ramda
In the following sections we will give practical examples of using Ramda, then apply the knowledge to 3 Boys components; BoyDetail.tsx, BoyList.tsx and Boys.tsx.

partial

Create any function with n arguments and wrap it with partial . Declare which args are pre-packaged, and the rest of the args will be waited for. In simple terms; if you have a function with five parameters, and you supply three of the arguments, you end up with a function that expects the last two.
// standalone example, copy paste anywhere
import { partial, partialObject, partialRight } from "ramda";
//// example 1
// create any function
const multiply = (a: number, b: number) => a * b;
// wrap it with partial, declare the pre-packaged args
const double = partial(multiply, [2]);
// the function waits for execution,
// executes upon the second arg being passed
double; // [λ]
double(3); // 6
//// example 2
// create any function
const greet = (
salutation: string,
title: string,
firstName: string,
lastName: string
) => salutation + ", " + title + " " + firstName + " " + lastName + "!";
// wrap it with partial, declare the pre-packaged args
const sayHello = partial(greet, ["Hello"]);
// the function waits for 3 arguments to be passed before executing
sayHello; // [λ]
sayHello("Ms", "Jane", "Jones"); // Hello, Ms Jane Jones!
// another variety, has 2 pre-packaged args
const sayHelloMs = partial(greet, ["Hello", "Ms"]);
// waits for 2 more args before executing
sayHelloMs; // // [λ]
sayHelloMs("Jane", "Jones"); // Hello, Ms Jane Jones!
Where can partial apply in the React world? Any event handler, where an anonymous function returns a named function with an argument. The below two are the same:
// src/boys/BoyDetail.tsx
const handleCancel = () => navigate("/boys");
const handleCancel = partial(navigate, ["/boys"]);
handleCancel will wait for the click event - onClick={handleCancel} - and navigate to /boys route.
Similar applications of partial are in Boys.tsx
// src/boys/Boys.tsx
import { partial } from "ramda";
const addNewBoy = () => navigate("/boys/add-boy");
const addNewBoy = partial(navigate, ["/boys/add-boy"]);
const handleRefresh = () => navigate("/boys");
const handleRefresh = partial(navigate, ["/boys"]);

curry

No FP premier is complete without curry. We will cover a simple example here, and recall how we used classic curry before.
// standalone example, copy paste anywhere
import { compose, curry } from "ramda";
const add = (x: number, y: number) => x + y;
const result = add(2, 3);
result; //? 5
// What happens if you don’t give add its required parameters?
// What if you give too little?
// add(2) // NaN
// So we curry: return a new function per expected parameter
const addCurried = curry(add); // just wrap the function in curry
const addCurriedClassic = (x: number) => (y: number) => x + y;
addCurried(2); // waits to be executed
addCurried(2)(3); //? 5
addCurriedClassic(2); // waits to be executed
addCurriedClassic(2)(3); //? 5
const add3 = (x: number, y: number, z: number) => x + y + z;
const greetFirstLast = (greeting: string, first: string, last: string) =>
`${greeting}, ${first} ${last}`;
const add3CurriedClassic = (x: number) => (y: number) => (z: number) =>
x + y + z;
const greetCurriedClassic =
(greeting: string) => (first: string) => (last: string) =>
`${greeting}, ${first} ${last}`;
const add3CurriedR = curry(add3); // just wrap the function
const greetCurriedR = curry(greetFirstLast); // just wrap the function
add3CurriedClassic(2)(3); // waiting to be executed
add3CurriedClassic(2)(3)(4); //?
// add3CurriedClassic(2, 3, 4) // doesn't work
greetCurriedClassic("hello"); // waiting to be executed
greetCurriedClassic("hello")("John")("Doe"); //?
// greetCurriedClassic('hello', 'John', 'Doe') // doesn't work
add3CurriedR(2)(3); // waiting to be executed
add3CurriedR(2)(3)(4); //? 9
// the advantage of ramda is flexible arity
add3CurriedR(2, 3, 4); //?
greetCurriedR("hello", "John"); // waiting to be executed
greetCurriedR("hello")("John")("Doe"); //? hello, John Doe
// flexible arity with ramda!
greetCurriedR("hello", "John", "Doe"); //? hello, John Doe
How is currying used in the React world? You will recall that we used currying in Heroes.tsx, HeroesList.tsx, VillianList.tsx VillainList.tsx components. We did not use Ramda curry, because it would be too complex at that time. You can optionally change them now.
// src/heroes/Heroes.tsx
import { curry } from "ramda";
// currying: the outer fn takes our custom arg and returns a fn that takes the event
const handleDeleteHero = (hero: Hero) => () => {
setHeroToDelete(hero);
setShowModal(true);
};
// we can use Ramda curry instead, we have to pass the unused event argument though
const handleDeleteHero = curry((hero: Hero, e: React.MouseEvent) => {
setHeroToDelete(hero);
setShowModal(true);
});
// src/heroes/HeroList.tsx
import { curry } from "ramda";
// currying: the outer fn takes our custom arg and returns a fn that takes the event
const handleSelectHero = (heroId: string) => () => {
const hero = deferredHeroes.find((h: Hero) => h.id === heroId);
navigate(
`/heroes/edit-hero/${hero?.id}?name=${hero?.name}&description=${hero?.description}`
);
};
// we can use Ramda curry instead, we have to pass the unused event argument though
const handleSelectHero = curry(
(heroId: string, e: MouseEvent<HTMLButtonElement>) => {
const hero = deferredHeroes.find((h: Hero) => h.id === heroId);
navigate(
`/heroes/edit-hero/${hero?.id}?name=${hero?.name}&description=${hero?.description}`
);
}
);
Let's make sure to use Ramda curry in the BoysList.tsx components for sure.
// src/boys/BoyList.tsx
import { curry } from "ramda";
// currying: the outer fn takes our custom arg and returns a fn that takes the event
const handleSelectBoy = (boyId: string) => () => {
const boy = deferredBoys.find((b: Boy) => b.id === boyId);
navigate(
`/boys/edit-boy/${boy?.id}?name=${boy?.name}&description=${boy?.description}`
);
};
// we can use Ramda curry instead, we have to pass the unused event argument though
const handleSelectBoy = curry(
(boyId: string, e: MouseEvent<HTMLButtonElement>) => {
const boy = deferredBoys.find((b: Boy) => b.id === boyId);
navigate(
`/boys/edit-boy/${boy?.id}?name=${boy?.name}&description=${boy?.description}`
);
}
);

ifElse

Takes 3 functions; predicate, truthy-result, false-result. The advantage over classic if else or ternary operator is that we can get very creative with function expressions vs conditions inside an if statement, or a ternary value. Easily explained with an example:
// standalone example, copy paste anywhere
const hasAccess = false; // toggle this to see the difference
// classic if else
function logAccessClassic(hasAccess: boolean) {
if (hasAccess) {
return "Access granted";
} else {
return "Access denied";
}
}
logAccessClassic(hasAccess); // Access granted | Access denied
// ternary operator
const logAccessTernary = (hasAccess: boolean) =>
hasAccess ? "Access granted" : "Access denied";
logAccessTernary(hasAccess); // // Access granted | Access denied
// Ramda ifElse
// The advantage is that you can package the logic away into function
// we can get very creative with function expressions
// vs conditions inside an if statement, or a ternary value
const logAccessRamda = ifElse(
(hasAccess: boolean) => hasAccess,
() => "Access granted",
() => "Access denied"
);
logAccessRamda(hasAccess); // Access granted | Access denied
Where can ifElse apply in the React world? Anywhere where there is complex conditional logic, which might be hard to express with classic if else statements or ternary operators. The example in BoyDetail.tsx is simple, but good for practicing Ramda ifElse.
// src/boys/BoyDetail.tsx
import { ifElse } from "ramda";
import { isTruthy } from "ramda-adjunct";
const handleSave = () => (name ? updateBoy(boy as Boy) : createBoy(boy as Boy));
const handleSave = ifElse(
() => isTruthy(name),
() => updateBoy(boy as Boy),
() => createBoy(boy as Boy)
);
Here is the BoyDetail.tsx component after the above changes. It can be compared to HeroDetail.tsx side by side to see the distinction between using Ramda and classic array methods.
// src/boys/BoyDetail.tsx
import { useState, ChangeEvent } from "react";
import { useNavigate, useParams } from "react-router-dom";
import { FaUndo, FaRegSave } from "react-icons/fa";
import InputDetail from "components/InputDetail";
import ButtonFooter from "components/ButtonFooter";
import PageSpinner from "components/PageSpinner";
import ErrorComp from "components/ErrorComp";
import { useEntityParams } from "hooks/useEntityParams";
import { usePostEntity } from "hooks/usePostEntity";
import { Boy } from "models/Boy";
import { usePutEntity } from "hooks/usePutEntity";
import { partial, ifElse } from "ramda";
import { isTruthy } from "ramda-adjunct";
export default function BoyDetail() {
const { id } = useParams();
const { name, description } = useEntityParams();
const [boy, setBoy] = useState({ id, name, description });
const { mutate: createBoy, status, error: postError } = usePostEntity("boy");
const {
updateEntity: updateBoy,
isUpdating,
isUpdateError,
} = usePutEntity("boy");
const navigate = useNavigate();
const handleCancel = partial(navigate, ["/boys"]);
const handleSave = ifElse(
() => isTruthy(name),
() => updateBoy(boy as Boy),
() => createBoy(boy as Boy)
);
const handleNameChange = (e: ChangeEvent<HTMLInputElement>) =>
setBoy({ ...boy, name: e.target.value });
const handleDescriptionChange = (e: ChangeEvent<HTMLInputElement>) =>
setBoy({ ...boy, description: e.target.value });
if (status === "loading" || isUpdating) {
return <PageSpinner />;
}
if (postError || isUpdateError) {
return <ErrorComp />;
}
return (
<div data-cy="boy-detail" className="card edit-detail">
<header className="card-header">
<p className="card-header-title">{name}</p>
&nbsp;
</header>
<div className="card-content">
<div className="content">
{id && (
<InputDetail name={"id"} value={id} readOnly={true}></InputDetail>
)}
<InputDetail
name={"name"}
value={name ? name : ""}
placeholder="e.g. Colleen"
onChange={handleNameChange}
></InputDetail>
<InputDetail
name={"description"}
value={description ? description : ""}
placeholder="e.g. dance fight!"
onChange={handleDescriptionChange}
></InputDetail>
</div>
</div>
<footer className="card-footer">
<ButtonFooter
label="Cancel"
IconClass={FaUndo}
onClick={handleCancel}
/>
<ButtonFooter label="Save" IconClass={FaRegSave} onClick={handleSave} />
</footer>
</div>
);
}

pipe

pipe is the bread and butter of Ramda; it is used to perform left-to-right function composition - the opposite of compose which is right-to-left. Explained with a simple example:
// standalone example, copy paste anywhere
import { compose, pipe } from "ramda";
// create a function that composes the three functions
const toUpperCase = (str: string) => str.toUpperCase();
const emphasizeFlavor = (flavor: string) => `${flavor} IS A GREAT FLAVOR`;
const appendIWantIt = (str: string) => `${str}. I want it`;
// note that with Ramda, we can pass the argument later
// and we shape the data through the composition / pipe
const classic = (flavor: string) =>
appendIWantIt(emphasizeFlavor(toUpperCase(flavor)));
const composeRamda = compose(appendIWantIt, emphasizeFlavor, toUpperCase);
const pipeRamda = pipe(toUpperCase, emphasizeFlavor, appendIWantIt);
classic("chocolate");
composeRamda("chocolate");
pipeRamda("chocolate");
// CHOCOLATE IS A GREAT FLAVOR. I want it
Where can pipe apply in the React world? Any sequence of actions would be a fit.
The below 3 varieties of handleCloseModal function from Boys.tsx are equivalent.
// src/boys/Boys.tsx
import { partial, pipe } from "ramda";
const handleCloseModal = () => {
setBoyToDelete(null);
setShowModal(false);
};
const handleCloseModal = pipe(
() => setBoyToDelete(null),
() => setShowModal(false)
);
const handleCloseModal = pipe(
partial(setBoyToDelete, [null]),
partial(setShowModal, [false])
);
The below 3 varieties of handleDeleteBoy function from Boys.tsx are also equivalent.
// src/boys/Boys.tsx
import { partial, pipe } from "ramda";
const handleDeleteBoy = (boy: Boy) => () => {
setBoyToDelete(boy);
setShowModal(true);
};
const handleDeleteBoy = (boy: Boy) =>
pipe(
() => setBoyToDelete(boy),
() => setShowModal(true)
);
const handleDeleteBoy = (boy: Boy) =>
pipe(partial(setBoyToDelete, [boy]), partial(setShowModal, [true]));
The below 3 varieties of handleDeleteFromModal function from Boys.tsx are also equivalent.
// src/boys/Boys.tsx
const handleDeleteFromModal = () => {
boyToDelete ? deleteBoy(boyToDelete) : null;
setShowModal(false);
};
const handleDeleteFromModal2 = pipe(
() => (boyToDelete ? deleteBoy(boyToDelete) : null),
() => setShowModal(false)
);
const handleDeleteFromModal3 = pipe(
() => (boyToDelete ? deleteBoy(boyToDelete) : null),
partial(setShowModal, [false])
);
Here is the Boys.tsx component after the above changes. It can be compared to Heroes.tsx to see the distinction between using Ramda and classic array methods.
// src/boys/Boys.tsx
import { useState } from "react";
import { useNavigate, Routes, Route } from "react-router-dom";
import ListHeader from "components/ListHeader";
import ModalYesNo from "components/ModalYesNo";
import ErrorComp from "components/ErrorComp";
import BoyList from "./BoyList";
import BoyDetail from "./BoyDetail";
import { useGetEntities } from "hooks/useGetEntities";
import { useDeleteEntity } from "hooks/useDeleteEntity";
import { Boy } from "models/Boy";
import { partial, pipe } from "ramda";
export default function Boys() {
const [showModal, setShowModal] = useState<boolean>(false);
const { entities: boys, getError } = useGetEntities("boys");
const [boyToDelete, setBoyToDelete] = useState<Boy | null>(null);
const { deleteEntity: deleteBoy, isDeleteError } = useDeleteEntity("boy");
const navigate = useNavigate();
const addNewBoy = partial(navigate, ["/boys/add-boy"]);
const handleRefresh = partial(navigate, ["/boys"]);
const handleCloseModal = pipe(
partial(setBoyToDelete, [null]),
partial(setShowModal, [false])
);
const handleDeleteBoy = (boy: Boy) =>
pipe(partial(setBoyToDelete, [boy]), partial(setShowModal, [true]));
const handleDeleteFromModal = pipe(
() => (boyToDelete ? deleteBoy(boyToDelete) : null),
partial(setShowModal, [false])
);
if (getError || isDeleteError) {
return <ErrorComp />;
}
return (
<div data-cy="boys">
<ListHeader
title="Boys"
handleAdd={addNewBoy}
handleRefresh={handleRefresh}
/>
<div>
<div>
<Routes>
<Route
path=""
element={
<BoyList boys={boys} handleDeleteBoy={handleDeleteBoy} />
}
/>
<Route path="/add-boy" element={<BoyDetail />} />
<Route path="/edit-boy/:id" element={<BoyDetail />} />
<Route
path="*"
element={
<BoyList boys={boys} handleDeleteBoy={handleDeleteBoy} />
}
/>
</Routes>
</div>
</div>
{showModal && (
<ModalYesNo
message="Would you like to delete the boy?"
onNo={handleCloseModal}
onYes={handleDeleteFromModal}
/>
)}
</div>
);
}

Refactoring search-filter with array methods to Ramda

For this example, we will extract search-filter logic in BoyList.tsx into a standalone TS file, alongside types and data. Given the data heroes array, we want to filter by any property (id, name, description) and display 1 or more objects. To accomplish this, we use 2 lego-functions searchExistsC & propertyExistsC that builds up to searchPropertiesC, C for classic.
// standalone example, copy paste anywhere
const heroes = [
{
id: "HeroAslaug",
name: "Aslaug",
description: "warrior queen",
},
{
id: "HeroBjorn",
name: "Bjorn Ironside",
description: "king of 9th century Sweden",
},
{
id: "HeroIvar",
name: "Ivar the Boneless",
description: "commander of the Great Heathen Army",
},
{
id: "HeroLagertha",
name: "Lagertha the Shieldmaiden",
description: "aka Hlaðgerðr",
},
{
id: "HeroRagnar",
name: "Ragnar Lothbrok",
description: "aka Ragnar Sigurdsson",
},
{
id: "HeroThora",
name: "Thora Town-hart",
description: "daughter of Earl Herrauðr of Götaland",
},
];
const textToSearch = "ragnar";
interface Hero {
id: string;
name: string;
description: string;
}
type HeroProperty = Hero["name"] | Hero["description"] | Hero["id"];
// use the classic array methods to search for properties in a given array of entities
/** returns a boolean whether the entity property exists in the search field */
const searchExistsC = (searchField: string, searchProperty: HeroProperty) =>
String(searchProperty).toLowerCase().indexOf(searchField.toLowerCase()) !==
-1;
/** finds the given entity's property in the search field */
const propertyExistsC = (searchField: string, item: Hero) =>
Object.values(item).find((property: HeroProperty) =>
searchExistsC(searchField, property)
);
/** given the search field and the entity array, returns the entity
in which the search field exists */
const searchPropertiesC = (data: Hero[], searchField: string) =>
[...data].filter((item: Hero) => propertyExistsC(searchField, item));
searchPropertiesC(heroes, textToSearch);
/*
[{
id: 'HeroRagnar',
name: 'Ragnar Lothbrok',
description: 'aka Ragnar Sigurdsson'
}]
*/
Although we have partitioned the functions like legos, the logic isn't very easy to follow while reading the code. The reason is having to use multiple arguments, and how the data changes from an array to an array item (an object), while also having to change the placement of the arguments. Using Ramda, we can make the code easier to read and use.
Here is a quick comparison of .toLowerCase() vs toLower() and .indexOf() vs indexOf. Instead of chaining on the data, Ramda helpers indexOf and toLower are functions that take the data as an argument:
  • someData.toLowerCase() vs toLower(someData)
  • array.indexOf(arrayItem) vs indexOf(arrayItem, array)
"HELLO".toLowerCase(); // hello
toLower("HELLO"); // hello
[1, 2, 3, 4].indexOf(3); // 2
indexOf(3, [1, 2, 3, 4]); // 2
We can refactor searchExistsC to use Ramda. Here is the before and after side by side:
import { indexOf, toLower } from "ramda";
const searchExistsC = (searchField: string, searchProperty: HeroProperty) =>
String(searchProperty).toLowerCase().indexOf(searchField.toLowerCase()) !==
-1;
const searchExists = (searchField: string, searchProperty: HeroProperty) =>
indexOf(toLower(searchField), toLower(searchProperty)) !== -1;
Below is a quick comparison of array.find vs Ramda find, Object.values vs Ramda values. Notice with Ramda version how the data comes at the end, with this style we can save an arg for later.
// data.find(callback)
Object.values(heroes[4]).find(
(property: HeroProperty) => property === "Ragnar Lothbrok"
);
// find(callback)(data)
find((property: HeroProperty) => property === "Ragnar Lothbrok")(
values(heroes[4])
);
Here is the refactor of propertyExistsC to use Ramda, before and after side by side. We can amplify the Ramda version with pipe to make the flow more similar to the classic version.
const propertyExistsC = (searchField: string, item: Hero) =>
Object.values(item).find((property: HeroProperty) =>
searchExistsC(searchField, property)
);
// we wrap the function in curry to allow passing the item argument independently, later
// f(a, b) to curry(f(a, b)) allows us to f(a)(b)
const propertyExists = curry((searchField: string, item: Hero) =>
find((property: HeroProperty) => searchExists(searchField, property))(
values(item)
)
);
// we can take it a step further with pipe
// the data item comes at the end, we take it and pipe through values & find
// this way the flow is more similar to the original function
const propertyExistsNew = curry((searchField: string, item: Hero) =>
pipe(
values,
find((property: HeroProperty) => searchExists(searchField, property))
)(item)
);
Our final function searchExistsC uses the previous two. Here are the classic and Ramda versions side by side. We can evolve the Ramda version to take 1 argument, and get the data later whenever it is available.
// (2 args) => data.filter(callback)
const searchPropertiesC = (data: Hero[], searchField: string) =>
[...data].filter((item: Hero) => propertyExistsC(searchField, item));
// (2 args) => filter(callback, data)
const searchProperties = (searchField: string, data: Hero[]) =>
filter((item: Hero) => propertyExists(searchField, item), [...data]);
// (2 args) => filter(callback)(data)
const searchPropertiesBetter = (searchField: string, data: Hero[]) =>
filter((item: Hero) => propertyExistsNew(searchField, item))(data);
// (1 arg) => filter(fn), the data can come later and it will flow through
// notice how we eliminated another piece of data; item
const searchPropertiesBest = (searchField: string) =>
filter(propertyExistsNew(searchField));
Here is the full example file that can be copy pasted anywhere
// standalone example, copy paste anywhere
import { indexOf, filter, find, curry, toLower, pipe, values } from "ramda";
const heroes = [
{
id: "HeroAslaug",
name: "Aslaug",
description: "warrior queen",
},
{
id: "HeroBjorn",
name: "Bjorn Ironside",
description: "king of 9th century Sweden",
},
{
id: "HeroIvar",
name: "Ivar the Boneless",
description: "commander of the Great Heathen Army",
},
{
id: "HeroLagertha",
name: "Lagertha the Shieldmaiden",
description: "aka Hlaðgerðr",
},
{
id: "HeroRagnar",
name: "Ragnar Lothbrok",
description: "aka Ragnar Sigurdsson",
},
{
id: "HeroThora",
name: "Thora Town-hart",
description: "daughter of Earl Herrauðr of Götaland",
},
];
const textToSearch = "ragnar";
interface Hero {
id: string;
name: string;
description: string;
}
Object.values(heroes[4]).find(
(property: HeroProperty) => property === "Ragnar Lothbrok"
);
find((property: HeroProperty) => property === "Ragnar Lothbrok")(
values(heroes[4])
); //?
type HeroProperty = Hero["name"] | Hero["description"] | Hero["id"];
/** returns a boolean whether the hero properties exist in the search field */
const searchExistsC = (searchField: string, searchProperty: HeroProperty) =>
String(searchProperty).toLowerCase().indexOf(searchField.toLowerCase()) !==
-1;
const propertyExistsC = (searchField: string, item: Hero) =>
Object.values(item).find((property: HeroProperty) =>
searchExistsC(searchField