React:如何将信息发送到父组件?

时间:2015-09-16 07:32:56

标签: coffeescript functional-programming reactjs

我创建了一个非常简单的导航视图控制器,模仿iOS UINavigationController的概念。它基本上只维护路线的历史,并动画推送到另一条路线或流行音乐。

(我希望你不介意coffeescript;)

createView = (spec) -> React.createFactory(React.createClass(spec))
{div, span, input, img, button} = React.DOM
cond = (condition, result, otherwise) -> if condition then result?() else otherwise?()
Transition = React.createFactory(React.addons.CSSTransitionGroup)

NavVC = createView
  displayName: 'NavVC'
  mixins: [React.addons.PureRenderMixin]
  propTypes:
    rootScene: React.PropTypes.string.isRequired
    renderScene: React.PropTypes.func.isRequired
  getInitialState: ->
    transition: 'navvc-appear'
    stack: [@props.rootScene]
  push: (route) ->
    @setState
      stack: React.addons.update(@state.stack, {$push: [route]})
      transition: 'navvc-push'
  pop: ->
    if @state.stack.length is 1
      console.warn("You shouldn't pop off the root view of a NavVC!")
    else
      @setState
        stack:  React.addons.update(@state.stack, {
          $splice:[[@state.stack.length - 1, 1]]
        })
        transition: 'navvc-pop'
  popFront: ->
    @setState
      stack: [@state.stack[0]]
      transition: 'navvc-pop'
  render: ->
    route = @state.stack[@state.stack.length - 1]
    pop = @pop if @state.stack.length > 1
    popFront = @popFront if @state.stack.length > 1
    div
      className: 'navvc'
      Transition
        transitionName: @state.transition
        @props.renderScene(route, @push, pop, popFront)

这很有效并且符合预期。这是一个使用NavVC递归推送和弹出的页面的简单示例。

Page = createView
  displayName: 'Page'
  mixins: [React.addons.PureRenderMixin]
  propTypes:
    title: React.PropTypes.string.isRequired
    push: React.PropTypes.func.isRequired
    pop: React.PropTypes.func
  push: ->
    @props.push(Math.random().toString(36).substr(2,100))
  render: ->
    div
      className: 'page'
      div
        className: 'title'
        onClick: @push
        @props.title
      cond @props.pop,
        => div
          className: 'back'
          onClick: @props.pop
          "< BACK"

App = createView
  renderScene: (route, push, pop, popFront) ->
    Page
      key: route
      title: route
      pop: pop
      push: push
  render: ->
    NavVC
      rootScene: 'Hello NavVC'
      renderScene: @renderScene


React.render App(), document.body

此处它在JSFiddle中实施。

现在这是我的问题。当子组件正在执行推送和弹出时,这非常有用。但是如果父母或兄弟组件正在进行推送或弹出呢?

假设页面本身完全没有意识到NavVC:

Page = createView
  displayName: 'Page'
  mixins: [React.addons.PureRenderMixin]
  propTypes:
    title: React.PropTypes.string.isRequired
  render: ->
    div
      className: 'page'
      div
        className: 'title'
        @props.title

假设我们有一些按钮是NavVC的兄弟姐妹/父母,它们为我们推送和弹出。

App = createView
  getInitialState: -> {}
  renderScene: (route, push, pop, popFront) ->
    @setState({push, pop})
    Page
      key: route
      title: route
  push: ->
    @state.push?(Math.random().toString(36).substr(2,100))
  render: ->
    div
      className: 'app'
      NavVC
        rootScene: 'Hello NavVC'
        renderScene: @renderScene
      cond @state.pop,
        => div
          className: 'left'
          onClick: @state.pop
          '<'
      div
        className: 'right'
        onClick: @push
        '>'

React.render App(), document.body

这会给我们带来错误。

  

不变违规:setState(...):无法在现有状态转换期间更新(例如在render内)。渲染方法应该是道具和状态的纯函数。

