Before starting this section, make sure to go through the prerequisite where were mirror Heroes to Boys.
Bu bölüme başlamadan önce, Heroesı Boysa yansıttığımız önkoşul bölümünden geçtiğinizden emin olun.
Neden Fonksiyonel Programlama, neden Ramda?
Eğer EcmaScript'e yapılan son önerilere bir göz atarsak, FP'nin yolunun olduğunu ve yeni JS özelliklerinin RamdaJS yardımcı programlarına benzer şekilde olacağını fark ederiz. Bir özet için ES2025'te neler yeni? başlıklı videoya göz atın. Bugün FP ve Ramda'yı öğrenmenin, en azından önümüzdeki on yıl için geleceğe dayanıklı bir JS beceri seti oluşturacağına inanıyoruz.
Tüm seviyelerde TDD ile oluşturulmuş sağlam testlere sahip bir React uygulamamız ve %100 kod kapsamımız var. Önkoşullarda Heroes varlık grubunu Boysa yansıttık. Şimdi cesur yeniden yapılandırmalar deneyebilir ve her şeyin hala çalışıp çalışmadığını görebiliriz.
FP üzerine yazılmış çok sayıda kaynak bulunsa da, özellikle modern React ve TypeScript ile FP ve Ramda kullanımına dair gerçek dünya örneklerine yönelik eksiklikler bulunmaktadır. Bu bölümün bu boşluğu doldurmasını umuyoruz.
Fonksiyonel Programlama JS kaynakları
Bu çalışmayı ilham kaynağı olarak kullanılan kaynaklar şunlardır. Bu bölümden yararlanmak için FP konusunda akıcı olmanız gerekmez, ancak daha sonra bilginizi tamamlamayı tercih ederseniz, bu kaynakları şiddetle önerilir.
İlerleyen bölümlerde Ramda kullanarak pratik örnekler sunacağız ve bu bilgileri 3 Boys bileşenine uygulayacağız; BoyDetail.tsx, BoyList.tsx ve Boys.tsx.
n bağımsız değişkenli herhangi bir işlev oluşturun ve bunu partial ile sarın. Önceden paketlenmiş olan bağımsız değişkenleri bildirin ve geri kalan bağımsız değişkenler beklenir. Basit bir şekilde anlatmak gerekirse; beş parametreli bir işleve sahipseniz ve bağımsız değişkenlerin üçünü sağlarsanız, son ikisini bekleyen bir işlevle karşılaşırsınız.
// 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!
partial React dünyasında nerede uygulanabilir? İsimsiz bir işlemin, bir bağımsız değişkenle adlandırılmış bir işlev döndürdüğü herhangi bir olay işleyici. Aşağıdaki iki örnek aynıdır:
Hiçbir FP tanıtımı, curry'siz tamamlanmış sayılmaz. Burada basit bir örnek üzerinde duracağız ve daha önce klasik curry'i nasıl kullandığımızı hatırlayacağız.
// 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
Curry, React dünyasında nasıl kullanılır? Heroes.tsx, HeroesList.tsx, VillianList.tsxVillainList.tsx bileşenlerinde curry'i kullandığımızı hatırlayacaksınız. O zaman Ramda curry'i kullanmadık, çünkü o zaman çok karmaşık olurdu. İsteğe bağlı olarak şimdi onları değiştirebilirsiniz.
// 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}` ); });
// 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}` ); });
3 işlev alır; yargı, doğru-sonuç, yanlış-sonuç. Klasik if else veya üçlü operatöre göre avantajı, işlev ifadeleriyle iç içe geçmiş ifadeler veya üçlü değerlerle çok daha yaratıcı hale gelebiliriz. Basit bir örnek ile açıklanabilir:
// 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
ifElse React dünyasında nerede uygulanabilir? Klasik if else ifadeleri veya üçlü operatörlerle ifade etmesi zor olan karmaşık koşullu mantık bulunan her yerde. BoyDetail.tsx'deki örnek basit olsa da, Ramda çalışmak icin uygun.
Yukarıdaki değişikliklerin ardından BoyDetail.tsx bileşeni burada. HeroDetail.tsx ile yan yana kıyaslanarak, Ramda kullanımı ve klasik dizi yöntemleri arasındaki fark görülebilir.
pipe, Ramda'nın temelidir; soldan sağa işlev bileşimi gerçekleştirmek için kullanılır - compose ise sağdan sola işlev bileşimidir. Basit bir örnek ile açıklanır:
// 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
React dünyasında pipe nerede uygulanabilir? Herhangi bir işlem dizisi uygun olacaktır.
Aşağıdaki 3 çeşit handleCloseModal fonksiyonu Boys.tsx dosyasından eşdeğerdir.
Yukarıdaki değişikliklerin ardından Boys.tsx bileşeni burada. Ramda ve klasik dizi yöntemleri arasındaki farkı görmek için Heroes.tsx ile karşılaştırılabilir.
Dizi yöntemleriyle arama filtresini Ramda'ya yeniden düzenleme
Bu örnekte, BoyList.tsx içindeki arama filtresi mantığını, tipler ve verilerle birlikte bağımsız bir TS dosyasına çıkaracağız. Veri heroes dizisine göre (id, name, description gibi) herhangi bir özellikle filtrelemek ve 1 veya daha fazla nesne göstermek istiyoruz. Bunu başarmak için, searchExistsC ve propertyExistsC adlı 2 lego-fonksiyon kullanarak searchPropertiesC'ye, C ise klasik anlamında ulaşırız.
// 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' }]*/
İşlevleri legolar gibi bölmüş olsak da, kodu okurken mantığı çok kolay takip etmek mümkün değildir. Bunun nedeni, birden fazla argüman kullanmak zorunda olmak ve verinin bir dizi öğesinden (bir nesne) bir dizi öğesine değişmesi, ayrıca argümanların yerini değiştirmek zorunda olmaktır. Ramda kullanarak kodu daha okunaklı ve kullanışlı hale getirebiliriz.
.toLowerCase() ve toLower() ile .indexOf() ve indexOf arasında hızlı bir karşılaştırma yapalım. Veri üzerinde zincirleme yerine, Ramda yardımcıları indexOf ve toLower, veriyi argüman olarak alan işlevlerdir:
someData.toLowerCase() vs toLower(someData)
array.indexOf(arrayItem) vs indexOf(arrayItem, array)
Aşağıda, array.find ile Ramda find, Object.values ile Ramda values arasında hızlı bir karşılaştırma bulunmaktadır. Ramda sürümünde verinin sonunda nasıl geldiğine dikkat edin; bu stil ile daha sonra kullanmak üzere bir argümanı kaydedebiliriz.
İşte propertyExistsC'nin Ramda kullanacak şekilde yeniden düzenlenmesi, önce ve sonra yan yana. Akışı klasik sürüme daha benzer hale getirmek için Ramda sürümünü pipe ile güçlendirebiliriz.
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));
Son işlevimiz searchExistsC önceki ikisini kullanır. İşte klasik ve Ramda sürümleri yan yana. Ramda sürümünü geliştirerek 1 argüman alacak hale getirebilir ve veriyi, ne zaman kullanılabilir olduğunu sonra alabiliriz.
// (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));
İşte her yere kopyalanıp yapıştırılabilen tam örnek dosyası:
// 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); //?
Bununla birlikte, searchProperties ve ona yol açan lego-fonksiyonları Ramda sürümleriyle değiştirebiliriz.
İşte ana değişikliklerin karşılaştırması:
// 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[]>) ); };
İşte bileşenin son hali:
// 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> );}
Araçlarımız ve testlerimiz, değişikliklerden sonra hala sağlam durumda ve bu, çok yüksek kapsamaya sahip olmanın lüksüdür. Kodumuza cesur, deneysel yeniden düzenlemeler uygulayabildik ve FP ve Ramda kullanarak kodun daha okunaklı ve çalışılabilir hale gelirken hiçbir şeyin gerilemediğinden emin olduk.
Önce ve sonraya bakmak için, bu bölümle ilgili PR'ı şu adreste bulabilirsiniz: https://github.com/muratkeremozcan/tour-of-heroes-react-cypress-ts/pull/110. Ayrıca Boys dosyalarını Heroes veya Villains ile karşılaştırabilirsiniz.