'children' as a function - 'render callbacks'


(Sunil Pai) #1

started writing this as a gist, but it makes more sense to discuss this here. open to thoughts / ideas / feedback!

edit - 15/6 - added more sightings, rrg example

react-render-callbacks

Information on the ‘children as a function’ / ‘render callback’ pattern for React components. Please contribute if you have thoughts / ideas.

intro

this.setState() is nice, but we abuse it to hold state that’s only local to some elements in .render(). It’s also used by mixins to give ‘functionality’ to a component, but is not very composable, leads to keyname clashes, etc. What we’d really like is to be able to use a React component to express a value that changes over time, without changing the pure nature of .render() / mashing on this.state.

The pattern here is to to pass a function/callback as children when rendering a React component. This callback will receive the ‘value’ at render time (and perhaps more, see examples below).

example 1 - simple state / ‘cursors’ via react-state

class Counter{
  render(){
    return <State initial={0}>
      {(val, set) =>        // that's right, we pass a function here. elegant!
        <div onClick={() => set(val + 1)}>  
          clicked {val} times
        </div>}
    </State>;
  }
}

React.render(<Counter/>, el);

// implementation
const State = React.createClass({
  getDefaultProps(){
    return {
      onChange: () => {}
    }
  },
  propTypes:{
    children: React.PropTypes.func
  },
  getInitialState(){
    return {
      value: this.props.initial
    }
  },
  set(value){
    this.setState({value});
    this.props.onChange(value); // in case you want to pass data 'up'
  },
  shouldComponentUpdate(){
    return true;
    // components of this type must always render 'through'
    // this is alright, since the 'children' should be considered dependent on
    // the parent's .state/.props
  },
  render() {
    return this.props.children(this.state.value, this.set); // 'call' children
  }
});

The nice thing here is that we can ‘do’ stuff to the value(s) before passing it on to the render callback.

example 2 - state + spring animations (via react-springs)

class Box {
  render(){
    return <State initial={{left: 0, top: 0}}>{ position, setPos =>
        <div onMouseMove={e => setPos({left: e.pageX, top: e.pageY})}>
          move your mouse around here,
          and the below div will track you like a spring
          <Springs to={position}>{ $pos =>   
              <div style={{...$pos, width: 100, height: 100}}>
                floaty ghost wooooo
              </div>}
          </Spring>
        </div>}
    </State>
  }
}

It’s also a nice way to ‘sideload’ data into a render tree.

example 3 - reading data from an js-csp channel

class App{
  render(){
    return <Channel src={this.props.src} transduce={ch => map(x => [x, x * 2])}>
      {[val, times2] => <div>{val} * 2 = {times2}</div>}
    </Channel>
  }
}
let ch = csp.chan(), i = 0;
setInterval(() => putAsync(ch, i++), 1000);

React.render(<App src={ch}/>, el)

As you can see, these ‘special’ components don’t really spit any dom themselves (though they could?), and just introduce a functional wrapper to augment the render tree.

One can also use this pattern to return custom element constructors
example 4 - context free forms via react-radio-group

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

spotted in the wild

caveats

  • testing these components is currently an issue, as shallow rendering ‘breaks’. However, I believe this could be fixed if shallowRenderer ‘recognized’ these types of components.

Stuggling with reusable components/scaling
(Sunil Pai) #2

filed https://github.com/facebook/react/issues/4127 wrt shallow rendering / testing.


(Alex Guerra) #3

Maybe nitpicking, but why overload children? According to the virtual DOM terminology page, children should be a ReactNodeList, and a function doesn’t fit that. It kind of makes sense, but it would be misleading in terms of ownership I think; if someone uses a string ref in the render function, it will be attached to the wrapping component, right? render makes more sense to me anyways.

And then why use custom function signatures? It’s basically just an element factory; pass in props as the first parameter and you can wrap components and pure factory functions all the same.

Definitely interesting though. It’s like component decorators except instead of just components it works with any factory function that accepts props and returns ReactElements. I’m pretty sure the decorated components in react-dnd are ultimately just being turned into / used as factories anyway, so this seems to be a much more flexible take on the same pattern.


(Sunil Pai) #4

so the docs don’t say anything about what it should be, and I’m kinda hoping this is useful enough to be considered a valid pattern :smile:

The choice of overloading ‘children’ is for both syntax and semantics, I guess? To show that the function is a representation of the UI at that point. One could totally rewrite to use another prop, but this seems so much cleaner (and if it’s ‘recognized’, could lead to further optimizations)

