Jesús García

Patrones Avanzados

React avanzado

  1. HOCs

  2. Render Props

  3. Context y Provider Pattern

  4. Compound Components

  5. Hooks

Higher Order Components

Pero primero...

Mixins

  • API deprecada usada para compartir código
  • Se identificarion varios problemas:
    • Coupling implícito entre componentes que comparten un mixin
    • Name clashes

Container Components

class CommentList extends React.Component {
  constructor(props) {
    super(props);
    this.handleChange = this.handleChange.bind(this);
    this.state = {
      comments: []
    };
  }

  componentDidMount() {
    DataSource.getComments().then(comments => {
        this.setState({comments})
    })
  }



  render() {
    return (
      <div>
        {this.state.comments.map((comment) => (
          <Comment comment={comment} key={comment.id} />
        ))}
      </div>
    );
  }
}

Separar componentes 'tontos', puramente visuales, de componentes 'contenedores' con lógica, llamadas a APIs, etc

Higher Order Components

const withComments = WrappedComponent => {
     class WithComments extends React.Component {
      constructor(props) {
        super(props);
        this.state = {
          comments: []
        };
      }
    
      componentDidMount() {
        DataSource.getComments().then(comments => {
            this.setState({comments})
        })
      }
    
    
      render() {
        return (
         <WrappedComponent comments={this.state.comments} {...this.props}/>
        );
      }
    } 
}

Crear componentes contenedores parametrizados

Higher Order Components

export const withCondition = trueOrFalse => Comp =>
    class WithCondition extends Component {
        render = () => trueOrFalse && <Comp {...this.props} />;
    };


const ifDev = withCondition(__DEV__)

const OnlyShownIfDev = ifDev(MyComponent)

Crear factorías de contenedores

const withCondition = trueOrFalse => Comp =>
    class WithCondition extends Component {
        render = () => trueOrFalse && <Comp {...this.props} />;
    };

const withCounter = initialCount => WrappedComponent => {
  class CounterHoc extends React.Component {
    state = {
      count: initialCount || 0,
    }

    increment = () => {
      this.setState(prevState => {
        return {count: prevState.count + 1}
      })
    }

    decrement = () => {
      this.setState(prevState => {
        return {count: prevState.count - 1}
      })
    }

    render() {
      return (
        <WrappedComponent
          count={this.state.count}
          increment={this.increment}
          decrement={this.decrement}
          {...this.props}
        />
      )
    }
  }
  return CounterHoc
}

const compose = (f, g) => x => f(g(x))

const enhance = compose(withCondition(__DEV__), withCounter(3))

const MyEnhancedComponent = enhance(MyComponent)

Componer HOCs

function withSomething(WrappedComponent) {
  class WithSomething extends React.Component {/* ... */}
  WithSomething.displayName = `WithSomething(${getDisplayName(WrappedComponent)})`;
  return WithSomething;
}

function getDisplayName(WrappedComponent) {
  return WrappedComponent.displayName || WrappedComponent.name || 'Component';
}

Recuperar displayName

Higher Order Components

  • No modifican el componente que envuelven
  • No usan herencia - son funciones puras
  • Suele emplearse currying para poder componer varios HOCs
  • La composición es "estática" - no puede hacerse en el render de un componente. Por ello, suelen usarse para envolver componentes al iniciarse la aplicación que van a usarse siempre con esa envoltura.

Problemas con Higher Order Components

  • Hay que pasar las props
  • Son estáticos - no se pueden usar en render por performance y pérdida del estado al re-renderizar
  • Se pierde displayName
  • Se pierden los métodos estáticos - hay que copiarlos o usar hoist-non-react-statics
  • Se pierden las refs - hay que usar forwardRef
  • Puede haber name clashes!

Render Props

(function as children)

Render Props

  • Se delega el rendering al usuario
  • Nos permite componener 'dinámicamente' - en el render
  • Soluciona problemas de los HOCs
    • Las props son explícitas - no hay colisiones de nombres
    • No hay problemas derivados de envolver un componente en otro (perder displayName, tener que hacer forwardRef, etc)

Render Props

