【问题标题】:React: How to send information to the parent component?React:如何向父组件发送信息?
【发布时间】:2015-12-12 16:07:44
【问题描述】:

我创建了一个非常简单的导航视图控制器,以 iOS UINavigationController 的概念为蓝本。它基本上只是维护路线的历史,并动画推送到另一条路线或弹出。

(我希望你不介意咖啡脚本;)

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

这会给我们一个错误。

Invariant Violation:setState(...):在现有状态转换期间无法更新(例如在render 内)。渲染方法应该是 props 和 state 的纯函数。

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

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

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

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


编辑 1:

所以我想我已经找到了朝着正确方向迈出的一步:EventEmitters。按钮发出事件,NavVC 监听这些事件。我们可以通过 App 组件连接这些事件,NavVC 将在挂载时监听这些事件,并在卸载时取消注册这些监听器。这样可以保持单向数据流!

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

【问题讨论】:

    标签: coffeescript functional-programming reactjs


    【解决方案1】:

    好的。这是一个巨大的痛苦,但我想出了一个令人满意的解决方案——不过我希望能提供一些意见。

    我创建了一个代理组件的想法,该组件适合某个可以由父级或兄弟级控制的空间。

    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
    

    这些 Proxy 组件在其渲染函数中从其他组件渲染。

    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 演示它的实际效果并查看日志语句以查看其呈现最佳状态。

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

    【讨论】:

      猜你喜欢
      • 2021-03-12
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-03-27
      • 1970-01-01
      • 2012-09-02
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多