javascript - Skicka props till överordnad komponent i React.js


Finns det inget enkelt sätt att skicka ett barns props till dess förälder med hjälp av händelser, i React.js?

var Child = React.createClass({
  render: function() {
    <a onClick={this.props.onClick}>Click me</a>
  }
});

var Parent = React.createClass({
  onClick: function(event) {
    // event.component.props ?why is this not available?
  },
  render: function() {
    <Child onClick={this.onClick} />
  }
});

Jag vet att du kan använda kontrollerade komponenter för att skicka ett inputs värde, men det vore trevligt att skicka hela paketet. Ibland innehåller barnkomponenten en uppsättning information som du helst inte vill behöva leta upp.

Kanske finns det ett sätt att binda komponenten till händelsen?

UPPDATERING - 2015-09-01

Efter att ha använt React i över ett år, och inspirerad av Sebastien Lorbers svar, har jag kommit fram till att att skicka barnkomponenter som argument till funktioner i föräldrar inte faktiskt är Reacts sätt, och det var aldrig en bra idé. Jag har ändrat svaret.

Svar #1

Redigera: se de sista exemplen för uppdaterade ES6-exempel.

Detta svar hanterar helt enkelt fallet med direkt förälder-barn-relation. När förälder och barn potentiellt har många mellanhänder, kolla detta svar.

Andra lösningar missar poängen

Även om de fortfarande fungerar bra, saknar andra svar något mycket viktigt.

Finns det inte ett enkelt sätt att skicka ett barns props till dess förälder med hjälp av events i React.js?

Föräldern har redan den där child-propen!: om barnet har en prop, är det för att dess förälder gav den propen till barnet! Varför vill du att barnet ska skicka tillbaka propen till föräldern, när föräldern uppenbarligen redan har den propen?

Bättre implementering

Barn: det behöver verkligen inte vara mer komplicerat än så.

var Child = React.createClass({
  render: function () {
    return <button onClick={this.props.onClick}>{this.props.text}</button>;
  },
});

Förälder med ett barn: använder värdet det skickar till barnet

var Parent = React.createClass({
  getInitialState: function() {
     return {childText: "Click me! (parent prop)"};
  },
  render: function () {
    return (
      <Child onClick={this.handleChildClick} text={this.state.childText}/>
    );
  },
  handleChildClick: function(event) {
     // You can access the prop you pass to the children 
     // because you already have it! 
     // Here you have it in state but it could also be
     //  in props, coming from another parent.
     alert("The Child button text is: " + this.state.childText);
     // You can also access the target of the click here 
     // if you want to do some magic stuff
     alert("The Child HTML is: " + event.target.outerHTML);
  }
});

JsFiddle

Förälder med lista över barn: du har fortfarande allt du behöver på föräldern och behöver inte göra barnet mer komplicerat.

var Parent = React.createClass({
  getInitialState: function() {
     return {childrenData: [
         {childText: "Click me 1!", childNumber: 1},
         {childText: "Click me 2!", childNumber: 2}
     ]};
  },
  render: function () {
    var children = this.state.childrenData.map(function(childData,childIndex) {
        return <Child onClick={this.handleChildClick.bind(null,childData)} text={childData.childText}/>;
    }.bind(this));
    return <div>{children}</div>;
  },

  handleChildClick: function(childData,event) {
     alert("The Child button data is: " + childData.childText + " - " + childData.childNumber);
     alert("The Child HTML is: " + event.target.outerHTML);
  }
});

JsFiddle

Det är också möjligt att använda this.handleChildClick.bind(null,childIndex) och sedan använda this.state.childrenData[childIndex]

Observera att vi binder med ett null-sammanhang eftersom annars ger React en varning relaterad till dess automatbindningssystem. Att använda null betyder att du inte vill ändra funktionssammanhanget. Se även.

Om inkapsling och koppling i andra svar

Detta är för mig en dålig idé när det gäller koppling och kapsling:

var Parent = React.createClass({
  handleClick: function(childComponent) {
     // using childComponent.props
     // using childComponent.refs.button
     // or anything else using childComponent
  },
  render: function() {
    <Child onClick={this.handleClick} />
  }
});

