Bu bölüme başlamadan önce, uygulamayı Kahramanlar ve Kötüler arasında daha genel hale getirmek için önbilgi bölümünden geçtiğinizden emin olun.
Bu bölümde, kahramanları kötülere yansıtacağız ve Context api'yi kötülere uygulayacağız. Context api, değeri bileşen ağacının derinliklerine geçirmemize olanak tanır, bunu her bileşenden açıkça geçirerek yapmamız gerekmez. Bu, bileşen ağacında durumu paylaşmak için context'i kullanmak ile durumu alt bileşenlere prop olarak geçirmek arasında güzel bir karşılaştırma sunacaktır.
Heroes.tsx, heroes özelliğini HeroList.tsx'e aktarır.
HeroDetail, hero{id, name, description} durumunu URL'den useParams ve useSearchParams ile alır.
Bu, uygulamamızdaki durum yönetiminin sınırlarıdır. Gerçekte context'e ihtiyacımız yoktur, ancak context api ile şeylerin nasıl farklı olabileceğini inceleyeceğiz.
Kötüler için Context API
Şu anda kahramanlardan kötülere tam bir yansıma elde ettik, işlevsellik ve testler tam olarak aynı şekilde gerçekleştiriliyor. Bununla birlikte, kötüler grubunu değiştirecek ve bunu yaparken Context api'den faydalanacağız.
Villains.tsx, villains özelliğini VillainList.tsx'e aktarır. Bunun yerine Context api'yi kullanarak villains'in Villains.txt altındaki tüm bileşenlerde kullanılabilir olmasını sağlayacağız.
Context api ile genel adımlar şunlardır:
Context'i oluşturun ve dışa aktarın. Genellikle bu ayrı bir dosyada bulunur ve arabulucu görevi görür.
// src/villains/VillainsContext.tsx (the common node)import { Villain } from"models/Villain";import { createContext } from"react";constVillainsContext=createContext<Villain[]>([]);exportdefault VillainsContext;
Alt bileşenlere geçirilecek durumu belirleyin. Orada context'i içe aktarın.
Örneğimizde, Villains.tsx dosyasından, villains'i VillainDetail.tsx'e geçiriyoruz. villains'i useGetEntity adlı kancadan alıyoruz. Şu anda villains'i bir özellik olarak VillainDetail.tsx'e geçiriyoruz ve bunun yerine context api'yi kullanmak istiyoruz. Bu yüzden context'i içe aktarıyoruz ve context sağlayıcıyı, içinde villains'e atanmış bir value özelliği olan rotalarla sarıyoruz (Adımlar 2, 3).
VillainsList.tsx bileşenine context aracılığıyla durumu iletmemiz gerekiyor. Bu nedenle context'i ve React'tan useContext'i içe aktarıyoruz. Paylaşılan context'i argüman olarak içeren useContext'i çağırıyoruz ve villains adlı bir değişkene atıyoruz (Adımlar 4, 5). Şimdi, özelliğin yerine, durumu VillainsContext'ten alıyoruz.
// src/villains/VillainList.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 { useContext } from"react";import { Villain } from"models/Villain";import VillainsContext from"./VillainsContext";typeVillainListProps= {handleDeleteVillain: ( villain:Villain ) => (e:MouseEvent<HTMLButtonElement>) =>void;};exportdefaultfunctionVillainList({ handleDeleteVillain }:VillainListProps) {constvillains=useContext(VillainsContext);constdeferredVillains=useDeferredValue(villains);constisStale= deferredVillains !== villains;const [filteredVillains,setFilteredVillains] =useState(deferredVillains);constnavigate=useNavigate();const [isPending,startTransition] =useTransition();// needed to refresh the list after deleting a villainuseEffect(() =>setFilteredVillains(deferredVillains), [deferredVillains]);consthandleSelectVillain= (villainId:string) => () => {constvillain=deferredVillains.find((h:Villain) =>h.id === villainId);navigate(`/villains/edit-villain/${villain?.id}?name=${villain?.name}&description=${villain?.description}` ); };typeVillainProperty=|Villain["name"]|Villain["description"]|Villain["id"];/** returns a boolean whether the villain properties exist in the search field */constsearchExists= (searchProperty:VillainProperty, 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:Villain[]) => [...data].filter((item:Villain) =>Object.values(item).find((property:VillainProperty) =>searchExists(property, searchField) ) );/** filters the villains data to see if the any of the properties exist in the list */consthandleSearch= (data:Villain[]) => (event:ChangeEvent<HTMLInputElement>) => {constsearchField=event.target.value;returnstartTransition(() =>setFilteredVillains(searchProperties(searchField, data)) ); };return ( <divstyle={{ opacity: isPending ?0.5:1, color: isStale ?"dimgray":"black", }} > {deferredVillains.length>0&& ( <divclassName="card-content"> <span>Search </span> <inputdata-cy="search"onChange={handleSearch(deferredVillains)} /> </div> )} <uldata-cy="villain-list"className="list"> {filteredVillains.map((villain, index) => ( <lidata-cy={`villain-list-item-${index}`} key={villain.id}> <divclassName="card"> <CardContentname={villain.name}description={villain.description} /> <footerclassName="card-footer"> <ButtonFooterlabel="Delete"IconClass={FaRegSave}onClick={handleDeleteVillain(villain)} /> <ButtonFooterlabel="Edit"IconClass={FaEdit}onClick={handleSelectVillain(villain.id)} /> </footer> </div> </li> ))} </ul> </div> );}
Bileşen testini güncelleyin ve bağlama sırasında context sağlayıcıyı da kullanın.
// src/villains/VillainList.cy.tsximport VillainList from"./VillainList";import"../styles.scss";import villains from"../../cypress/fixtures/villains.json";import VillainsContext from"./VillainsContext";describe("VillainList", () => {it("no villains should not display a list nor search bar", () => {cy.wrappedMount( <VillainListhandleDeleteVillain={cy.stub().as("handleDeleteVillain")} /> );cy.getByCy("villain-list").should("exist");cy.getByCyLike("villain-list-item").should("not.exist");cy.getByCy("search").should("not.exist"); });context("with villains in the list", () => {beforeEach(() => {cy.wrappedMount( <VillainsContext.Providervalue={villains}> <VillainListhandleDeleteVillain={cy.stub().as("handleDeleteVillain")} /> </VillainsContext.Provider> ); });it("should render the villain layout", () => {cy.getByCyLike("villain-list-item").should("have.length",villains.length );cy.getByCy("card-content");cy.contains(villains[0].name);cy.contains(villains[0].description);cy.get("footer").first().within(() => {cy.getByCy("delete-button");cy.getByCy("edit-button"); }); });it("should search and filter villain by name and description", () => {cy.getByCy("search").type(villains[0].name);cy.getByCyLike("villain-list-item").should("have.length",1).contains(villains[0].name);cy.getByCy("search").clear().type(villains[2].description);cy.getByCyLike("villain-list-item").should("have.length",1).contains(villains[2].description); });it("should handle delete", () => {cy.getByCy("delete-button").first().click();cy.get("@handleDeleteVillain").should("have.been.called"); });it("should handle edit", () => {cy.getByCy("edit-button").first().click();cy.location("pathname").should("eq","/villains/edit-villain/"+ villains[0].id ); }); });});
RTL testini, bağlam sağlayıcıyı kullanarak renderlarken de güncelleyin.
// src/villains/VillainList.test.tsximport VillainList from"./VillainList";import { wrappedRender, screen, waitFor } from"test-utils";import userEvent from"@testing-library/user-event";import { villains } from"../../db.json";import VillainsContext from"./VillainsContext";describe("VillainList", () => {consthandleDeleteVillain=jest.fn();it("no villains should not display a list nor search bar",async () => {wrappedRender(<VillainListhandleDeleteVillain={handleDeleteVillain} />);expect(awaitscreen.findByTestId("villain-list")).toBeInTheDocument();expect(screen.queryByTestId("villain-list-item-1")).not.toBeInTheDocument();expect(screen.queryByTestId("search-bar")).not.toBeInTheDocument(); });describe("with villains in the list", () => {beforeEach(() => {wrappedRender( <VillainsContext.Providervalue={villains}> <VillainListhandleDeleteVillain={handleDeleteVillain} /> </VillainsContext.Provider> ); });constcardContents=async () =>screen.findAllByTestId("card-content");constdeleteButtons=async () =>screen.findAllByTestId("delete-button");consteditButtons=async () =>screen.findAllByTestId("edit-button");it("should render the villain layout",async () => {expect(awaitscreen.findByTestId(`villain-list-item-${villains.length-1}`) ).toBeInTheDocument();expect(awaitscreen.findByText(villains[0].name)).toBeInTheDocument();expect(awaitscreen.findByText(villains[0].description) ).toBeInTheDocument();expect(awaitcardContents()).toHaveLength(villains.length);expect(awaitdeleteButtons()).toHaveLength(villains.length);expect(awaiteditButtons()).toHaveLength(villains.length); });it("should search and filter villain by name and description",async () => {constsearch=awaitscreen.findByTestId("search");userEvent.type(search, villains[0].name);awaitwaitFor(async () =>expect(awaitcardContents()).toHaveLength(1));awaitscreen.findByText(villains[0].name);userEvent.clear(search);awaitwaitFor(async () =>expect(awaitcardContents()).toHaveLength(villains.length) );userEvent.type(search, villains[2].description);awaitwaitFor(async () =>expect(awaitcardContents()).toHaveLength(1)); });it("should handle delete",async () => {userEvent.click((awaitdeleteButtons())[0]);expect(handleDeleteVillain).toHaveBeenCalled(); });it("should handle edit",async () => {userEvent.click((awaiteditButtons())[0]);awaitwaitFor(() =>expect(window.location.pathname).toEqual("/villains/edit-villain/"+ villains[0].id ) ); }); });});
Özel bir kanca kullanarak bağlamı paylaşma
VillainsContext'te oluşturulan bağlam, hakem görevi görüyor. Villains.tsx bağlamı, villains durumunu alt bileşeni VillainList'e iletmek için kullanır. VillainList durumunu VillainsContext'ten alır. VillainsContext yerine, içe aktarmaları azaltacak bir kanca kullanabiliriz.
src/villains/VillainsContext.ts dosyasını kaldırın ve bunun yerine src/hooks/useVillainsContext.ts adlı bir kanca oluşturun. Kancanın ilk yarısı VillainsContext ile aynıdır. Durumu ayarlamak için bir ayarlayıcı ekleriz, böylece durumu alan bileşenler de onu ayarlama yeteneği kazanır. Ayrıca kancanın işlevselliğiyle ilgili durumu ve etkileri kancanın içinde yönetir ve yalnızca bileşenlerin ihtiyaç duyduğu değer(ler)i döndürürüz.
// src/hooks/useVillainsContext.tsimport { createContext, useContext, SetStateAction, Dispatch } from"react";import { Villain } from"models/Villain";// Context api lets us pass a value deep into the component tree// without explicitly threading it through every component (2nd tier state management)constVillainsContext=createContext<Villain[]>([]);// to be used as VillainsContext.Provider,// takes a prop as `value`, which is the context/data/state to shareexportdefault VillainsContext;constVillainsSetContext= createContext<Dispatch< SetStateAction<Villain[] |null>>|null>(null);// Manage state and effects related to a hook’s functionality// within the hook and return only the value(s) that components needexportfunctionuseVillainsContext() {constvillains=useContext(VillainsContext);constsetVillains=useContext(VillainsSetContext);return [villains, setVillains] asconst;}
Villains.tsx'deki tek değişiklik, VillainsContext'in içe aktarma konumudur.
Benzer şekilde, VillainsList bileşen testinde yalnızca içe aktarma değişir. useVillainsContext adlı kancayı kullanıyoruz. Aynı durum VillainList.test.tsx için de geçerlidir.
// src/villains/VillainList.cy.tsximport VillainList from"./VillainList";import"../styles.scss";import villains from"../../cypress/fixtures/villains.json";import VillainsContext from"hooks/useVillainsContext";describe("VillainList", () => {it("no villains should not display a list nor search bar", () => {cy.wrappedMount( <VillainListhandleDeleteVillain={cy.stub().as("handleDeleteVillain")} /> );cy.getByCy("villain-list").should("exist");cy.getByCyLike("villain-list-item").should("not.exist");cy.getByCy("search").should("not.exist"); });context("with villains in the list", () => {beforeEach(() => {cy.wrappedMount( <VillainsContext.Providervalue={villains}> <VillainListhandleDeleteVillain={cy.stub().as("handleDeleteVillain")} /> </VillainsContext.Provider> ); });it("should render the villain layout", () => {cy.getByCyLike("villain-list-item").should("have.length",villains.length );cy.getByCy("card-content");cy.contains(villains[0].name);cy.contains(villains[0].description);cy.get("footer").first().within(() => {cy.getByCy("delete-button");cy.getByCy("edit-button"); }); });it("should search and filter villain by name and description", () => {cy.getByCy("search").type(villains[0].name);cy.getByCyLike("villain-list-item").should("have.length",1).contains(villains[0].name);cy.getByCy("search").clear().type(villains[2].description);cy.getByCyLike("villain-list-item").should("have.length",1).contains(villains[2].description); });it("should handle delete", () => {cy.getByCy("delete-button").first().click();cy.get("@handleDeleteVillain").should("have.been.called"); });it("should handle edit", () => {cy.getByCy("edit-button").first().click();cy.location("pathname").should("eq","/villains/edit-villain/"+ villains[0].id ); }); });});
// src/villains/VillainList.test.tsximport VillainList from"./VillainList";import { wrappedRender, screen, waitFor } from"test-utils";import userEvent from"@testing-library/user-event";import { villains } from"../../db.json";import VillainsContext from"hooks/useVillainsContext";describe("VillainList", () => {consthandleDeleteVillain=jest.fn();it("no villains should not display a list nor search bar",async () => {wrappedRender(<VillainListhandleDeleteVillain={handleDeleteVillain} />);expect(awaitscreen.findByTestId("villain-list")).toBeInTheDocument();expect(screen.queryByTestId("villain-list-item-1")).not.toBeInTheDocument();expect(screen.queryByTestId("search-bar")).not.toBeInTheDocument(); });describe("with villains in the list", () => {beforeEach(() => {wrappedRender( <VillainsContext.Providervalue={villains}> <VillainListhandleDeleteVillain={handleDeleteVillain} /> </VillainsContext.Provider> ); });constcardContents=async () =>screen.findAllByTestId("card-content");constdeleteButtons=async () =>screen.findAllByTestId("delete-button");consteditButtons=async () =>screen.findAllByTestId("edit-button");it("should render the villain layout",async () => {expect(awaitscreen.findByTestId(`villain-list-item-${villains.length-1}`) ).toBeInTheDocument();expect(awaitscreen.findByText(villains[0].name)).toBeInTheDocument();expect(awaitscreen.findByText(villains[0].description) ).toBeInTheDocument();expect(awaitcardContents()).toHaveLength(villains.length);expect(awaitdeleteButtons()).toHaveLength(villains.length);expect(awaiteditButtons()).toHaveLength(villains.length); });it("should search and filter villain by name and description",async () => {constsearch=awaitscreen.findByTestId("search");userEvent.type(search, villains[0].name);awaitwaitFor(async () =>expect(awaitcardContents()).toHaveLength(1));awaitscreen.findByText(villains[0].name);userEvent.clear(search);awaitwaitFor(async () =>expect(awaitcardContents()).toHaveLength(villains.length) );userEvent.type(search, villains[2].description);awaitwaitFor(async () =>expect(awaitcardContents()).toHaveLength(1)); });it("should handle delete",async () => {userEvent.click((awaitdeleteButtons())[0]);expect(handleDeleteVillain).toHaveBeenCalled(); });it("should handle edit",async () => {userEvent.click((awaiteditButtons())[0]);awaitwaitFor(() =>expect(window.location.pathname).toEqual("/villains/edit-villain/"+ villains[0].id ) ); }); });});
VillainList.tsx'de, Villains.tsx'de olduğu gibi içe aktarma değişir. Ayrıca, useContext'i içe aktarmaya gerek kalmaz. Şimdi villains'i de yapılandırıyoruz; const [villains] = useVillainsContext(). Gerekirse, bağlamı ayarlamak için kancadan ayarlayıcıyı da alabiliriz.
// src/villains/VillainList.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 { useVillainsContext } from"hooks/useVillainsContext";import { Villain } from"models/Villain";typeVillainListProps= {handleDeleteVillain: ( villain:Villain ) => (e:MouseEvent<HTMLButtonElement>) =>void;};exportdefaultfunctionVillainList({ handleDeleteVillain }:VillainListProps) {const [villains] =useVillainsContext();constdeferredVillains=useDeferredValue(villains);constisStale= deferredVillains !== villains;const [filteredVillains,setFilteredVillains] =useState(deferredVillains);constnavigate=useNavigate();const [isPending,startTransition] =useTransition();// needed to refresh the list after deleting a villainuseEffect(() =>setFilteredVillains(deferredVillains), [deferredVillains]);// currying: the outer fn takes our custom arg and returns a fn that takes the eventconsthandleSelectVillain= (villainId:string) => () => {constvillain=deferredVillains.find((h:Villain) =>h.id === villainId);navigate(`/villains/edit-villain/${villain?.id}?name=${villain?.name}&description=${villain?.description}` ); };typeVillainProperty=|Villain["name"]|Villain["description"]|Villain["id"];/** returns a boolean whether the villain properties exist in the search field */constsearchExists= (searchProperty:VillainProperty, 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:Villain[]) => [...data].filter((item:Villain) =>Object.values(item).find((property:VillainProperty) =>searchExists(property, searchField) ) );/** filters the villains data to see if the any of the properties exist in the list */consthandleSearch= (data:Villain[]) => (event:ChangeEvent<HTMLInputElement>) => {constsearchField=event.target.value;returnstartTransition(() =>setFilteredVillains(searchProperties(searchField, data)) ); };return ( <divstyle={{ opacity: isPending ?0.5:1, color: isStale ?"dimgray":"black", }} > {deferredVillains.length>0&& ( <divclassName="card-content"> <span>Search </span> <inputdata-cy="search"onChange={handleSearch(deferredVillains)} /> </div> )} <uldata-cy="villain-list"className="list"> {filteredVillains.map((villain, index) => ( <lidata-cy={`villain-list-item-${index}`} key={villain.id}> <divclassName="card"> <CardContentname={villain.name}description={villain.description} /> <footerclassName="card-footer"> <ButtonFooterlabel="Delete"IconClass={FaRegSave}onClick={handleDeleteVillain(villain)} /> <ButtonFooterlabel="Edit"IconClass={FaEdit}onClick={handleSelectVillain(villain.id)} /> </footer> </div> </li> ))} </ul> </div> );}
Özet ve Çıkarımlar
Context API, değeri her bileşenden açıkça geçirmeye gerek kalmadan bileşen ağacının derinliklerine geçirmemizi sağlar.
Özel bir kanca kullanın, durumu ve etkileri kancanın içinde yönetin ve bileşenlerin ihtiyaç duyduğu değerleri döndürün, bu değer ve setValue olabilir.
cy.intercept ve MSW ile, bir ağ isteğinin dışarı çıktığını kontrol ettik ve işlemin bir kancanın çağrılmasına neden olduğunu kontrol ettik. Sonuç olarak, kancaların değiştirilmesinin testler üzerinde veya işlevsellik üzerinde hiçbir etkisi olmadı. İşte bu yüzden biraz daha yüksek bir soyutlama düzeyinde test etmek istiyoruz ve uygulamanın sonuçlarını uygulama ayrıntılarına kıyasla doğrulamak istiyoruz.