javascript reactjs ecmascript-6 functional-programming

Alternativa FP al polimorfismo en JavaScript/ReactJS



ecmascript-6 functional-programming (3)

Actualmente estoy trabajando en un proyecto de ReactJS donde necesito crear componentes "reutilizables" en los que algunos de los métodos deberían ser "reemplazados". En OOP usaría polimorfismo. He leído algo y parece que el consenso es usar HoC / composición, pero no puedo entender cómo lograrlo. Me imagino que si pudiera obtener una muestra de ES6 usando composición, tal vez sería más fácil adaptar la idea a ReactJS después.

Debajo hay un ejemplo de ESO OOP (ignore el manejo del evento solo para probarlo) de prácticamente todo lo que me gustaría lograr en ReactJS. ¿Alguien tiene alguna guía sobre cómo dividir un componente ReactJS en un HoC o incluso simplemente demostrar cómo usaría la composición en ES6 según el ejemplo?

class TransferComponent { constructor(){ let timeout = null; this.render(); this.events(); } events(){ let scope = this; document.getElementById(''button'').addEventListener(''click'', function(){ scope.validate.apply(scope); }); } validate(){ if(this.isValid()){ this.ajax(); } } isValid(){ if(document.getElementById(''username'').value !== ''''){ return true; } return false; } ajax(){ clearTimeout(this.timeout); document.getElementById(''message'').textContent = ''Loading...''; this.timeout = setTimeout(function(){ document.getElementById(''message'').textContent = ''Success''; }, 500); } render(){ document.getElementById(''content'').innerHTML = ''<input type="text" id="username" value="username"/>/n/ <button id="button" type="button">Validate</button>''; } } class OverrideTransferComponent extends TransferComponent{ isValid(){ if(document.getElementById(''username'').value !== '''' && document.getElementById(''password'').value !== ''''){ return true; } return false; } render(){ document.getElementById(''content'').innerHTML = ''<input type="text" id="username" value="username"/>/n/ <input type="text" id="password" value="password"/>/n/ <button id="button" type="button">Validate</button>''; } } const overrideTransferComponent = new OverrideTransferComponent();

<div id="content"></div> <div id="message"></div>

ACTUALIZACIÓN: Aunque mi pregunta original fue sobre FP, creo que render props es una muy buena solución para mi problema y evita los problemas de HoC.


La respuesta con respecto a su código de ejemplo se encuentra en la parte central / inferior de esta publicación.

Una buena forma de proceder es la composición Reaccionar, es decir, el patrón render-callback , también conocido como función-como-niño. La principal ventaja que tiene sobre HOC es que le permite componer componentes dinámicamente en tiempo de ejecución (por ejemplo, en render) en lugar de forma estática en el momento del autor.

Independientemente de si usa render-callback o HOC, el objetivo en la composición de componentes es delegar el comportamiento reutilizable a otros componentes , y luego pasar esos componentes como accesorios al componente que los necesita.

Ejemplo abstracto:

El siguiente componente Delegator usa el patrón de renderización-devolución de llamada para delegar la lógica de implementación a un ImplementationComponent que se transfiere como un apoyo:

const App = () => <Delegator ImplementationComponent={ImplementationB} />; class Delegator extends React.Component { render() { const { ImplementationComponent } = this.props; return ( <div> <ImplementationComponent> { ({ doLogic }) => { /* ... do/render things based on doLogic ... */ } } </ImplementationComponent> </div> ); } }

Los diversos componentes de implementación se verían así:

class ImplementationA extends React.Component { doSomeLogic() { /* ... variation A ... */ } render() { this.props.children({ doLogic: this.doSomeLogic }) } } class ImplementationB extends React.Component { doSomeLogic() { /* ... variation B ... */ } render() { this.props.children({ doLogic: this.doSomeLogic }) } }

Más adelante en la línea, puede anidar más componentes secundarios en el componente Delegator siguiendo el mismo patrón de composición:

class Delegator extends React.Component { render() { const { ImplementationComponent, AnotherImplementation, SomethingElse } = this.props; return ( <div> <ImplementationComponent> { ({ doLogic }) => { /* ... */} } </ImplementationComponent> <AnotherImplementation> { ({ doThings, moreThings }) => { /* ... */} } </AnotherImplementation> <SomethingElse> { ({ foo, bar }) => { /* ... */} } </SomethingElse> </div> ); } }

Ahora el componente hijo anidado permite múltiples implementaciones concretas:

const App = () => ( <div> <Delegator ImplementationComponent={ImplementationB} AnotherImplementation={AnotherImplementation1} SomethingElse={SomethingVariationY} /> <Delegator ImplementationComponent={ImplementationC} AnotherImplementation={AnotherImplementation2} SomethingElse={SomethingVariationZ} /> </div> );

Responda (su ejemplo):

Aplicando el patrón de composición anterior a su ejemplo, la solución reestructura su código, pero asume que debe hacer lo siguiente:

  • permitir variaciones de entradas y su lógica de validación
  • cuando el usuario envía una entrada válida, entonces haz algo de ajax

En primer lugar, para facilitar las cosas, cambié el DOM a:

<div id="content-inputs"></div> <div id="content-button"></div>

Ahora, TransferComponent solo sabe cómo mostrar un botón y hacer algo cuando se presiona el botón y los datos son válidos. No sabe qué entradas mostrar o cómo validar los datos. VaryingComponent esa lógica al VaryingComponent anidado.

export default class TransferComponent extends React.Component { constructor() { super(); this.displayDOMButton = this.displayDOMButton.bind(this); this.onButtonPress = this.onButtonPress.bind(this); } ajax(){ console.log(''doing some ajax'') } onButtonPress({ isValid }) { if (isValid()) { this.ajax(); } } displayDOMButton({ isValid }) { document.getElementById(''content-button'').innerHTML = ( ''<button id="button" type="button">Validate</button>'' ); document.getElementById(''button'') .addEventListener(''click'', () => this.onButtonPress({ isValid })); } render() { const { VaryingComponent } = this.props; const { displayDOMButton } = this; return ( <div> <VaryingComponent> {({ isValid, displayDOMInputs }) => { displayDOMInputs(); displayDOMButton({ isValid }); return null; }} </VaryingComponent> </div> ) } };

Ahora creamos implementaciones concretas de VaryingComponent para desarrollar diversas lógicas de visualización y validación de entrada.

La implementación solo para nombre de usuario:

export default class UsernameComponent extends React.Component { isValid(){ return document.getElementById(''username'').value !== ''''; } displayDOMInputs() { document.getElementById(''content-inputs'').innerHTML = ( ''<input type="text" id="username" value="username"/>'' ); } render() { const { isValid, displayDOMInputs } = this; return this.props.children({ isValid, displayDOMInputs }); } }

La implementación de nombre de usuario y contraseña:

export default class UsernamePasswordComponent extends React.Component { isValid(){ return ( document.getElementById(''username'').value !== '''' && document.getElementById(''password'').value !== '''' ); } displayDOMInputs() { document.getElementById(''content-inputs'').innerHTML = ( ''<input type="text" id="username" value="username"/>/n/ <input type="text" id="password" value="password"/>/n'' ); } render() { const { isValid, displayDOMInputs } = this; return this.props.children({ isValid, displayDOMInputs }); } }

Finalmente, las instancias de composición de TansferComponent verían así:

<TransferComponent VaryingComponent={UsernameComponent} /> <TransferComponent VaryingComponent={UsernamePasswordComponent} />


Al leer su pregunta, no está claro si se está refiriendo a la composición o la herencia, pero son conceptos de OOP diferentes. Te recomiendo que eches un vistazo a este artículo si no sabes la diferencia entre ellos.

Acerca de su problema específico con React. Te recomiendo que pruebes la composición del usuario, ya que te da mucha flexibilidad para construir tu UI y pasar accesorios.

Si está trabajando con React, probablemente ya esté usando composición cuando complete Diálogos dinámicamente, por ejemplo. Como los documentos React muestran:

function FancyBorder(props) { return ( <div className={''FancyBorder FancyBorder-'' + props.color}> {props.children} </div> ); } function WelcomeDialog() { return ( <FancyBorder color="blue"> <h1 className="Dialog-title"> Welcome </h1> <p className="Dialog-message"> Thank you for visiting our spacecraft! </p> </FancyBorder> ); }

La gente de Facebook ha estado desarrollando UI muy desafiantes y creando miles de componentes con React y no encontró un buen caso de uso para la herencia sobre la composición. Como dice el documento:

React tiene un poderoso modelo de composición, y recomendamos usar composición en lugar de herencia para volver a usar el código entre los componentes.

Si realmente desea utilizar la herencia, su recomendación es que extraiga la funcionalidad que desea reutilizar sobre los componentes en un módulo de JavaScript separado. Los componentes pueden importarlo y usar esa función, objeto o clase, sin extenderlo.

En el ejemplo que proporcionó, dos funciones en un utils.js funcionarían perfectamente. Ver:

isUsernameValid = (username) => username !== ''''; isPasswordValid = (password) => password !== '''';

Puede importarlos y usarlos en sus componentes sin problemas.


El ejemplo de FP no reactiva

Para empezar, en la Programación Funcional, la función es un ciudadano de primera clase. Esto significa que puede tratar las funciones como lo haría con los datos en OOP (es decir, pasar como parámetros, asignar variables, etc.).

Su ejemplo reúne datos con comportamiento en los objetos. Para escribir una solución puramente funcional, querremos separar estos .

La Programación funcional se basa fundamentalmente en separar los datos del comportamiento.

Entonces, comencemos con isValid .

Funcional es valido

Hay algunas maneras de ordenar la lógica aquí, pero vamos a seguir con esto:

  1. Dada una lista de identificadores
  2. Todos los ID son válidos si no existe un ID no válido

Lo cual, en JS, se traduce a:

const areAllElementsValid = (...ids) => !ids.some(isElementInvalid)

Necesitamos un par de funciones auxiliares para que esto funcione:

const isElementInvalid = (id) => getValueByElementId(id) === '''' const getValueByElementId = (id) => document.getElementById(id).value

Podríamos escribir todo eso en una línea, pero romperlo lo hace un poco más legible. ¡Con eso, ahora tenemos una función genérica que podemos usar para determinar isValid para nuestros componentes!

areAllElementsValid(''username'') // TransferComponent.isValid areAllElementsValid(''username'', ''password'') // TransferOverrideComponent.isValid

Rendimiento funcional

isValid un poco en isValid usando el document . En la verdadera programación funcional, las funciones deben ser puras . O, en otras palabras, el resultado de una llamada a función solo debe determinarse a partir de sus entradas (también conocido como idempotente) y no puede tener efectos secundarios .

Entonces, ¿cómo renderizamos al DOM sin efectos secundarios? Bueno, React utiliza un DOM virtual (una estructura de datos sofisticada que vive en la memoria y se transfiere y devuelve de las funciones para mantener la pureza funcional) para la biblioteca central. Los efectos secundarios de React viven en la biblioteca de react-dom .

Para nuestro caso, usaremos un DOM virtual súper simple (de tipo string ).

const USERNAME_INPUT = ''<input type="text" id="username" value="username"/>'' const PASSWORD_INPUT = ''<input type="text" id="password" value="password"/>'' const VALIDATE_BUTTON = ''<button id="button" type="button">Validate</button>''

Estos son nuestros componentes, para usar la terminología de React, que podemos componer en UI:

USERNAME_INPUT + VALIDATE_BUTTON // TransferComponent.render USERNAME_INPUT + PASSWORD_INPUT + VALIDATE_BUTTON // TransferOverrideComponent.render

Esto probablemente parezca una simplificación excesiva y no funcional en absoluto. ¡Pero el operador + es de hecho funcional! Piénsalo:

  • toma dos entradas (el operando izquierdo y el operando derecho)
  • devuelve un resultado (para cadenas, la concatenación de los operandos)
  • No tiene efectos secundarios
  • no cambia sus entradas (el resultado es una nueva cadena - los operandos no se modifican)

Entonces, con eso, ¡ render ahora es funcional!

¿Qué hay de ajax?

Desafortunadamente, no podemos realizar una llamada ajax, mutar el DOM, configurar oyentes de eventos o establecer tiempos de espera sin efectos secundarios . Podríamos seguir la compleja ruta de crear mónadas para estas acciones, pero para nuestros propósitos, basta decir que seguiremos usando los métodos no funcionales.

Aplicarlo en React

Aquí hay una reescritura de tu ejemplo usando patrones comunes de React. Estoy usando componentes controlados para las entradas de formulario. La mayoría de los conceptos funcionales de los que hemos hablado realmente viven bajo el capó en React, por lo que esta es una implementación bastante simple que no utiliza nada sofisticado.

class Form extends React.Component { constructor(props) { super(props); this.state = { loading: false, success: false }; } handleSubmit() { if (this.props.isValid()) { this.setState({ loading: true }); setTimeout( () => this.setState({ loading: false, success: true }), 500 ); } } render() { return ( <div> <form onSubmit={this.handleSubmit}> {this.props.children} <input type="submit" value="Submit" /> </form> { this.state.loading && ''Loading...'' } { this.state.success && ''Success'' } </div> ); } }

El uso del state probablemente parezca un efecto secundario, ¿no es así? De alguna manera lo es, pero profundizar en las partes internas de React puede revelar una implementación más funcional de la que se puede ver en nuestro componente individual.

Aquí está el Form para su ejemplo. Tenga en cuenta que podemos manejar el envío de varias maneras aquí. Una forma es pasar el username y la password como accesorios en el Form (probablemente como un soporte genérico de data ). Otra opción es pasar una handleSubmit llamada handleSubmit específica para ese formulario (al igual que lo hacemos para validate ).

class LoginForm extends React.Component { constructor(props) { super(props); this.state = { username: '''', password: '''' }; } isValid() { return this.state.username !== '''' && this.state.password !== ''''; } handleUsernameChange(event) { this.setState({ username: event.target.value }); } handlePasswordChange(event) { this.setState({ password: event.target.value }); } render() { return ( <Form validate={this.isValid} > <input value={this.state.username} onChange={this.handleUsernameChange} /> <input value={this.state.password} onChange={this.handlePasswordChange} /> </Form> ); } }

También puedes escribir otro Form pero con diferentes entradas

class CommentForm extends React.Component { constructor(props) { super(props); this.state = { comment: '''' }; } isValid() { return this.state.comment !== ''''; } handleCommentChange(event) { this.setState({ comment: event.target.value }); } render() { return ( <Form validate={this.isValid} > <input value={this.state.comment} onChange={this.handleCommentChange} /> </Form> ); } }

Por ejemplo, tu aplicación puede representar ambas implementaciones de Form :

class App extends React.Component { render() { return ( <div> <LoginForm /> <CommentForm /> </div> ); } }

Finalmente, usamos ReactDOM lugar de innerHTML

ReactDOM.render( <App />, document.getElementById(''content'') );

La naturaleza funcional de React a menudo se oculta al usar JSX. Los animo a que lean sobre cómo lo que estamos haciendo realmente es solo un conjunto de funciones compuestas juntas. Los documentos oficiales cubren esto bastante bien.

Para más información, James K. Nelson ha reunido algunos recursos estelares en React que deberían ayudar con su comprensión funcional: https://reactarmory.com/guides/learn-react-by-itself/react-basics