Använda props: Som jag förklarade ovan har du redan props i föräldern, så det är meningslöst att skicka hela barnkomponenten för att komma åt props.

Använda refs: Du har redan klickmålet i händelsen, och i de flesta fall är detta tillräckligt. Dessutom kunde du ha använt en ref direkt på barnet:

<Child ref="theChild" .../>

Och få åtkomst till DOM-noden i föräldern med

React.findDOMNode(this.refs.theChild)

För mer avancerade fall där du vill få åtkomst till flera referenser av barnet i föräldern, kan barnet passera alla DOM-noder direkt i callbacken.

Komponenten har ett gränssnitt (props) och föräldern bör inte anta något om barnets inre funktion, inklusive dess inre DOM-struktur eller vilka DOM-noder den deklarerar referenser för. Att en förälder använder en referens av ett barn innebär att du starkt kopplar de två komponenterna.

För att illustrera problemet, tar jag detta citat om Shadow DOM, som används i webbläsare för att rendera saker som reglage, scrollbars, videospelare...:

De skapade en gräns mellan vad du, webbutvecklaren, kan nå och vad som anses vara implementationsdetaljer, och som därför är otillgängliga för dig. Webbläsaren kan dock vandra över denna gräns som den vill. Med denna gräns på plats kunde de bygga alla HTML-element med samma gamla goda webtekniker, av divs och spans precis som du skulle göra.

Problemet är att om du låter barnets implementationsdetaljer läcka in i föräldern, gör du det väldigt svårt att refaktorera barnet utan att påverka föräldern. Detta innebär att som biblioteksskapare (eller som webbläsarredaktör med Shadow DOM) är detta mycket farligt eftersom du låter klienten få åtkomst till för mycket, vilket gör det mycket svårt att uppgradera koden utan att bryta bakåtkompatibiliteten.

Om Chrome hade implementerat sin rullningslist så att klienten kunde få tillgång till de inre DOM-noderna i den rullningslisten, skulle detta innebära att klienten kan ha möjlighet att helt enkelt förstöra den rullningslisten, och att appar skulle gå sönder lättare när Chrome utför sin automatisk uppdatering efter att ha omstrukturerat rullningslisten... Istället ger de bara tillgång till vissa säkra saker som att anpassa vissa delar av rullningslisten med CSS.

Om att använda något annat

Att skicka hela komponenten i callbacken är farligt och kan få nybörjarutvecklare att göra väldigt konstiga saker som att kalla childComponent.setState(...) eller childComponent.forceUpdate(), eller att tilldela den nya variabler, inuti föräldern, vilket gör hela appen mycket svårare att förstå.

Redigera: ES6-exempel

Eftersom många människor nu använder ES6, här är samma exempel för ES6-syntax

Barnet kan vara mycket enkelt:

const Child = ({
  onClick, 
  text
}) => (
  <button onClick={onClick}>
    {text}
  </button>
)