The point about refs is valid, but you could use the callback form via ref={el => this.special = el} to alleviate that.

I don’t understand the bit about custom function signatures? Do you mean custom classes? The benefit is that React won’t reinstantiate components on the tree (and all the other goodies). This is critical for stuff like animation (so that spring instances are reused, etc).

One nice thing about this pattern is you don’t have to use cloneElement to get a similar api for your own custom component.

Thanks for responding!


(Alex Guerra) #5

I was referencing this, though obviously it’s working? It was really just a semantic argument though. I wonder if it has any other implications?

By custom function signatures I mean things like (position, setPos) instead of just (props) in the render function. It seems to me the the latter is more in-line with how element factory functions usually work, and would allow you to pass component factories directly. But it’s kind of a dumb point because you can just do (a, b) => { <Whatever a={a} b={b}/> } anyway :smile:


(Sunil Pai) #6

Observation - let’s say one component’s .render tree is like
A > B > C > D > E

By js rules, this will be evaluated in the order
E - D - C - B - A

However, if we introduce a render callback in the middle, like
A > B > State () => C > D > E

then the evaluation order changes to
State - B - A - E - D - C

fascinating. no idea whether it’s important yet.


(Sunil Pai) #7

as mentioned by @HeyImAlex, this is the part in the docs https://facebook.github.io/react/docs/glossary.html that specifies that children has a type ReactNodeList (but yay, it’s not enforced). React team, would you please comment? /cc @sebmarkbage et al


(Sunil Pai) #8

filed https://github.com/facebook/react/issues/4136 wrt methods to deal with shouldComponentUpdate.


(Elie Rotenberg) #9

Following up on the Twitter thread

The ‘children as a ReactNodeList-returning function’ is no more and no less than a syntactic trick to inline higher-order components.
As a matter of fact, I tend to expose my higher order components in three forms:

  1. enhance(Component, ...args), which returns a React.Component class,
  2. decorate(...args), which is a React.Component decorator,
  3. <Injector>, which uses the children as a function pattern.

The three are semantically equivalent (apart from ownership), however they yield different intents.

  1. is a component transformer; you can use it on component classes you were given (eg. from another lib),
  2. is a component decorator; the inner component is purely private and the public component has the API contract of the outer component,
  3. is a component injector; the enhancement is the responsibility of the parent component.

Maybe this is overengineering, as I end up using 2) almost excusively in the real world.
What do you guys think?


(Phips Peter) #10

We have noticed some strangeness with the ordering when a child component would like to notify a parent component about a change but cannot do that because of the render cycle.


(Phips Peter) #11

I strongly believe that we should be passing Elements instead of callbacks for rendering. React’s cloneElement has a lot of nice properties. I also think that the transformation from function to component is not that difficult when you think of render being a pure function. I also feel like it is nice to have callbacks always be for mutators and elements be used for display.


(Sunil Pai) #12

The problem with cloneElement, is that it introduces coupling between this “higher order component” and it’s children. Either you need knowledge of the children structure, or the child needs to expect props for this wrapper. While that’s great for a bunch of use cases, it’s hard to do animations, generic store wrappers etc.

(Also, this isn’t a callback in the traditional sense, no? It doesnt get called for all state changes, just on .render.)


(Sunil Pai) #13

I’m curious about this, could you give an example? Thanks!


(Sunil Pai) #14

An alternative to the render callback pattern, but still keeping some of the nicer properties -

class Box extends React.Component{
  render(){
    return <div onMouseMove={e => this.setState({left:e.pageX, top: e.pageY})}>
      move your mouse around here,
      and the below div will track you like a spring
    
      {/* the below spring doesn't render any dom */}  
      <Springs to={{left: this.state.left, top: this.state.top}} 
        onSpringUpdate={({left, top}) => this.setState({springPosition: {left, top}})} />      
      <div style={{...this.state.springPosition, width: 100, height: 100}}>
        floaty ghost wooooo
      </div>
    
    </div>;    
  }
}

This avoids having to use the render-callback pattern, but asks that you manage the state yourself. However, this is still better than initializing/destroying spring instances during your own component’s lifecycle methods, because the react render tree takes care of that for you.


(Sophie Alpert) #15

Sorry, very overdue comment, but: that children: ReactNodeList annotation applies specifically to ReactDOMElement – that is, in <div>{children}</div>, children needs to be a ReactNodeList. You can pass whatever you want (TProps in that example) to a composite component.