当App渲染时,NavVC将调用renderScene,它将调用setState,这需要立即重新渲染。目前,我提出的唯一解决方案是将setState推迟到渲染周期之后:

    window.setTimeout => 
      @setState({push, pop})
    , 0

您可以在以下JSFiddle中查看此示例的工作版本。

但是这种解决方案在某些非常根本的方面似乎是错误的。它不是声明性的,它每次都涉及两次渲染,它打破了单向数据流模式。我非常想在两个用例中使用相同的组件 - 孩子推送和弹出,或者父/兄弟推送和弹出,或者可能是孩子推动和父母弹出的混合。我似乎无法想出一个功能模式来实现这一点,而不会破坏一些非常基本的良好编程规则。

编辑1:

所以我认为我已朝着正确的方向迈出了一步:EventEmitters。按钮发出事件,NavVC侦听这些事件。我们可以通过App组件连接这些事件,NavVC将在mount上侦听这些事件,并在unmount上取消注册这些侦听器。这样可以保持单向数据流!

但仍然存在一个问题。 NavVC需要能够告诉后退按钮是否可以弹出,所以后退按钮知道它是否可以显示......我还没想出来。

1 个答案:

答案 0 :(得分:0)

好的。这是一个巨大的痛苦,但我找到了一个令人满意的解决方案 - 我喜欢一些输入。

我创建了一个代理组件的概念,它适合某些空间,可以由父节点或兄弟节点控制。

App = createView
  componentWillMount: ->
    @PushProxy = createProxy(@renderPush)
    @PopProxy = createProxy(@renderPop)
  renderPush: (push) ->
    div
      className: 'right'
      onClick: -> push(Math.random().toString(36).substr(2,100))
      '>'
  renderPop: (pop) ->
    cond pop,
      => div
        className: 'left'
        onClick: pop
        '<'
  renderScene: (route, push, pop, popFront) ->
    Page
      key: route
      title: route
  render: ->
    console.log "render app"
    div
      className: 'app'
      @PushProxy()
      @PopProxy()
      NavVC
        rootScene: 'Hello NavVC'
        renderScene: @renderScene
        pushProxy: @PushProxy
        popProxy: @PopProxy

这些代理组件从其渲染函数中的其他组件呈现。

NavVC = createView
  # clip
  render: ->
    console.log "render navvc"
    route = @state.stack[@state.stack.length - 1]
    pop = @pop if @state.stack.length > 1
    popFront = @popFront if @state.stack.length > 1
    @props.pushProxy.dispatch(@push)
    @props.popProxy.dispatch(pop)
    div
      className: 'navvc'
      Transition
        transitionName: @state.transition
        @props.renderScene(route, @push, pop, popFront)

这个方法仍然需要一个零的setTimeout,但它的优化不是为了渲染超过必要的,所以我觉得我没关系。

rememberLast = (f) ->
  lastArg = (->)
  lastResult = false
  (arg) ->
    if lastArg isnt arg
      lastResult = f(arg)
    lastArg = arg
    return lastResult

defer = (f) ->
  (arg) ->
      window.setTimeout ->
        f(arg)
      , 0

createProxy = (renderComponent) ->  
  value = undefined
  listeners = {}
  listen = (f) ->
    id = Math.random().toString(36).substr(2,100)
    listeners[id] = f
    if value isnt undefined then f(value)
    {stop: -> delete listeners[id]}
  dispatch = (x) ->
    value = x
    for id, f of listeners
      f(x)

  Proxy = createView
    displayName: 'Proxy'
    getInitialState: ->
      value: undefined
    componentWillMount: ->
      @listener = listen (value) =>
        @setState({value})
    componentWillUnMount: ->
      @listener.stop()
    render: ->
      if @state.value isnt undefined
        console.log "proxy render"
        renderComponent(@state.value)
      else
        false

  Proxy.dispatch = rememberLast defer dispatch
  return Proxy

查看this JSFiddle演示它的实际效果并查看日志语句,以便最佳地看到它的渲染效果。

我很想听听别人的反应,如果这是正确的想法。也许有一种更有原则的方式来做到这一点。