¿Cómo puedo comunicarme entre componentes de reacción relacionados?
Recién comencé con ReactJS y estoy un poco atascado en un problema que tengo.
Mi aplicación es esencialmente una lista con filtros y un botón para cambiar el diseño. Por el momento estoy usando tres componentes: y <list />
, ahora, obviamente, cuando cambio la configuración quiero activar algún método para actualizar mi vista.< Filters />
<TopBar />
< Filters />
<list />
¿Cómo puedo hacer que esos 3 componentes interactúen entre sí o necesito algún tipo de modelo de datos global en el que pueda realizar cambios?
El mejor enfoque dependerá de cómo planee organizar esos componentes. Algunos escenarios de ejemplo que me vienen a la mente ahora mismo:
<Filters />
es un componente secundario de<List />
- Ambos
<Filters />
y<List />
son hijos de un componente principal. <Filters />
y<List />
vivir en componentes raíz completamente separados.
Puede haber otros escenarios en los que no estoy pensando. Si el tuyo no encaja entre estos, házmelo saber. Aquí hay algunos ejemplos muy aproximados de cómo he estado manejando los dos primeros escenarios:
Escenario 1
Podría pasar un controlador de <List />
a <Filters />
, que luego podría invocarse en el onChange
evento para filtrar la lista con el valor actual.
JSFiddle para #1 →
/** @jsx React.DOM */
var Filters = React.createClass({
handleFilterChange: function() {
var value = this.refs.filterInput.getDOMNode().value;
this.props.updateFilter(value);
},
render: function() {
return <input type="text" ref="filterInput" onChange={this.handleFilterChange} placeholder="Filter" />;
}
});
var List = React.createClass({
getInitialState: function() {
return {
listItems: ['Chicago', 'New York', 'Tokyo', 'London', 'San Francisco', 'Amsterdam', 'Hong Kong'],
nameFilter: ''
};
},
handleFilterUpdate: function(filterValue) {
this.setState({
nameFilter: filterValue
});
},
render: function() {
var displayedItems = this.state.listItems.filter(function(item) {
var match = item.toLowerCase().indexOf(this.state.nameFilter.toLowerCase());
return (match !== -1);
}.bind(this));
var content;
if (displayedItems.length > 0) {
var items = displayedItems.map(function(item) {
return <li>{item}</li>;
});
content = <ul>{items}</ul>
} else {
content = <p>No items matching this filter</p>;
}
return (
<div>
<Filters updateFilter={this.handleFilterUpdate} />
<h4>Results</h4>
{content}
</div>
);
}
});
React.renderComponent(<List />, document.body);
Escenario #2
Similar al escenario n.° 1, pero el componente principal será el que pasará la función de controlador a <Filters />
y pasará la lista filtrada a <List />
. Me gusta más este método ya que desacopla el <List />
archivo <Filters />
.
JSFiddle para #2 →
/** @jsx React.DOM */
var Filters = React.createClass({
handleFilterChange: function() {
var value = this.refs.filterInput.getDOMNode().value;
this.props.updateFilter(value);
},
render: function() {
return <input type="text" ref="filterInput" onChange={this.handleFilterChange} placeholder="Filter" />;
}
});
var List = React.createClass({
render: function() {
var content;
if (this.props.items.length > 0) {
var items = this.props.items.map(function(item) {
return <li>{item}</li>;
});
content = <ul>{items}</ul>
} else {
content = <p>No items matching this filter</p>;
}
return (
<div className="results">
<h4>Results</h4>
{content}
</div>
);
}
});
var ListContainer = React.createClass({
getInitialState: function() {
return {
listItems: ['Chicago', 'New York', 'Tokyo', 'London', 'San Francisco', 'Amsterdam', 'Hong Kong'],
nameFilter: ''
};
},
handleFilterUpdate: function(filterValue) {
this.setState({
nameFilter: filterValue
});
},
render: function() {
var displayedItems = this.state.listItems.filter(function(item) {
var match = item.toLowerCase().indexOf(this.state.nameFilter.toLowerCase());
return (match !== -1);
}.bind(this));
return (
<div>
<Filters updateFilter={this.handleFilterUpdate} />
<List items={displayedItems} />
</div>
);
}
});
React.renderComponent(<ListContainer />, document.body);
Escenario #3
Cuando los componentes no pueden comunicarse entre ningún tipo de relación padre-hijo, la documentación recomienda configurar un sistema de eventos global .
Hay varias formas de hacer que los componentes se comuniquen. Algunos pueden adaptarse a su caso de uso. Aquí hay una lista de algunos que me ha resultado útil saber.
Reaccionar
Comunicación directa entre padres e hijos.
const Child = ({fromChildToParentCallback}) => (
<div onClick={() => fromChildToParentCallback(42)}>
Click me
</div>
);
class Parent extends React.Component {
receiveChildValue = (value) => {
console.log("Parent received value from child: " + value); // value is 42
};
render() {
return (
<Child fromChildToParentCallback={this.receiveChildValue}/>
)
}
}
Aquí el componente hijo llamará a una devolución de llamada proporcionada por el padre con un valor, y el padre podrá obtener el valor proporcionado por los hijos en el padre.
Si crea una función/página de su aplicación, es mejor tener un solo padre que administre las devoluciones de llamada/estado (también llamado container
o smart component
), y que todos los niños sean apátridas y solo informen cosas al padre. De esta manera, puede "compartir" fácilmente el estado del padre con cualquier niño que lo necesite.
Contexto
React Context permite mantener el estado en la raíz de la jerarquía de componentes y poder inyectar este estado fácilmente en componentes muy anidados, sin la molestia de tener que pasar accesorios a cada componente intermedio.
Hasta ahora, el contexto era una característica experimental, pero hay una nueva API disponible en React 16.3.
const AppContext = React.createContext(null)
class App extends React.Component {
render() {
return (
<AppContext.Provider value={{language: "en",userId: 42}}>
<div>
...
<SomeDeeplyNestedComponent/>
...
</div>
</AppContext.Provider>
)
}
};
const SomeDeeplyNestedComponent = () => (
<AppContext.Consumer>
{({language}) => <div>App language is currently {language}</div>}
</AppContext.Consumer>
);
El consumidor está utilizando el patrón de función render prop/children
Consulte esta publicación de blog para obtener más detalles.
Antes de React 16.3, recomendaría usar react-broadcast que ofrece una API bastante similar y usa la API de contexto anterior.
Portales
Utilice un portal cuando desee mantener 2 componentes juntos para que se comuniquen con funciones simples, como en padre/hijo normal, pero no quiera que estos 2 componentes tengan una relación padre/hijo en el DOM, porque de restricciones visuales/CSS que implica (como índice z, opacidad...).
En este caso puedes utilizar un "portal". Existen diferentes bibliotecas de reacción que utilizan portales , generalmente utilizadas para modales , ventanas emergentes, información sobre herramientas...
Considera lo siguiente:
<div className="a">
a content
<Portal target="body">
<div className="b">
b content
</div>
</Portal>
</div>
Podría producir el siguiente DOM cuando se renderice en el interior reactAppContainer
:
<body>
<div id="reactAppContainer">
<div className="a">
a content
</div>
</div>
<div className="b">
b content
</div>
</body>
Más detalles aquí
Tragamonedas
Defines un espacio en algún lugar y luego llenas el espacio desde otro lugar de tu árbol de renderizado.
import { Slot, Fill } from 'react-slot-fill';
const Toolbar = (props) =>
<div>
<Slot name="ToolbarContent" />
</div>
export default Toolbar;
export const FillToolbar = ({children}) =>
<Fill name="ToolbarContent">
{children}
</Fill>
Esto es un poco similar a los portales, excepto que el contenido completo se representará en un espacio que usted defina, mientras que los portales generalmente representan un nuevo nodo dom (a menudo un hijo de document.body).
Verifique la biblioteca de reacción-slot-fill
Autobús de eventos
Como se indica en la documentación de React :
Para la comunicación entre dos componentes que no tienen una relación padre-hijo, puede configurar su propio sistema de eventos global. Suscríbase a eventos en componenteDidMount(), cancele la suscripción en componenteWillUnmount() y llame a setState() cuando reciba un evento.
Hay muchas cosas que puedes usar para configurar un bus de eventos. Puede simplemente crear una serie de oyentes y, al publicar el evento, todos los oyentes recibirán el evento. O puedes usar algo como EventEmitter o PostalJs
Flujo
Flux is basically an event bus, except the event receivers are stores. This is similar to the basic event bus system except the state is managed outside of React
Original Flux implementation looks like an attempt to do Event-sourcing in a hacky way.
Redux is for me the Flux implementation that is the closest from event-sourcing, an benefits many of event-sourcing advantages like the ability to time-travel. It is not strictly linked to React and can also be used with other functional view libraries.
Egghead's Redux video tutorial is really nice and explains how it works internally (it really is simple).
Cursors
Cursors are coming from ClojureScript/Om and widely used in React projects. They permit to manage the state outside of React, and let multiple components have read/write access to the same part of the state, without needing to know anything about the component tree.
Many implementations exists, including ImmutableJS, React-cursors and Omniscient
Edit 2016: it seems that people agree cursors work fine for smaller apps but it does not scale well on complex apps. Om Next does not have cursors anymore (while it's Om that introduced the concept initially)
Elm architecture
The Elm architecture is an architecture proposed to be used by the Elm language. Even if Elm is not ReactJS, the Elm architecture can be done in React as well.
Dan Abramov, the author of Redux, did an implementation of the Elm architecture using React.
Both Redux and Elm are really great and tend to empower event-sourcing concepts on the frontend, both allowing time-travel debugging, undo/redo, replay...
The main difference between Redux and Elm is that Elm tend to be a lot more strict about state management. In Elm you can't have local component state or mount/unmount hooks and all DOM changes must be triggered by global state changes. Elm architecture propose a scalable approach that permits to handle ALL the state inside a single immutable object, while Redux propose an approach that invites you to handle MOST of the state in a single immutable object.
While the conceptual model of Elm is very elegant and the architecture permits to scale well on large apps, it can in practice be difficult or involve more boilerplate to achieve simple tasks like giving focus to an input after mounting it, or integrating with an existing library with an imperative interface (ie JQuery plugin). Related issue.
Also, Elm architecture involves more code boilerplate. It's not that verbose or complicated to write but I think the Elm architecture is more suited to statically typed languages.
FRP
Libraries like RxJS, BaconJS or Kefir can be used to produce FRP streams to handle communication between components.
You can try for example Rx-React
I think using these libs is quite similar to using what the ELM language offers with signals.
CycleJS framework does not use ReactJS but uses vdom. It share a lot of similarities with the Elm architecture (but is more easy to use in real life because it allows vdom hooks) and it uses RxJs extensively instead of functions, and can be a good source of inspiration if you want to use FRP with React. CycleJs Egghead videos are nice to understand how it works.
CSP
CSP (Communicating Sequential Processes) are currently popular (mostly because of Go/goroutines and core.async/ClojureScript) but you can use them also in javascript with JS-CSP.
James Long has done a video explaining how it can be used with React.
Sagas
A saga is a backend concept that comes from the DDD / EventSourcing / CQRS world, also called "process manager". It is being popularized by the redux-saga project, mostly as a replacement to redux-thunk for handling side-effects (ie API calls etc). Most people currently think it only services for side-effects but it is actually more about decoupling components.
It is more of a compliment to a Flux architecture (or Redux) than a totally new communication system, because the saga emit Flux actions at the end. The idea is that if you have widget1 and widget2, and you want them to be decoupled, you can't fire action targeting widget2 from widget1. So you make widget1 only fire actions that target itself, and the saga is a "background process" that listens for widget1 actions, and may dispatch actions that target widget2. The saga is the coupling point between the 2 widgets but the widgets remain decoupled.
If you are interested take a look at my answer here
Conclusion
If you want to see an example of the same little app using these different styles, check the branches of this repository.
I don't know what is the best option in the long term but I really like how Flux looks like event-sourcing.
If you don't know event-sourcing concepts, take a look at this very pedagogic blog: Turning the database inside out with apache Samza, it is a must-read to understand why Flux is nice (but this could apply to FRP as well)
I think the community agrees that the most promising Flux implementation is Redux, which will progressively allow very productive developer experience thanks to hot reloading. Impressive livecoding ala Bret Victor's Inventing on Principle video is possible!