Jesús García
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
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
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
(function as children)
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
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...
const MyContext = React.createContext(defaultValue);
const App = () => (
<MyContext.Provider value={overRidesDefaultValue}>
<MyContext.Consumer>
{
value => <div>{value}</div>
}
<MyContext.Consumer>
<MyContext.Provider>
)
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 ;)
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
Componente padre que comparte estado con sus hijos de forma invisible al usuario, p.ej:
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>
)
}
}
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
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>
</>
)
}
Constructor, setState
useState, useReducer
componentDidMount, componentDidUpdate, componentWillUnmount
useEffect, useLayoutEffect
Existe un plugin de eslint para estas reglas