class Counter extends React.Component {
  state = {count: 0}
  increment = () => {
    this.setState(prevState => ({
      count: prevState.count + 1
    }))
  }
  decrement = () => {
    this.setState(prevState => ({
      count: prevState.count - 1
    }))
  }
  render(){
    /* Llamamos a children como una función. 
       El JSX que devuelva será lo que se pinte.
    */
    return children({count: this.state.count, increment: this.increment, 
    decrement: this.decrement})
  }
}

// Mismo ejemplo con métodos incluidos en el state para evitar re-renders innecesarios
class Counter extends React.Component {
  increment = () => {
    this.setState(prevState => ({
      count: prevState.count + 1
    }))
  }
  decrement = () => {
    this.setState(prevState => ({
      count: prevState.count - 1
    }))
  }
  state = {count: 0, increment: this.increment, decrement: this.decrement}
  render(){
    return children(this.state)
  }
}
const App = () => {
  return (
    <Counter>
      {({count, increment, decrement}) => (
        <>
          <div>{count}</div>
          <button onClick={increment}>+</button>
          <button onClick={decrement}>-</button>
        </>
      )}
    </Counter>
  )
}

La función es invisible a React: no es un componente, React ve el JSX que devuelve

const App = () => {
  return (
    <Counter>
      {props => (
        <MyCounterComponent {...props} />
      )}
    </Counter>
  )
}

Es sencillo usar un componente dentro de un componente con render props: podemos envolverlo en una función

const withCounter = WrappedComponent =>
  class WithCounter extends React.Component {
    render() {
      return (
        <Counter>
          {counterProps => (
            <WrappedComponent {...counterProps} {...this.props} />
          )}
        </Counter>
      )
    }
  }

Implementar un HOC a partir de un componente con render props es sencillo. Muchas librerías proveen ambos

Context

Compartir datos a todos los niveles del árbol de componentes

const MyContext = React.createContext(defaultValue);

const App = () => (
  <MyContext.Provider value={overRidesDefaultValue}>
    <My>
      <Nested>
        <ComponentTree>
          <MyContext.Consumer>
            /* render props! */
            {
              value => <div>{value}</div>
            }
          <MyContext.Consumer>
        </ComponentTree>
      </Nested>
    </My>
  <MyContext.Provider>
)

p.ej. tema visual, si el usuario está logado...

3 maneras de acceder al contexto

const MyContext = React.createContext(defaultValue);

const App = () => (
  <MyContext.Provider value={overRidesDefaultValue}>
      <MyContext.Consumer>
        {
          value => <div>{value}</div>
        }
      <MyContext.Consumer>
  <MyContext.Provider>
)

Consumer con

render props

const MyContext = React.createContext(defaultValue);

class MyClass extends React.Component {
  componentDidMount() {
    let value = this.context;
  }
  componentDidUpdate() {
    let value = this.context;
  }
  componentWillUnmount() {
    let value = this.context;
  }
  render() {
    let value = this.context;
    /* render something based on the value of MyContext */
  }
}
MyClass.contextType = MyContext;

y hooks ;)

Class con

contextType

Provider Pattern

const MyProviderContext = React.createContext()
class MyProvider extends React.Component {
  /* Podemos exponer el consumidor como una propiedad estática de la clase */
  static Consumer = MyProviderContext.Consumer
  /* El estado y métodos que queramos pasar por context.
     Metiéndolo todo en state nos evitamos re-renders innecesarios
     de los consumers.
  */
  state = {}
  render() {
    /* Podemos adaptar para render props también - o usar el paquete de npm render-props */
    const {children} = this.props
    const ui = typeof children === 'function' ? children(this.state) : children
    return (
      <MyProviderContext.Provider value={this.state}>
        {ui}
      </MyProviderContext.Provider>
    )
  }
}

export default MyProvider

Compound Components

Componente padre que comparte estado con sus hijos de forma invisible al usuario, p.ej:

  • <select> y <option>
  • radio buttons
  • wizard para hacer varios pasos
  • tabs, desplegables, ...

 

Compound Components

Se desacopla el rendering de la lógica interna del componente, ofreciendo una API sencilla y flexible al usuario

