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.
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.
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 anywhereimport { partial, partialObject, partialRight } from"ramda";//// example 1// create any functionconstmultiply= (a:number, b:number) => a * b;// wrap it with partial, declare the pre-packaged argsconstdouble=partial(multiply, [2]);// the function waits for execution,// executes upon the second arg being passeddouble; // [λ]double(3); // 6//// example 2// create any functionconstgreet= ( salutation:string, title:string, firstName:string, lastName:string) => salutation +", "+ title +" "+ firstName +" "+ lastName +"!";// wrap it with partial, declare the pre-packaged argsconstsayHello=partial(greet, ["Hello"]);// the function waits for 3 arguments to be passed before executingsayHello; // [λ]sayHello("Ms","Jane","Jones"); // Hello, Ms Jane Jones!// another variety, has 2 pre-packaged argsconstsayHelloMs=partial(greet, ["Hello","Ms"]);// waits for 2 more args before executingsayHelloMs; // // [λ]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:
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 anywhereimport { compose, curry } from"ramda";constadd= (x:number, y:number) => x + y;constresult=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 parameterconstaddCurried=curry(add); // just wrap the function in curryconstaddCurriedClassic= (x:number) => (y:number) => x + y;addCurried(2); // waits to be executedaddCurried(2)(3); //? 5addCurriedClassic(2); // waits to be executedaddCurriedClassic(2)(3); //? 5constadd3= (x:number, y:number, z:number) => x + y + z;constgreetFirstLast= (greeting:string, first:string, last:string) =>`${greeting}, ${first}${last}`;constadd3CurriedClassic= (x:number) => (y:number) => (z:number) => x + y + z;constgreetCurriedClassic= (greeting:string) => (first:string) => (last:string) =>`${greeting}, ${first}${last}`;constadd3CurriedR=curry(add3); // just wrap the functionconstgreetCurriedR=curry(greetFirstLast); // just wrap the functionadd3CurriedClassic(2)(3); // waiting to be executedadd3CurriedClassic(2)(3)(4); //?// add3CurriedClassic(2, 3, 4) // doesn't workgreetCurriedClassic("hello"); // waiting to be executedgreetCurriedClassic("hello")("John")("Doe"); //?// greetCurriedClassic('hello', 'John', 'Doe') // doesn't workadd3CurriedR(2)(3); // waiting to be executedadd3CurriedR(2)(3)(4); //? 9// the advantage of ramda is flexible arityadd3CurriedR(2,3,4); //?greetCurriedR("hello","John"); // waiting to be executedgreetCurriedR("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.tsxVillainList.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.tsximport { curry } from"ramda";// currying: the outer fn takes our custom arg and returns a fn that takes the eventconsthandleDeleteHero= (hero:Hero) => () => {setHeroToDelete(hero);setShowModal(true);};// we can use Ramda curry instead, we have to pass the unused event argument thoughconsthandleDeleteHero=curry((hero:Hero, e:React.MouseEvent) => {setHeroToDelete(hero);setShowModal(true);});
// src/heroes/HeroList.tsximport { curry } from"ramda";// currying: the outer fn takes our custom arg and returns a fn that takes the eventconsthandleSelectHero= (heroId:string) => () => {consthero=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 thoughconsthandleSelectHero=curry( (heroId:string, e:MouseEvent<HTMLButtonElement>) => {consthero=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.tsximport { curry } from"ramda";// currying: the outer fn takes our custom arg and returns a fn that takes the eventconsthandleSelectBoy= (boyId:string) => () => {constboy=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 thoughconsthandleSelectBoy=curry( (boyId:string, e:MouseEvent<HTMLButtonElement>) => {constboy=deferredBoys.find((b:Boy) =>b.id === boyId);navigate(`/boys/edit-boy/${boy?.id}?name=${boy?.name}&description=${boy?.description}` ); });
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 anywhereconsthasAccess=false; // toggle this to see the difference// classic if elsefunctionlogAccessClassic(hasAccess:boolean) {if (hasAccess) {return"Access granted"; } else {return"Access denied"; }}logAccessClassic(hasAccess); // Access granted | Access denied// ternary operatorconstlogAccessTernary= (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 valueconstlogAccessRamda=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.
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.
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 anywhereimport { compose, pipe } from"ramda";// create a function that composes the three functionsconsttoUpperCase= (str:string) =>str.toUpperCase();constemphasizeFlavor= (flavor:string) =>`${flavor} IS A GREAT FLAVOR`;constappendIWantIt= (str:string) =>`${str}. I want it`;// note that with Ramda, we can pass the argument later// and we shape the data through the composition / pipeconstclassic= (flavor:string) =>appendIWantIt(emphasizeFlavor(toUpperCase(flavor)));constcomposeRamda=compose(appendIWantIt, emphasizeFlavor, toUpperCase);constpipeRamda=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.
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.
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 anywhereconstheroes= [ { 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", },];consttextToSearch="ragnar";interfaceHero { id:string; name:string; description:string;}typeHeroProperty=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 */constsearchExistsC= (searchField:string, searchProperty:HeroProperty) =>String(searchProperty).toLowerCase().indexOf(searchField.toLowerCase()) !==-1;/** finds the given entity's property in the search field */constpropertyExistsC= (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 */constsearchPropertiesC= (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)
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.
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.
constpropertyExistsC= (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)constpropertyExists=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 functionconstpropertyExistsNew=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)constsearchPropertiesC= (data:Hero[], searchField:string) => [...data].filter((item:Hero) =>propertyExistsC(searchField, item));// (2 args) => filter(callback, data)constsearchProperties= (searchField:string, data:Hero[]) =>filter((item:Hero) =>propertyExists(searchField, item), [...data]);// (2 args) => filter(callback)(data)constsearchPropertiesBetter= (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; itemconstsearchPropertiesBest= (searchField:string) =>filter(propertyExistsNew(searchField));
Here is the full example file that can be copy pasted anywhere
// standalone example, copy paste anywhereimport { indexOf, filter, find, curry, toLower, pipe, values } from"ramda";constheroes= [ { 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", },];consttextToSearch="ragnar";interfaceHero { 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])); //?typeHeroProperty=Hero["name"] |Hero["description"] |Hero["id"];/** returns a boolean whether the hero properties exist in the search field */constsearchExistsC= (searchField:string, searchProperty:HeroProperty) =>String(searchProperty).toLowerCase().indexOf(searchField.toLowerCase()) !==-1;constpropertyExistsC= (searchField:string, item:Hero) =>Object.values(item).find((property:HeroProperty) =>searchExistsC(searchField, property) );constsearchPropertiesC= (data:Hero[], searchField:string) => [...data].filter((item:Hero) =>propertyExistsC(searchField, item));searchPropertiesC(heroes, textToSearch); //?// rewrite in ramdaconstsearchExists= (searchField:string, searchProperty:HeroProperty) =>indexOf(toLower(searchField),toLower(searchProperty)) !==-1;constpropertyExists=curry((searchField:string, item:Hero) =>find((property:HeroProperty) =>searchExists(searchField, property))(values(item) ));// refactor propertyExists to use pipeconstpropertyExistsNew=curry((searchField:string, item:Hero) =>pipe( values,find((property:HeroProperty) =>searchExists(searchField, property)) )(item));// refactor in better ramdaconstsearchProperties= (searchField:string, data:Hero[]) =>filter((item:Hero) =>propertyExists(searchField, item), [...data]);constsearchPropertiesBetter= (searchField:string, data:Hero[]) =>filter((item:Hero) =>propertyExistsNew(searchField, item))(data);constsearchPropertiesBest= (searchField:string) =>filter(propertyExistsNew(searchField));searchProperties(textToSearch, heroes); //?searchPropertiesBetter(textToSearch, heroes); //?searchPropertiesBest(textToSearch)(heroes); //?
With that, we can replace searchProperties and the lego-functions that lead up to it with their Ramda versions.
Here is the comparison of the key changes:
// src/boys/BoyList.tsx (before)/** returns a boolean whether the boy properties exist in the search field */constsearchExists= (searchProperty:BoyProperty, searchField:string) =>String(searchProperty).toLowerCase().indexOf(searchField.toLowerCase()) !==-1;/** given the data and the search field, returns the data in which the search field exists */constsearchProperties= (searchField:string, data:Boy[]) => [...data].filter((item:Boy) =>Object.values(item).find((property:BoyProperty) =>searchExists(property, searchField) ) );consthandleSearch= (data:Boy[]) => (event:ChangeEvent<HTMLInputElement>) => {constsearchField=event.target.value;returnstartTransition(() =>setFilteredBoys(searchProperties(searchField, data)) ); };
// src/boys/BoyList.tsx (after)/** returns a boolean whether the boy properties exist in the search field */constsearchExists= (searchField:string, searchProperty:BoyProperty) =>indexOf(toLower(searchField),toLower(searchProperty)) !==-1;/** finds the given boy's property in the search field */constpropertyExists=curry((searchField:string, item:Boy) =>pipe( values,find((property:BoyProperty) =>searchExists(searchField, property)) )(item));/** given the search field and the boy array, returns the boy in which the search field exists */constsearchProperties= ( searchField:string): (<PextendsBoy,CextendsreadonlyP[] |Dictionary<P>>( collection:C) =>C) =>filter(propertyExists(searchField));/** filters the boys data to see if the any of the properties exist in the list */consthandleSearch= (data:Boy[]) => (event:ChangeEvent<HTMLInputElement>) => {constsearchField=event.target.value;constsearchedBoy=searchProperties(searchField)(data);returnstartTransition(() =>setFilteredBoys(searchedBoy asReact.SetStateAction<Boy[]>) ); };
Here is the final form of the component:
// src/boys/BoyList.tsximport { useNavigate } from"react-router-dom";import CardContent from"components/CardContent";import ButtonFooter from"components/ButtonFooter";import { FaEdit, FaRegSave } from"react-icons/fa";import { ChangeEvent, MouseEvent, useTransition, useEffect, useState, useDeferredValue,} from"react";import { Boy } from"models/Boy";import { BoyProperty } from"models/types";import { indexOf, find, curry, toLower, pipe, values, filter, Dictionary,} from"ramda";typeBoyListProps= { boys:Boy[];handleDeleteBoy: (boy:Boy) => (e:MouseEvent<HTMLButtonElement>) =>void;};exportdefaultfunctionBoyList({ boys, handleDeleteBoy }:BoyListProps) {constdeferredBoys=useDeferredValue(boys);constisStale= deferredBoys !== boys;const [filteredBoys,setFilteredBoys] =useState(deferredBoys);constnavigate=useNavigate();const [isPending,startTransition] =useTransition();// needed to refresh the list after deleting a boyuseEffect(() =>setFilteredBoys(deferredBoys), [deferredBoys]);consthandleSelectBoy=curry( (boyId:string, e:MouseEvent<HTMLButtonElement>) => {constboy=deferredBoys.find((b:Boy) =>b.id === boyId);navigate(`/boys/edit-boy/${boy?.id}?name=${boy?.name}&description=${boy?.description}` ); } );/** returns a boolean whether the boy properties exist in the search field */constsearchExists= (searchField:string, searchProperty:BoyProperty) =>indexOf(toLower(searchField),toLower(searchProperty)) !==-1;/** finds the given boy's property in the search field */constpropertyExists=curry((searchField:string, item:Boy) =>pipe( values,find((property:BoyProperty) =>searchExists(searchField, property)) )(item) );/** given the search field and the boy array, returns the boy in which the search field exists */constsearchProperties= ( searchField:string ): (<PextendsBoy,CextendsreadonlyP[] |Dictionary<P>>( collection:C ) =>C) =>filter(propertyExists(searchField));/** filters the boys data to see if the any of the properties exist in the list */consthandleSearch= (data:Boy[]) => (event:ChangeEvent<HTMLInputElement>) => {constsearchField=event.target.value;constsearchedBoy=searchProperties(searchField)(data);returnstartTransition(() =>setFilteredBoys(searchedBoy asReact.SetStateAction<Boy[]>) ); };return ( <divstyle={{ opacity: isPending ?0.5:1, color: isStale ?"dimgray":"black", }} > {deferredBoys.length>0&& ( <divclassName="card-content"> <span>Search </span> <inputdata-cy="search"onChange={handleSearch(deferredBoys)} /> </div> )} <uldata-cy="boy-list"className="list"> {filteredBoys.map((boy, index) => ( <lidata-cy={`boy-list-item-${index}`} key={boy.id}> <divclassName="card"> <CardContentname={boy.name} description={boy.description} /> <footerclassName="card-footer"> <ButtonFooterlabel="Delete"IconClass={FaRegSave}onClick={handleDeleteBoy(boy)} /> <ButtonFooterlabel="Edit"IconClass={FaEdit}onClick={handleSelectBoy(boy.id)} /> </footer> </div> </li> ))} </ul> </div> );}
Our tooling and tests are still intact after the changes, and that is the luxury of having very high coverage. We were able to apply daring, experimental refactors to our code, and are still confident that nothing regressed while the code became easier to read and work with using FP and Ramda.
To take a look at the before and after, you can find the PR for this section at https://github.com/muratkeremozcan/tour-of-heroes-react-cypress-ts/pull/110. You can also compare the Boys group of files to Heroes or Villains.