为什么我不能在事件处理程序中访问功能组件的道具?

Why can't I access my functional component's props within an event handler?

我在我的一个组件中遇到了一个奇怪的问题,即 props 和本地状态都没有出现在事件处理程序函数中。

export default function KeyboardState({layout, children}) {

  // Setup local component state
  const [contentHeight, setContentHeight] = useState(layout.height)
  const [keyboardHeight, setKeyboardHeight] = useState(0)
  const [keyboardVisible, setKeyboardVisible] = useState(false)
  const [keyboardWillShow, setKeyboardWillShow] = useState(false)
  const [keyboardWillHide, setKeyboardWillHide] = useState(false)
  const [keyboardAnimationDuration, setKeyboardAnimationDuration] = useState(INITIAL_ANIMATION_DURATION)

  // set up event listeners
  useEffect(()=> {
      let subscriptions = []
      if (Platform.OS === 'ios') {
          subscriptions = [
              Keyboard.addListener('keyboardWillShow', keyboardWillShowHandler),
              // other listeners
          ]
      }
      return ()=> subscriptions.forEach(subscription => subscription.remove())
  }, [])

  // ODD BEHAVIOR
  console.log(layout) // object: {height: 852, width: 414, x: 0, y: 44}
  console.log(contentHeight) // 550
  const keyboardWillShowHandler = (event) => {
      console.log(layout) // object: {height: 0, width: 0, x: 0, y: 0}
      console.log(contentHeight) // 0
      setKeyboardWillShow(true)
      measureEvent(event)
  }

什么可能导致这种情况发生?在事件处理程序中,我们应该可以访问本地组件属性和状态,所以我想不出一个解释。

我已经确认此组件在此期间没有收到更新的布局道具(通过使用 useEffect 并将布局作为依赖项传递给 watch)。对我来说,特别奇怪的是,事件处理程序中的日志记录仅为值为 0 的布局提供了正确的对象结构,而传入的布局属性没有改变。

编辑:
问题似乎出在我如何初始化 returns 布局的自定义挂钩上。我一直在将它初始化为一个属性为 0 的高度、宽度、x 和 y 的对象。不优雅的、特定于设备的解决方案是将它初始化为一个值与 iPhone X 的尺寸匹配的对象。但是我将如何动态地实现这一目标?为什么当组件未检测到新的传入道具值时,我的布局在事件处理程序中的 KeyboardState 中变回初始状态?

/*UseComponentSize.js (custom hook) */
const useComponentSize = (initial = initialState) => {
  const [layout, setLayout] = useState(initial)
  const onLayout = useCallback(event => {
    console.log(event)
    const newLayout = {...event.nativeEvent.layout}
    newLayout.y = newLayout.y + (Platform.OS === 'android' ? Constants.statusBarHeight : 0)
    setLayout(newLayout)
  }, [])

  return [layout, onLayout]
}

/*App.js*/
const [layout, onLayout] = useComponentSize()
return (
  <View style={styles.container}>
    <View style={styles.layout} onLayout={onLayout}>
      <KeyboardState layout={layout}>
         ...
      </KeyboardState>
   </View>
  </View>
)

编辑 2:

我通过将自定义挂钩中的布局初始化为 null 解决了这个问题,同时在布局不为 null 的情况下将我的 App.js 修改为 return。即:

/*App.js*/
const [layout, onLayout] = useComponentSize()
return (
  <View style={styles.container}>
    <View style={styles.layout} onLayout={onLayout}>
      {layout && (
        <KeyboardState layout={layout}>
           ...
        </KeyboardState>
       )}
     </View>
    </View>
  )

我仍然不知道为什么我的 KeyboardState 组件 return 在事件处理程序之外正确地编辑了布局道具,而 return 在事件处理程序。

我的处理程序函数引用了一个陈旧的值,而不是更新后的状态。

原因是,,是我的处理程序属于定义它的渲染,因此后续的重新渲染不会改变事件侦听器,因此它仍然从其渲染中读取旧值。

这是通过添加一个 ref 跟踪布局状态的变化来解决的:

export default const useKeyboard = () => {
  const [layout, _setLayout] = useState(null)
  const layoutRef = useRef(layout) // initially null
  const [contentHeight, setContentHeight] = useState(null)

  const setLayout = (value) => {
      layoutRef.current = value // keep ref update
      _setLayout(value) // then call setState
  }

  // onLayout function applied to onLayout prop of view component within App.js
  const onLayout = useCallback(event => {
      const newLayout = {...event.nativeEvent.layout}
      newLayout.y = newLayout.y + (Platform.OS === 'android' ? Constants.statusBarHeight : 0)
      setLayout(newLayout) // calls modified setLayout function
      setContentHeight(newLayout.height)
  }, [])

  useEffect(()=> {
      Keyboard.addListener('keyboardWillShow', keyboardWillShowHandler) 
      // cleanup function not shown for brevity
      }, [])

   const keyboardWillShowHandler = (event) => {
      setKeyboardWillShow(true)
      measureEvent(event)
  }
   const measureEvent = (event) => {
      const { endCoordinates: {height, screenY}, duration = INITIAL_ANIMATION_DURATION} = event
      setContentHeight(screenY - layoutRef.current.y) // access ref value
  }

  return {layout: layoutRef.current, onLayout, contentHeight}
}

我认为在这里使用 ref 不是正确的解决方案。当任何依赖关系发生变化时,我们不会更新事件侦听器,并且每次都只调用旧函数,它具有旧数据足迹。

使用 useCallback 包装您的侦听器及其所需的 deps,然后将侦听器函数作为 dep 传递,以将事件侦听器添加到新函数。

const keyboardWillShowHandler = useCallback((event) => {
      console.log(layout) // object: {height: 0, width: 0, x: 0, y: 0}
      console.log(contentHeight) // 0
      setKeyboardWillShow(true)
      measureEvent(event)
  }, [layout, contentHeight]);

useEffect(()=> {
      let subscriptions = []
      if (Platform.OS === 'ios') {
          subscriptions = [
              Keyboard.addListener('keyboardWillShow', keyboardWillShowHandler),
              // other listeners
          ]
      }
      return ()=> subscriptions.forEach(subscription => subscription.remove())
  }, [keyboardWillShowHandler])