Föräldern kan vara antingen en klass (och den kan eventuellt hantera tillståndet själv, men jag skickar det som props här:

class Parent1 extends React.Component {
  handleChildClick(childData,event) {
     alert("The Child button data is: " + childData.childText + " - " + childData.childNumber);
     alert("The Child HTML is: " + event.target.outerHTML);
  }
  render() {
    return (
      <div>
        {this.props.childrenData.map(child => (
          <Child
            key={child.childNumber}
            text={child.childText} 
            onClick={e => this.handleChildClick(child,e)}
          />
        ))}
      </div>
    );
  }
}

Men det kan också förenklas om det inte behöver hantera tillstånd:

const Parent2 = ({childrenData}) => (
  <div>
     {childrenData.map(child => (
       <Child
         key={child.childNumber}
         text={child.childText} 
         onClick={e => {
            alert("The Child button data is: " + child.childText + " - " + child.childNumber);
                    alert("The Child HTML is: " + e.target.outerHTML);
         }}
       />
     ))}
  </div>
)

JsFiddle

PRESTANDA VARNING (gäller för ES5/ES6): om du använder PureComponent eller shouldComponentUpdate, kommer ovanstående implementationer inte att optimeras som standard eftersom användning av onClick={e => doSomething()}, eller bindning direkt under renderingsfasen, skapar en ny funktion varje gång föräldern renderas. Om detta är en prestandaflaskhals i din app, kan du skicka data till barnen, och injicera den igen inne i en "stabil" callback (satt på föräldraklassen och bunden till this i klassens konstruktör) så att PureComponent-optimering kan träda i kraft, eller så kan du implementera din egen shouldComponentUpdate och ignorera callbacken i props-jämförelsechecken.

Du kan också använda Recompose-biblioteket, som tillhandahåller högre ordningens komponenter för att uppnå finjusterade optimeringar:

// A component that is expensive to render
const ExpensiveComponent = ({ propA, propB }) => {...}

// Optimized version of same component, using shallow comparison of props
// Same effect as React's PureRenderMixin
const OptimizedComponent = pure(ExpensiveComponent)

// Even more optimized: only updates if specific prop keys have changed
const HyperOptimizedComponent = onlyUpdateForKeys(['propA', 'propB'])(ExpensiveComponent)

I det här fallet skulle du kunna optimera Child-komponenten genom att använda:

const OptimizedChild = onlyUpdateForKeys(['text'])(Child)

Svar #2

Uppdatering (2015-09-01): Originalpostaren har gjort denna fråga till något av ett rörligt mål. Den har uppdaterats igen. Så, jag känner mig ansvarig att uppdatera mitt svar.

Först, ett svar på ditt angivna exempel:

Ja, detta är möjligt.

Du kan lösa detta genom att uppdatera Childs onClick till this.props.onClick.bind(null, this):

var Child = React.createClass({
  render: function () {
    return <a onClick={this.props.onClick.bind(null, this)}>Click me</a>;
  }
});

Händelsehanteraren i din förälder kan sedan komma åt komponenten och händelsen på följande sätt:

  onClick: function (component, event) {
    // console.log(component, event);
  },

JSBin-ögonblicksbild

Men själva frågan är vilseledande

Förälder vet redan Barnets props.

Detta är inte tydligt i det angivna exemplet eftersom inga props faktiskt tillhandahålls. Denna exempel kod kan bättre stödja den fråga som ställs:

var Child = React.createClass({
  render: function () {
    return <a onClick={this.props.onClick}> {this.props.text} </a>;
  }
});

var Parent = React.createClass({
  getInitialState: function () {
    return { text: "Click here" };
  },
  onClick: function (event) {
    // event.component.props ?why is this not available? 
  },
  render: function() {
    return <Child onClick={this.onClick} text={this.state.text} />;
  }
});

Det blir mycket tydligare i det här exemplet att du redan vet vilka props Child har.

JSBin-ögonblicksbild

Om det verkligen handlar om att använda ett barns rekvisita…

Om det verkligen handlar om att använda ett barns rekvisita kan du helt undvika all kontakt med barnet.

JSX har ett API för spridda attribut som jag ofta använder på komponenter som Child. Det tar alla props och applicerar dem på en komponent. Child skulle se ut så här:

var Child = React.createClass({
  render: function () {
    return <a {...this.props}> {this.props.text} </a>;
  }
});

Tillåter dig att använda värdena direkt i Föräldern:

var Parent = React.createClass({
  getInitialState: function () {
    return { text: "Click here" };
  },
  onClick: function (text) {
    alert(text);
  },
  render: function() {
    return <Child onClick={this.onClick.bind(null, this.state.text)} text={this.state.text} />;
  }
});

JSBin-ögonblicksbild

Och ingen ytterligare konfiguration krävs när du kopplar in ytterligare Child-komponenter

var Parent = React.createClass({
  getInitialState: function () {
    return {
      text: "Click here",
      text2: "No, Click here",
    };
  },
  onClick: function (text) {
    alert(text);
  },
  render: function() {
    return <div>
      <Child onClick={this.onClick.bind(null, this.state.text)} text={this.state.text} />
      <Child onClick={this.onClick.bind(null, this.state.text2)} text={this.state.text2} />
    </div>;
  }
});

JSBin-ögonblicksbild

Men jag misstänker att det inte är ditt verkliga användningsfall. Så låt oss gräva djupare…

Ett robust praktiskt exempel

Den generiska naturen hos det givna exemplet är svår att prata om. Jag har skapat en komponent som visar ett praktiskt användningsområde för frågan ovan, implementerad på ett mycket Reacty sätt:

DTServiceCalculator fungerande exempel
DTServiceCalculator repo

Denna komponent är en enkel servicekalkylator. Du förser den med en lista över tjänster (med namn och priser) och den kommer att beräkna en totalsumma för de valda priserna.

Barn är saligt okunniga

ServiceItem är barnkomponenten i detta exempel. Den har inte många åsikter om omvärlden. Den kräver några props, varav en är en funktion som ska anropas när den klickas.

<div onClick={this.props.handleClick.bind(this.props.index)} />

Det gör inget annat än att anropa den angivna handleClick-återanropningen med det angivna index[källa].

Föräldrar är barn

DTServicesCalculator är föräldrakomponenten i detta exempel. Den är också ett barn. Låt oss titta.

DTServiceCalculator skapar en lista av barnkomponenter (ServiceItems) och förser dem med props [source]. Det är föräldrakomponenten till ServiceItem men det är barnkomponenten till den komponent som skickar listan till den. Den äger inte datan. Så det delegerar återigen hanteringen av komponenten till sin föräldrakomponent source

<ServiceItem chosen={chosen} index={i} key={id} price={price} name={name} onSelect={this.props.handleServiceItem} />

handleServiceItem fångar upp indexet, som skickas från barnet, och tillhandahåller det till dess förälder [källa]

handleServiceClick (index) {
  this.props.onSelect(index);
}

Ägare vet allt

Begreppet "Ägande" är viktigt i React. Jag rekommenderar att läsa mer om det här.

I exemplet jag har visat fortsätter jag att delegera hanteringen av en händelse uppåt i komponentträdet tills vi når den komponent som äger tillståndet.

När vi väl kommer dit hanterar vi val/avval av tillstånd på följande sätt [källa]:

handleSelect (index) {
  let services = […this.state.services];
  services[index].chosen = (services[index].chosen) ? false : true;
  this.setState({ services: services });
}

Slutsats

Försök att hålla dina yttersta komponenter så ogenomskinliga som möjligt. Sträva efter att se till att de har väldigt få preferenser om hur en föräldrakomponent kan välja att implementera dem.

Håll koll på vem som äger de data du manipulerar. I de flesta fall behöver du delegera händelsehantering uppåt i trädet till den komponent som äger det tillståndet.

Sidnot: Flux-mönstret är ett bra sätt att minska denna typ av nödvändig anslutning i appar.

Svar #3

Det verkar finnas ett enkelt svar. Tänk på detta:

var Child = React.createClass({
  render: function() {
    <a onClick={this.props.onClick.bind(null, this)}>Click me</a>
  }
});

var Parent = React.createClass({
  onClick: function(component, event) {
    component.props // #=> {Object...}
  },
  render: function() {
    <Child onClick={this.onClick} />
  }
});

Nyckeln är att kalla bind(null, this) på this.props.onClick-händelsen, som skickas från föräldern. Nu accepterar onClick-funktionen argumenten component OCH event. Jag tror att det är det bästa av alla världar.

UPPDATERING: 2015-09-01

Detta var en dålig idé: att låta detaljer om barnimplementeringen läcka in till föräldern var aldrig en bra väg. Se Sebastien Lorbers svar.

Svar #4

Frågan är hur man skickar ett argument från barnkomponenten till föräldrakomponenten. Detta exempel är lätt att använda och testat:

//Child component
class Child extends React.Component {
    render() {
        var handleToUpdate  =   this.props.handleToUpdate;
        return (<div><button onClick={() => handleToUpdate('someVar')}>Push me</button></div>
        )
    }
}

//Parent component
class Parent extends React.Component {
    constructor(props) {
        super(props);
        var handleToUpdate  = this.handleToUpdate.bind(this);
    }

    handleToUpdate(someArg){
        alert('We pass argument from Child to Parent: \n' + someArg);
    }

    render() {
        var handleToUpdate  =   this.handleToUpdate;
        return (<div>
          <Child handleToUpdate = {handleToUpdate.bind(this)} />
        </div>)
    }
}

if(document.querySelector("#demo")){
    ReactDOM.render(
        <Parent />,
        document.querySelector("#demo")
    );
}

Titta på JSFIDDLE

Relaterade artiklar