Abbiamo visto che uno dei concetti cardine su cui si basa React è il flusso unidirezionale di informazioni. Se immaginiamo la nostra appplicazione come una struttura gerarchica di componenti, i dati scorrono, utilizzando l’oggetto Props, dai componenti più in alto nella gerarchia verso il basso. Può succedere che un componente voglia modificare l’oggetto State del componente padre o debba condividere della informazioni con un altro componente ad esso adiacente.
Realizzeremo un semplice esempio per illustrare quanto appena descritto.
Abbiamo un ComponenteA che ha come componenti figli B, C e D. All’interno di C e D sarà presente un contatore che sarà incrementato col pulsante "Aggiungi 1" dal Componente B. Il componente A si farà carico di mantenere al suo interno un oggetto State in cui verrà salvato il valore del contatore.
const Contatore = ({nome, valore}) => {
return (
<div className="counter">
<h2>{nome}</h2>
<p>{valore}</p>
</div>
);
};
const ComponenteB = props => {
return (
<div>
<h2>Componente B</h2>
<button onClick={props.aggiorna}>Aggiungi 1</button>
</div>
)
}
class ComponenteA extends React.Component {
constructor(props) {
super(props);
this.state = {contatore: 0};
this.contatori = ["ContatoreC", "ContatoreD"];
this.aggiorna = this.aggiorna.bind(this);
}
aggiorna() {
this.setState((prevState, props) => ({
contatore: prevState.contatore + 1
})
);
}
render() {
const contatori = this.contatori.map(contatore =>
<Contatore key={contatore} nome={contatore} valore={this.state.contatore} />
);
return (
<div>
<h1>ComponenteA</h1>
<div>
<ComponenteB aggiorna={this.aggiorna} />
{contatori}
</div>
</div>
);
}
}
Abbiamo passato al componenteB la funzione aggiorna() del componenteA. Nel costruttore del ComponenteA abbiamo invocato this.aggiorna.bind(), come abbiamo spiegato nell’articolo precedente. Ogni volta che clicchiamo sul pusante all’interno del componenteB, verrà aggiornato il valore di state.contatore presente nel componenteA e il valore aggiornato sarà poi passato ai due contatori (ContatoreC, ContatoreD) come mostrato nell’immagine qui sotto.
Vediamo una variazione dell’esempio di sopra. Supponiamo di voler aggiungere più pulsanti che sommeranno ciascuno un valore diverso al contatore. Supponiamo, inoltre, di voler aggiungere questi pulsanti in maniera dinamica e quindi di volerli inserire come discendenti dell’elemento <ComponentA></ComponentA>. Avremo quindi un componente App il cui metodo render() conterrà un codice come il seguente.
<ComponenteA>
<Pulsante incremento={1} />
<Pulsante incremento={3} />
<Pulsante incremento={5} />
</ComponenteA>
Modifichiamo allora l’esempio di sopra. Utlilizzeremo React.Children.map() e React.cloneElement().
const rootNode = document.querySelector('.root');
const Contatore = ({nome, valore}) => {
return (
<div className="counter">
<h2>{nome}</h2>
<p>{valore}</p>
</div>
);
};
const Pulsante = props =>
<button onClick={props.onClick}>Aggiungi {props.incremento}</button>;
class ComponenteA extends React.Component {
constructor(props) {
super(props);
this.state = {contatore: 0};
this.contatori = ["ContatoreC", "ContatoreD"];
this.pulsanti = React.Children.map(props.children, child =>
React.cloneElement(child, {
onClick: this.aggiorna.bind(this, child.props.incremento)
})
);
}
aggiorna(incremento) {
this.setState((prevState, props) => ({
contatore: prevState.contatore + incremento
})
);
}
render() {
const contatori = this.contatori.map(contatore =>
<Contatore key={contatore} nome={contatore} valore={this.state.contatore} />
);
return (
<div>
<h1>ComponenteA</h1>
<div>
{this.pulsanti}
{contatori}
</div>
</div>
);
}
}
const App = props => {
return (
<ComponenteA>
<Pulsante incremento={1} />
<Pulsante incremento={3} />
<Pulsante incremento={5} />
</ComponenteA>
)
}
ReactDOM.render(<App />, rootNode);
I Pulsanti discendenti dell’elemento <ComponenteA></ComponenteA> saranno accessibili all’interno del ComponenteA attraverso this.props.children. Con la funzione map() creiamo una copia di ciascuno dei Pulsanti e passiamo come attributo onClick il metodo aggiorna(), definito all’interno del ComponenteA. Il metodo aggiorna() riceverà come primo parametro la proprietà incremento di ciascun Pulsante. All’interno di Pulsante passiamo la proprietà onClick al componente nativo button. Quando cliccheremo su ciascuno dei pulsanti, verrà quindi invocata la funzione aggiorna() con il rispettivo valore di incremento passato come argomento. Sarà quindi aggiornato l’oggetto state e il nuovo valore sarà passato ai componenti ContatoreC e ContatoreD.
Passare le informazioni con React Context
Quando realizziamo le nostre applicazioni in React, si viene a formare una gerarchia di componenti. Possiamo rappresentare la struttura delle nostre applicazioni con un albero in cui ogni nodo è costituito da un componente. In alcune situazioni, capita di dover passare delle informazioni dalla radice alle foglie dell’albero. Detto in altri termini dovremo passare le proprietà dal componente più esterno a quelli più interni.
Con riferimento all’immagine, sarebbe più conveniente se potessimo accedere alle informazioni presenti nel componente App direttamente dal Componente5. Proprio per questo tipo di situazioni, React mette a disposizione la Context API. Vediamo subito un esempio.
const rootNode = document.querySelector('.root');
const DummyComponent = props => <div>{props.children}</div>
class App extends React.Component {
/*
* dobbiamo definire una funzione getChildContext() che restituisce
* le proprietà che saranno disponibili nell'oggetto 'context'
*
*/
getChildContext() {
return {messaggioViaContext: "Info passata via Context"}
}
render() {
return (
<div>
{this.props.children}
</div>
);
}
}
/*
* Definiamo il tipo di ciascuna proprietà dell'oggetto 'context'
* NOTA: A partire da React v15.5 React.PropTypes è stato deprecato,
* utilizzare la libreria prop-types (https://www.npmjs.com/package/prop-types)
* In questo esempio utilizziamo React v15.4.2
*/
App.childContextTypes = {
messaggioViaContext: React.PropTypes.string
}
class ComponenteCheUtilizzaContext extends React.Component {
render() {
return (
/* Usiamo l'oggetto context */
<p>{this.context.messaggioViaContext}</p>
);
}
}
/*
* Dobbiamo definire l'oggetto contextTypes per il componente
* che userà l'oggetto context.
*/
ComponenteCheUtilizzaContext.contextTypes = {
messaggioViaContext: React.PropTypes.string
}
const Root = (
<App>
<DummyComponent>
<DummyComponent>
<ComponenteCheUtilizzaContext />
</DummyComponent>
</DummyComponent>
</App>
);
ReactDOM.render(Root, rootNode);
I passi per usare la Context API sono:
- definire il metodo getChildContext() nel componente (Context Provider – il componente App nell’esempio) da cui vogliamo passare delle informazioni. Il metodo getChildContext() restituisce un oggetto con le proprietà che vogliamo siano disponibili nell’oggetto context che poi useremo in un altro componente.
- Definire ContextProvider.childContextTypes (App.childContextTypes nell’esempio)
- Definire contextTypes (ComponenteCheUtilizzaContext.contextTypes) nel componente in cui vorremmo usare l’oggetto context.
- Accedere all’oggetto context all’interno del componente figlio.
Il risultato del nostro esempio è mostrato nell’immagine sottostante.
Se definiamo contextTypes in un componente (il componente che userà Context), alcuni dei metodi visti quando abbiamo parlato del ciclo di vita di un componente riceveranno un argomento in più, ovvero context. I metodi sono i seguenti:
- constructor(props, context)
- componentWillReceiveProps(nextProps, nextContext)
- shouldComponentUpdate(nextProps, nextState, nextContext)
- componentWillUpdate(nextProps, nextState, nextContext)
- componentDidUpdate(prevProps, prevState, prevContext)
Uso della prop Ref
Come ultimo esempio, vediamo l’uso della proprietà ref a cui passeremo una funzione la quale sarà eseguita subito dopo che vengono invocate le funzioni componentDidMount() e componentWillUnmount() del componente. Quando ref è usata su un elemento avente un corrispettivo elemento HTML, la funzione passata a ref riceve come argomento l’elemento DOM corrispondente. Se invece ref è usata su un elemento costruito utilizzando un Class Component, la funzione passata a ref riceverà come argomento un’istanza del componente.
class App extends React.Component {
constructor(props) {
super(props);
this.esegui = this.esegui.bind(this);
}
esegui() {
this.element.esegui();
}
render() {
return (
<div>
<ComponenteA ref={elem => {this.element = elem}} />
<button onClick={this.esegui} >Chiama funzione</button>
</div>
);
}
}
class ComponenteA extends React.Component {
constructor(props) {
super(props);
}
esegui() {
// Metodo esegui() del ComponenteA
console.log("Metodo esegui() del " + this.constructor.name);
}
render() {
return null;
}
}
// app è un'istanza del componente App
const app = ReactDOM.render(<App />, rootNode);
All’interno del metodo render() del componente App, passiamo all’attributo ref dell’elemento <ComponentA></ComponentA> una funzione e salviamo in this.element un riferimento all’istanza del ComponenteA. Quando effettueremo un click sul pulsante "Chiama funzione", eseguiamo il metodo esegui del componente App che a sua volta invocherà il metodo this.element.esegui in cui stampiamo un messaggio nella console del browser.
Nel prossimo articolo vedremo come utilizzare i form, vari elementi input e textarea e le differenze in React rispetto ai rispettivi elementi del DOM.