const App = () => (
    <Counter>
      <Counter.Increment/>
      <Counter.View render={count => <div>{count}</div>}>
      <Counter.Decrement/>
    <Counter/>
)
class App extends React.Component {
  state = {selectedValue: 'Apple'}
  handleChange(selectedValue) {
    this.setState({selectedValue})
  }

  render() {
    return (
      <RadioGroup
        name="fruit"
        selectedValue={this.state.selectedValue}
        onChange={this.handleChange}
      >
        <RadioButton value="apple">Apple</RadioButton>
        <RadioButton value="orange">Orange</RadioButton>
        <RadioButton value="watermelon">Watermelon</RadioButton>
      </RadioGroup>
    )
  }
}

Compound Components

Dos formas de implementarlos:

React.cloneElement

Context

function Increment({onClick}) {
      return <button {...{onClick}}>+</button>
    }
    function Decrement({onClick}) {
      return <button {...{onClick}}>-</button>
    }
    function View({count, render}) {
      return render(count)
    }
    
    class Counter extends React.Component {
      state = {
        count: 0,
      }
      static Increment = Increment
      static Decrement = Decrement
      static View = View
      increment = () => {
        this.setState(state => ({
          count: state.count + 1,
        }))
      }
      decrement = () => {
        this.setState(state => ({
          count: state.count - 1,
        }))
      }
    
      render() {
        return React.Children.map(this.props.children, child => {
          if (child.type.name === 'Increment') {
            return React.cloneElement(child, {
              onClick: this.increment,
            })
          } else if (child.type.name === 'Decrement') {
            return React.cloneElement(child, {
              onClick: this.decrement,
            })
          } else {
            // View
            return React.cloneElement(child, {
              count: this.state.count,
            })
          }
        })
      }
    }
    
    function App() {
      return (
        <Counter>
          <Counter.Increment />
          <Counter.View render={count => <div>{count}</div>} />
          <Counter.Decrement />
        </Counter>
      )
    }
const Context = React.createContext()

function Increment() {
  return (
    <Context.Consumer>
      {({increment}) => <button onClick={increment}>+</button>}
    </Context.Consumer>
  )
}
function Decrement() {
  return (
    <Context.Consumer>
      {({decrement}) => <button onClick={decrement}>-</button>}
    </Context.Consumer>
  )
}
function View({children}) {
  return <Context.Consumer>{({count}) => children(count)}</Context.Consumer>
}

export class Counter extends React.Component {
  static Increment = Increment
  static Decrement = Decrement
  static View = View

  increment = () => {
    this.setState(state => ({
      count: state.count + 1,
    }))
  }
  decrement = () => {
    this.setState(state => ({
      count: state.count - 1,
    }))
  }
  state = {
    count: 0,
    decrement: this.decrement,
    increment: this.increment,
  }

  render() {
    return (
      <Context.Provider value={this.state}>
        {this.props.children}
      </Context.Provider>
    )
  }
}

No muy usada ya, más sencillo con la nueva API de Context

Hooks

Hooks

  • Nueva API para dotar a los componentes función de las mismas capacidades que las clases
  • Simplifican la gestión del estado, de side-effects, de acceso a context, ...
  • Permiten abstraer y compartir lógica con estado
function App() {
  const [count, setCount] = useState(0)
  const increment = () => {
    setCount(prevCount => prevCount + 1)
  }
  const decrement = () => {
    setCount(prevCount => prevCount - 1)
  }
  return (
    <>
      <button onClick={increment}>+</button>
      <div>{count}</div>
      <button onClick={decrement}>-</button>
    </>
  )
}

Hooks

Equivalencias con métodos de clases

Constructor, setState

useState, useReducer

componentDidMount, componentDidUpdate, componentWillUnmount

useEffect, useLayoutEffect

Reglas de hooks

  • Solo se pueden llamar desde componentes de React, o desde otros hooks 
  • Solo se pueden llamar en el "primer nivel" de la función - no dentro de loops o condicionales

Existe un plugin de eslint para estas reglas

Formación React Kairós - Patrones Avanzados

By Jesús García Martínez

Formación React Kairós - Patrones Avanzados

  • 145