Skip to content

[Fiber] Custom ReactFiberReconciler renderer receives old props in commitUpdate after setState in parent component. #9648

@HriBB

Description

@HriBB

Hi

I am not sure if this is a bug or not, probably I am doing something wrong, just want to make sure. I am trying to create a PaperJS bridge for React, based on ReactARTFiber.

I am running Ubuntu 16.04, Chrome Version 58.0.3029.96 (64-bit) and using a hacked version of React 16.0.0-alpha.12, where I export ReactFiberReconciler through react-dom. (I know about #9103, how can we import ReactFiberReconciler in the mean time?)

Anyway, below is the code for my App. App component has a changePathColor method, where I do this.setState({ strokeColor: newColor }), which triggers a re-render, but the commitUpdate function in PaperRenderer receives the old value for strokeColor.

BTW I have some other questions, what would be the best place to ask or learn more about fiber, apart from the source code?

import React, { Component } from 'react'
import PropTypes from 'prop-types'

import invariant from 'fbjs/lib/invariant'
import emptyObject from 'fbjs/lib/emptyObject'
import { ReactFiberReconciler } from 'react-dom'

import paper from 'paper'

const COLORS = [
  'red',
  'black',
  'green',
  'orange',
  'brown',
  'violet',
]

const TYPES = {
  LAYER: 'Layer',
  PATH: 'Path',
  CIRCLE: 'Circle',
  GROUP: 'Group',
  TOOL: 'Tool',
}

const SEGMENTS = [
  // ...
]

const SEGMENTS2 = [
  // ...
]

const Layer = TYPES.LAYER
const Path = TYPES.PATH
const Circle = TYPES.CIRCLE
const Group = TYPES.GROUP
const Tool = TYPES.TOOL

function getRandomInt(min, max) {
    return Math.floor(Math.random() * (max - min + 1)) + min
}

class App extends Component {

  constructor(props) {
    super(props)
    this.state = {
      strokeColor: COLORS[0],
      activeTool: 'move',
      width: 800,
      height: 600,
      zoom: 1,
      circles: [
        { id: 1, center: [100,100], radius: 40, strokeColor: 'black', strokeScaling: false },
        { id: 2, center: [150,150], radius: 30, strokeColor: 'red', strokeScaling: false },
        { id: 3, center: [200,200], radius: 35, strokeColor: 'green', strokeScaling: false },
      ],
    }
  }

  addCircle = () => {
    const { circles, height, width } = this.state
    const newCircles = [...circles, {
      id: circles.length + 1,
      center: [getRandomInt(0,width),getRandomInt(0,height)],
      radius: getRandomInt(10,60),
      strokeColor: COLORS[getRandomInt(0,COLORS.length-1)],
      strokeScaling: false,
    }]
    this.setState({ circles: newCircles })
  }

  changePathColor = () => {
    const { strokeColor } = this.state
    const index = COLORS.indexOf(strokeColor)
    const nextColor = COLORS[index+1] || COLORS[0]
    this.setState({ strokeColor: nextColor }, () => { // <<<<<<<<< change strokeColor
      console.log('strokeColor should be', nextColor)
    })
  }

  onWheel = (e) => {
    const { activeTool, zoom } = this.state
    if (activeTool === 'move') {
      const delta = e.wheelDelta || -e.deltaY
      const newZoom = delta > 0 ? zoom * 1.1 : zoom / 1.1
      this.setState({ zoom: newZoom })
    }
  }

  onMoveMouseDown = (e, scope) => {
    this.point = e.point
  }

  onMoveMouseDrag = (e) => {
    // TODO this.setState({ center: ... })
    e.tool.view.scrollBy(this.point.subtract(e.point))
  }

  onMoveMouseUp = (e) => {
    this.point = null
  }

  onPenMouseDown = (e) => {
    if (this.path) {
      this.path.selected = false
    }
    this.path = new paper.Path({
      segments: [e.point],
      strokeColor: 'black',
      fullySelected: true
    })
  }

  onPenMouseDrag = (e) => {
    this.path.add(e.point)
  }

  onPenMouseUp = (e) => {
  	this.path.simplify(10)
  	this.path.fullySelected = true
  }

  render() {
    const { activeTool, circles, strokeColor, height, width, zoom } = this.state
    const paperProps = {
      activeTool,
      strokeColor,
      height,
      width,
      zoom,
      onWheel: this.onWheel,
    }
    return (
      <div>
        <h1>App</h1>
        <div>
          <span>Tools: </span>
          <button onClick={() => this.setState({ activeTool: 'move' })}>Move</button>
          <button onClick={() => this.setState({ activeTool: 'pen' })}>Pen</button>
          <span> | Active tool: </span>
          <b>{activeTool}</b>
          <span> | Color: </span>
          <button onClick={this.changePathColor}>{strokeColor}</button>
          <span> | </span>
          <button onClick={this.addCircle}>Add Circle</button>
        </div>
        <Paper {...paperProps}>
          <Layer>
            <Path segments={SEGMENTS} strokeColor={strokeColor} strokeScaling={false} />
            <Group>
              <Circle center={[333,333]} radius={20} strokeColor={'black'} fillColor={'green'} strokeScaling={false} />
            </Group>
          </Layer>
          <Layer>
            <Path dashArray={[6,4]} segments={SEGMENTS2} strokeColor={strokeColor} strokeScaling={false} />
            <Group>
              <Circle center={[464,444]} radius={20} strokeColor={'black'} fillColor={'orange'} strokeScaling={false} />
            </Group>
          </Layer>
          <Layer>
            {circles.map(circle => <Circle key={circle.id} {...circle} />)}
          </Layer>
          <Tool
            name={'move'}
            onMouseDown={this.onMoveMouseDown}
            onMouseDrag={this.onMoveMouseDrag}
            onMouseUp={this.onMoveMouseUp}
          />
          <Tool
            name={'pen'}
            onMouseDown={this.onPenMouseDown}
            onMouseDrag={this.onPenMouseDrag}
            onMouseUp={this.onPenMouseUp}
          />
        </Paper>
      </div>
    )
  }

}


class Paper extends Component {

  componentDidMount() {
    const { activeTool, children, height, width, zoom } = this.props

    this._paper = new paper.PaperScope()
    this._paper.setup(this._canvas)

    this._paper.view.viewSize = new paper.Size(width, height)

    this._paper.view.zoom = zoom

    this._mountNode = PaperRenderer.createContainer(this._paper)

    PaperRenderer.updateContainer(
      children,
      this._mountNode,
      this,
    )

    this._paper.view.draw()

    if (activeTool) {
      this._paper.tools.forEach(tool => {
        if (tool.name === activeTool) {
          tool.activate()
        }
      })
    }
  }

  componentDidUpdate(prevProps, prevState) {
    const { activeTool, children, height, width, zoom } = this.props

    if (width !== prevProps.width || height !== prevProps.height) {
      this._paper.view.viewSize = new paper.Size(width, height)
    }

    if (zoom !== prevProps.zoom) {
      this._paper.view.zoom = zoom
    }

    PaperRenderer.updateContainer(
      children,
      this._mountNode,
      this,
    )

    this._paper.view.draw()

    if (activeTool !== prevProps.activeTool) {
      this._paper.tools.forEach(tool => {
        if (tool.name === activeTool) {
          tool.activate()
        }
      })
    }
  }

  componentWillUnmount() {
    PaperRenderer.updateContainer(
      null,
      this._mountNode,
      this,
    )
  }

  render() {
    console.log(this.props.strokeColor);
    const { height, onWheel, width } = this.props
    const canvasProps = {
      ref: ref => this._canvas = ref,
      height,
      onWheel,
      width,
    }
    return (
      <canvas {...canvasProps} />
    )
  }

}


function applyLayerProps(instance, props, prevProps = {}) {
  // TODO
}

function applyToolProps(tool, props, prevProps = {}) {
  // TODO
}

function applyGroupProps(tool, props, prevProps = {}) {
  // TODO
}

function applyCircleProps(instance, props, prevProps = {}) {
  if (props.center !== prevProps.center) {
    instance.center = new paper.Point(props.center)
  }
  if (props.strokeColor !== prevProps.strokeColor) {
    instance.strokeColor = props.strokeColor
  }
  if (props.strokeWidth !== prevProps.strokeWidth) {
    instance.strokeWidth = props.strokeWidth
  }
  if (props.fillColor !== prevProps.fillColor) {
    instance.fillColor = props.fillColor
  }
}

function applyPathProps(instance, props, prevProps = {}) {
  console.log('applyPathProps', props.strokeColor) // <<<<<<<< strokeColor does not change
  if (props.strokeColor !== prevProps.strokeColor) {
    instance.strokeColor = props.strokeColor
  }
  if (props.strokeWidth !== prevProps.strokeWidth) {
    instance.strokeWidth = props.strokeWidth
  }
}


const PaperRenderer = ReactFiberReconciler({

  appendChild(parentInstance, child) {
    if (child.parentNode === parentInstance) {
      child.remove()
    }

    if (
      child instanceof paper.Path &&
      (
        parentInstance instanceof paper.Layer ||
        parentInstance instanceof paper.Group
      )
    ) {
      child.addTo(parentInstance)
    }
  },

  appendInitialChild(parentInstance, child) {
    if (typeof child === 'string') {
      // Noop for string children of Text (eg <Text>{'foo'}{'bar'}</Text>)
      invariant(false, 'Text children should already be flattened.')
      return
    }

    if (
      child instanceof paper.Path &&
      (
        parentInstance instanceof paper.Layer ||
        parentInstance instanceof paper.Group
      )
    ) {
      child.addTo(parentInstance)
    }
  },

  commitTextUpdate(textInstance, oldText, newText) {
    // Noop
  },

  commitMount(instance, type, newProps) {
    // Noop
  },

  commitUpdate(instance, type, oldProps, newProps) {
    console.log('commitUpdate', instance, type, newProps)
    instance._applyProps(instance, newProps, oldProps)
  },

  createInstance(type, props, internalInstanceHandle) {
    //console.log('createInstance', type, props)
    const { children, ...paperProps } = props
    let instance

    switch (type) {
      case TYPES.TOOL:
        instance = new paper.Tool(paperProps)
        instance._applyProps = applyToolProps
        break
      case TYPES.LAYER:
        instance = new paper.Layer(paperProps)
        instance._applyProps = applyLayerProps
        break
      case TYPES.GROUP:
        instance = new paper.Group(paperProps)
        instance._applyProps = applyGroupProps
        break
      case TYPES.PATH:
        instance = new paper.Path(paperProps)
        instance._applyProps = applyPathProps
        break
      case TYPES.CIRCLE:
        instance = new paper.Path.Circle(paperProps)
        instance._applyProps = applyCircleProps
        break
    }

    invariant(instance, 'PaperReact does not support the type "%s"', type)

    instance._applyProps(instance, props)

    return instance
  },

  createTextInstance(text, rootContainerInstance, internalInstanceHandle) {
    return text
  },

  finalizeInitialChildren(domElement, type, props) {
    return false
  },

  insertBefore(parentInstance, child, beforeChild) {
    invariant(
      child !== beforeChild,
      'PaperReact: Can not insert node before itself'
    )

    child.insertAbove(beforeChild)
  },

  prepareForCommit() {
    // Noop
  },

  prepareUpdate(domElement, type, oldProps, newProps) {
    return true
  },

  removeChild(parentInstance, child) {
    //destroyEventListeners(child)

    child.remove()
  },

  resetAfterCommit() {
    // Noop
  },

  resetTextContent(domElement) {
    // Noop
  },

  getRootHostContext() {
    return emptyObject
  },

  getChildHostContext() {
    return emptyObject
  },

  scheduleAnimationCallback: window.requestAnimationFrame,

  scheduleDeferredCallback: window.requestIdleCallback,

  shouldSetTextContent(props) {
    return (
      typeof props.children === 'string' ||
      typeof props.children === 'number'
    )
  },

  useSyncScheduling: true,
})

export default App

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions