Syvällinen sukellus Reactin komponenttiarkkitehtuuriin, jossa vertaillaan koostamista ja periytymistä. Opi, miksi React suosii koostamista ja tutustu malleihin, kuten HOC:eihin, Render Props -ominaisuuksiin ja Hookeihin skaalautuvien, uudelleenkäytettävien komponenttien rakentamiseksi.
React-komponenttiarkkitehtuuri: Miksi koostaminen voittaa periytymisen
Ohjelmistokehityksen maailmassa arkkitehtuuri on ensiarvoisen tärkeää. Tapa, jolla rakennamme koodimme, määrittää sen skaalautuvuuden, ylläpidettävyyden ja uudelleenkäytettävyyden. Reactia käyttäville kehittäjille yksi perustavanlaatuisimmista arkkitehtonisista päätöksistä pyörii sen ympärillä, miten logiikkaa ja käyttöliittymää jaetaan komponenttien välillä. Tämä tuo meidät klassiseen keskusteluun olio-ohjelmoinnissa, joka on kuviteltu uudelleen Reactin komponenttipohjaista maailmaa varten: Koostaminen vs. Periytyminen.
Jos sinulla on tausta klassisissa olio-ohjelmointikielissä, kuten Javassa tai C++:ssa, periytyminen voi tuntua luonnolliselta ensivalinnalta. Se on tehokas konsepti 'on-a' -suhteiden luomiseen. Kuitenkin virallinen React-dokumentaatio tarjoaa selkeän ja vahvan suosituksen: "Facebookissa käytämme Reactia tuhansissa komponenteissa, emmekä ole löytäneet mitään käyttötapauksia, joissa suosittelisimme komponenttiperiytymishierarkioiden luomista."
Tämä postaus tarjoaa kattavan tarkastelun tästä arkkitehtonisesta valinnasta. Avaamme, mitä periytyminen ja koostaminen tarkoittavat React-kontekstissa, osoitamme, miksi koostaminen on idiomaattinen ja parempi lähestymistapa, ja tutustumme tehokkaisiin malleihin – korkeamman asteen komponenteista moderneihin Hookeihin – jotka tekevät koostamisesta kehittäjän parhaan ystävän luotaessa vahvoja ja joustavia sovelluksia globaalille yleisölle.
Vanhan kaartin ymmärtäminen: Mitä periytyminen on?
Periytyminen on olio-ohjelmoinnin (OOP) peruspilari. Se mahdollistaa uuden luokan (aliluokan tai lapsen) hankkia olemassa olevan luokan (yliluokan tai vanhemman) ominaisuudet ja metodit. Tämä luo tiiviisti sidotun 'on-a'-suhteen. Esimerkiksi GoldenRetriever
on Dog
, joka on Animal
.
Periytyminen ei-React-kontekstissa
Katsotaan yksinkertaista JavaScript-luokkaesimerkkiä konseptin vakiinnuttamiseksi:
class Animal {
constructor(name) {
this.name = name;
}
speak() {
console.log(`${this.name} tekee ääntä.`);
}
}
class Dog extends Animal {
constructor(name, breed) {
super(name); // Kutsuu vanhemman konstruktoria
this.breed = breed;
}
speak() { // Ohittaa vanhemman metodin
console.log(`${this.name} haukkuu.`);
}
fetch() {
console.log(`${this.name} hakee palloa!`);
}
}
const myDog = new Dog('Buddy', 'Golden Retriever');
myDog.speak(); // Output: "Buddy haukkuu."
myDog.fetch(); // Output: "Buddy hakee palloa!"
Tässä mallissa Dog
-luokka saa automaattisesti name
-ominaisuuden ja speak
-metodin Animal
-luokasta. Se voi myös lisätä omia metodejaan (fetch
) ja ohittaa olemassa olevia. Tämä luo jäykän hierarkian.
Miksi periytyminen kompastuu Reactissa
Vaikka tämä 'on-a'-malli toimii joillekin tietorakenteille, se luo merkittäviä ongelmia, kun sitä sovelletaan Reactin käyttöliittymäkomponentteihin:- Tiukka kytkentä: Kun komponentti perii peruskomponentista, se kytkeytyy tiiviisti vanhempansa toteutukseen. Muutos peruskomponentissa voi yllättäen rikkoa useita alikomponentteja ketjussa. Tämä tekee uudelleenjärjestelystä ja ylläpidosta hauraan prosessin.
- Joustamaton logiikan jakaminen: Entä jos haluat jakaa tietyn toiminnon, kuten tietojen hakemisen, sellaisten komponenttien kanssa, jotka eivät sovi samaan 'on-a'-hierarkiaan? Esimerkiksi
UserProfile
jaProductList
saattavat molemmat tarvita tietojen hakemista, mutta niillä ei ole järkeä periä yhteisestäDataFetchingComponent
-komponentista. - Prop-läpivalumisen helvetti: Syvässä periytymisketjussa on vaikeaa välittää props-ominaisuuksia ylimmän tason komponentista syvälle sisäkkäiseen lapseen. Saatat joutua välittämään props-ominaisuuksia välissä olevien komponenttien kautta, jotka eivät edes käytä niitä, mikä johtaa sekavaan ja paisuneeseen koodiin.
- "Gorilla-Banana -ongelma": Kuuluisa lainaus OOP-asiantuntijalta Joe Armstrongilta kuvaa tätä ongelmaa täydellisesti: "Halusit banaania, mutta mitä sait, oli gorilla, jolla oli banaani ja koko viidakko." Periytymisen avulla et voi vain saada haluamaasi toiminnallisuutta; sinut pakotetaan tuomaan koko yläluokka mukana.
Näiden ongelmien takia React-tiimi suunnitteli kirjaston joustavammaksi ja tehokkaammaksi paradigmaksi: koostamiseksi.
React-tavan omaksuminen: Koostamisen voima
Koostaminen on suunnitteluperiaate, joka suosii 'has-a'- tai 'uses-a'-suhdetta. Sen sijaan, että komponentti olisi toinen komponentti, sillä on muita komponentteja tai se käyttää niiden toimintoja. Komponentteja käsitellään rakennuspalikoina – kuten LEGO-tiiliä – jotka voidaan yhdistää eri tavoilla monimutkaisten käyttöliittymien luomiseksi joutumatta lukittuna jäykkään hierarkiaan.
Reactin koostemalli on uskomattoman monipuolinen, ja se ilmenee useissa keskeisissä malleissa. Katsotaan niitä yksinkertaisimmasta moderneimpaan ja tehokkaimpaan.
Tekniikka 1: Sisältäminen `props.children` avulla
Yksinkertaisin koostamisen muoto on sisältäminen. Tässä komponentti toimii yleisenä säiliönä tai 'laatikona', ja sen sisältö välitetään vanhempikomponentista. Reactilla on tätä varten erityinen, sisäänrakennettu prop: props.children
.
Kuvittele, että tarvitset Card
-komponentin, joka voi paketoida minkä tahansa sisällön johdonmukaisella reunuksella ja varjolla. Sen sijaan, että loisit TextCard
-, ImageCard
- ja ProfileCard
-variantteja periytymällä, luot yhden yleisen Card
-komponentin.
// Card.js - Yleinen säiliökomponentti
function Card(props) {
return (
<div className="card">
{props.children}
</div>
);
}
// App.js - Card-komponentin käyttö
function App() {
return (
<div>
<Card>
<h1>Tervetuloa!</h1>
<p>Tämä sisältö on Card-komponentin sisällä.</p>
</Card>
<Card>
<img src="/path/to/image.jpg" alt="Esimerkkikuva" />
<p>Tämä on kuvakortti.</p>
</Card>
</div>
);
}
Card
-komponentti ei tiedä tai välitä siitä, mitä se sisältää. Se yksinkertaisesti tarjoaa kääretyylin. Sisältö avaavien ja sulkevien <Card>
-tunnisteiden välillä välitetään automaattisesti props.children
-ominaisuutena. Tämä on kaunis esimerkki irrottamisesta ja uudelleenkäytettävyydestä.
Tekniikka 2: Erikoistuminen Props-ominaisuuksilla
Joskus komponentti tarvitsee useita 'reikiä', jotka täytetään muilla komponenteilla. Vaikka voisit käyttää props.children
-ominaisuutta, selkeämpi ja jäsennellympi tapa on välittää komponentteja tavallisina props-ominaisuuksina. Tätä mallia kutsutaan usein erikoistumiseksi.
Harkitse Modal
-komponenttia. Modalilla on tyypillisesti otsikko-osio, sisältö-osio ja toimintaosio (painikkeilla, kuten "Vahvista" tai "Peruuta"). Voimme suunnitella Modal
-komponenttimme hyväksymään nämä osiot props-ominaisuuksina.
// Modal.js - Erikoistuneempi säiliö
function Modal(props) {
return (
<div className="modal-backdrop">
<div className="modal-content">
<div className="modal-header">{props.title}</div>
<div className="modal-body">{props.body}</div>
<div className="modal-footer">{props.actions}</div>
</div>
</div>
);
}
// App.js - Modalin käyttö erityisillä komponenteilla
function App() {
const confirmationTitle = <h2>Vahvista toiminto</h2>;
const confirmationBody = <p>Oletko varma, että haluat jatkaa tätä toimintoa?</p>;
const confirmationActions = (
<div>
<button>Vahvista</button>
<button>Peruuta</button>
</div>
);
return (
<Modal
title={confirmationTitle}
body={confirmationBody}
actions={confirmationActions}
/>
);
}
Modal
on erittäin uudelleenkäytettävä asettelukomponentti. Erikoistamme sitä välittämällä erityisiä JSX-elementtejä sen title
-, body
- ja actions
-ominaisuuksille. Tämä on paljon joustavampaa kuin luoda ConfirmationModal
- ja WarningModal
-aliluokkia. Yksinkertaisesti koostamme Modal
-komponentin eri sisällöllä tarpeen mukaan.
Tekniikka 3: Korkeamman asteen komponentit (HOC:t)
Ei-käyttöliittymälogiikan, kuten tietojen noutamisen, todennuksen tai lokituksen, jakamiseksi React-kehittäjät kääntyivät historiallisesti mallin puoleen, jota kutsutaan korkeamman asteen komponenteiksi (HOC:t). Vaikka Hookit ovat suurelta osin korvanneet ne modernissa Reactissa, on ratkaisevan tärkeää ymmärtää ne, koska ne edustavat keskeistä kehitysaskelta Reactin koostamistarinaan ja ovat edelleen olemassa monissa koodikannoissa.
HOC on funktio, joka ottaa komponentin argumenttina ja palauttaa uuden, parannetun komponentin.
Luodaan HOC nimeltä withLogger
, joka kirjaa komponentin props-ominaisuudet aina, kun se päivittyy. Tämä on hyödyllistä virheenkorjaukseen.
// withLogger.js - HOC
import React, { useEffect } from 'react';
function withLogger(WrappedComponent) {
// Se palauttaa uuden komponentin...
return function EnhancedComponent(props) {
useEffect(() => {
console.log('Komponentti päivitetty uusilla props-ominaisuuksilla:', props);
}, [props]);
// ... joka renderöi alkuperäisen komponentin alkuperäisillä props-ominaisuuksilla.
return <WrappedComponent {...props} />;
};
}
// MyComponent.js - Parannettava komponentti
function MyComponent({ name, age }) {
return (
<div>
<h1>Hei, {name}!</h1>
<p>Olet {age} vuotta vanha.</p>
</div>
);
}
// Parannetun komponentin vienti
export default withLogger(MyComponent);
withLogger
-funktio kietoo MyComponent
-komponentin, antaen sille uusia lokitusominaisuuksia muokkaamatta MyComponent
-komponentin sisäistä koodia. Voisimme soveltaa tätä samaa HOC-komponenttia mihin tahansa muuhun komponenttiin antaaksemme sille saman lokitusominaisuuden.
HOC:ien haasteet:
- Wrapper Hell: Useiden HOC:ien soveltaminen yhteen komponenttiin voi johtaa syvästi sisäkkäisiin komponentteihin React DevToolsissa (esim.
withAuth(withRouter(withLogger(MyComponent)))
), mikä vaikeuttaa virheenkorjausta. - Prop-nimeämisen törmäykset: Jos HOC injektoi prop-ominaisuuden (esim.
data
), jota wrapped-komponentti jo käyttää, se voidaan vahingossa korvata. - Implisiittinen logiikka: Ei ole aina selvää komponentin koodista, mistä sen props-ominaisuudet tulevat. Logiikka on piilotettu HOC:n sisällä.
Tekniikka 4: Render Props -ominaisuudet
Render Prop -malli syntyi ratkaisuksi joihinkin HOC:ien puutteisiin. Se tarjoaa selkeämmän tavan jakaa logiikkaa.
Komponentti, jolla on render prop, ottaa funktion prop-ominaisuutena (yleensä nimeltään render
) ja kutsuu tätä funktiota määrittääkseen, mitä renderöidä, välittäen sille kaikki tilat tai logiikan argumentteina.
Luodaan MouseTracker
-komponentti, joka seuraa hiiren X- ja Y-koordinaatteja ja tekee ne saataville kaikille komponenteille, jotka haluavat käyttää niitä.
// MouseTracker.js - Komponentti, jossa on render prop
import React, { useState, useEffect } from 'react';
function MouseTracker({ render }) {
const [position, setPosition] = useState({ x: 0, y: 0 });
const handleMouseMove = (event) => {
setPosition({ x: event.clientX, y: event.clientY });
};
useEffect(() => {
window.addEventListener('mousemove', handleMouseMove);
return () => {
window.removeEventListener('mousemove', handleMouseMove);
};
}, []);
// Kutsu render-funktiota tilan kanssa
return render(position);
}
// App.js - MouseTrackerin käyttö
function App() {
return (
<div>
<h1>Siirrä hiirtäsi ympärillä!</h1>
<MouseTracker
render={mousePosition => (
<p>Nykyinen hiiren sijainti on ({mousePosition.x}, {mousePosition.y})</p>
)}
/>
</div>
);
}
MouseTracker
kapseloi kaiken hiiren liikkeen seurantaan liittyvän logiikan. Se ei renderöi mitään itsessään. Sen sijaan se delegoi renderöintilogiikan sen render
-ominaisuudelle. Tämä on selkeämpää kuin HOC:it, koska näet täsmälleen, mistä mousePosition
-tiedot tulevat suoraan JSX:n sisällä.
children
-ominaisuutta voidaan myös käyttää funktiona, mikä on yleinen ja elegantti muunnelma tästä mallista:
// Käyttämällä lapsia funktiona
<MouseTracker>
{mousePosition => (
<p>Nykyinen hiiren sijainti on ({mousePosition.x}, {mousePosition.y})</p>
)}
</MouseTracker>
Tekniikka 5: Hookit (Moderni ja suositeltava lähestymistapa)
React 16.8:ssa esitellyt Hookit mullistivat tavan, jolla kirjoitamme React-komponentteja. Ne mahdollistavat tilan ja muiden React-ominaisuuksien käytön funktionaalisissa komponenteissa. Tärkeintä on, että mukautetut Hookit tarjoavat elegantimman ja suoremman ratkaisun tilallisen logiikan jakamiseen komponenttien välillä.
Hookit ratkaisevat HOC:ien ja Render Propsien ongelmat paljon siistimmällä tavalla. Uudistetaan MouseTracker
-esimerkki mukautetuksi Hookiksi nimeltä useMousePosition
.
// hooks/useMousePosition.js - Mukautettu Hook
import { useState, useEffect } from 'react';
export function useMousePosition() {
const [position, setPosition] = useState({ x: 0, y: 0 });
useEffect(() => {
const handleMouseMove = (event) => {
setPosition({ x: event.clientX, y: event.clientY });
};
window.addEventListener('mousemove', handleMouseMove);
return () => {
window.removeEventListener('mousemove', handleMouseMove);
};
}, []); // Tyhjä riippuvuustaulukko tarkoittaa, että tämä efekti suoritetaan vain kerran
return position;
}
// DisplayMousePosition.js - Hookia käyttävä komponentti
import { useMousePosition } from './hooks/useMousePosition';
function DisplayMousePosition() {
const position = useMousePosition(); // Kutsu vain Hookia!
return (
<p>
Hiiren sijainti on ({position.x}, {position.y})
</p>
);
}
// Toinen komponentti, ehkä interaktiivinen elementti
import { useMousePosition } from './hooks/useMousePosition';
function InteractiveBox() {
const { x, y } = useMousePosition();
const style = {
position: 'absolute',
top: y - 25, // Keskitä laatikko kursoriin
left: x - 25,
width: '50px',
height: '50px',
backgroundColor: 'lightblue',
};
return <div style={style} />;
}
Tämä on valtava parannus. Ei ole 'wrapper helliä', prop-nimien törmäyksiä eikä monimutkaisia render prop -funktioita. Logiikka on täysin irrotettu uudelleenkäytettäväksi funktioksi (useMousePosition
), ja mikä tahansa komponentti voi 'kiinnittyä' tähän tilalliseen logiikkaan yhdellä, selkeällä koodirivillä. Mukautetut Hookit ovat koostamisen äärimmäinen ilmaus modernissa Reactissa, jonka avulla voit rakentaa oman kirjaston uudelleenkäytettävistä logiikkalohkoista.
Lyhyt vertailu: Koostaminen vs. Periytyminen Reactissa
Avainerojen yhteenvetona React-kontekstissa tässä on suora vertailu:
Näkökulma | Periytyminen (Anti-Pattern Reactissa) | Koostaminen (Suositellaan Reactissa) |
---|---|---|
<strong>Suhde</strong> | 'on-a'-suhde. Erikoistunut komponentti on versio peruskomponentista. | 'has-a'- tai 'uses-a'-suhde. Monimutkaisella komponentilla on pienempiä komponentteja tai se käyttää jaettua logiikkaa. |
<strong>Kytkentä</strong> | <strong>Korkea.</strong> Lapsikomponentit ovat tiiviisti sidoksissa niiden vanhempien toteutukseen. | <strong>Alhainen.</strong> Komponentit ovat riippumattomia ja niitä voidaan käyttää eri konteksteissa ilman muutoksia. |
<strong>Joustavuus</strong> | <strong>Alhainen.</strong> Jäykät, luokkapohjaiset hierarkiat vaikeuttavat logiikan jakamista eri komponenttipuissa. | <strong>Korkea.</strong> Logiikkaa ja käyttöliittymää voidaan yhdistellä ja käyttää uudelleen lukemattomilla tavoilla, kuten rakennuspalikoilla. |
<strong>Koodin uudelleenkäytettävyys</strong> | Rajoitettu ennalta määritettyyn hierarkiaan. Saat koko "gorillan", kun haluat vain "banaanin". | Erinomainen. Pieniä, kohdennettuja komponentteja ja Hookeja voidaan käyttää koko sovelluksessa. |
<strong>React-idiomi</strong> | React-tiimi ei suosittele. | Suositeltava ja idiomaattinen tapa React-sovellusten rakentamiseen. |
Johtopäätös: Ajattele koostamista
Koostamisen ja periytymisen välinen keskustelu on ohjelmistosuunnittelun perustavanlaatuinen aihe. Vaikka periytymisellä on paikkansa klassisessa OOP:ssä, käyttöliittymäkehityksen dynaaminen, komponenttipohjainen luonne tekee siitä huonon sopivuuden Reactille. Kirjasto suunniteltiin pohjimmiltaan omaksumaan koostamista.
Suosimalla koostamista saat:
- Joustavuutta: Kykyä sekoittaa ja sovittaa käyttöliittymää ja logiikkaa tarpeen mukaan.
- Ylläpidettävyyttä: Löyhästi kytketyt komponentit on helpompi ymmärtää, testata ja uudelleenjärjestää erillään.
- Skaalautuvuutta: Koostumispohjainen ajattelutapa kannustaa luomaan pienien, uudelleenkäytettävien komponenttien ja Hookien suunnittelujärjestelmän, jota voidaan käyttää suurten, monimutkaisten sovellusten rakentamiseen tehokkaasti.
Globaalina React-kehittäjänä koostamisen hallitseminen ei ole vain parhaiden käytäntöjen noudattamista – se on ymmärrystä ydinfilosofiasta, joka tekee Reactista niin tehokkaan ja tuottavan työkalun. Aloita luomalla pieniä, kohdennettuja komponentteja. Käytä props.children
-ominaisuutta yleisiin säiliöihin ja props-ominaisuuksia erikoistumiseen. Logiikan jakamiseen käytä ensin mukautettuja Hookeja. Ajattelemalla koostamista olet hyvällä matkalla rakentamaan tyylikkäitä, vahvoja ja skaalautuvia React-sovelluksia, jotka kestävät aikaa.