getSnapshotBeforeUpdate 使用反应挂钩

getSnapshotBeforeUpdate using react hooks

如何使用 React Hooks 实现 getSnapshotBeforeUpdate 给我的相同逻辑?

根据 React Hooks FAQ,还没有办法使用挂钩实现 getSnapshotBeforeUpdateComponentDidCatch 生命周期方法

Do Hooks cover all use cases for classes?

Our goal is for Hooks to cover all use cases for classes as soon as possible. There are no Hook equivalents to the uncommon getSnapshotBeforeUpdate and componentDidCatch lifecycles yet, but we plan to add them soon.

It is a very early time for Hooks, so some integrations like DevTools support or Flow/TypeScript typings may not be ready yet. Some third-party libraries might also not be compatible with Hooks at the moment.

您可以使用 useMemo() 而不是 getSnapshotBeforeUpdate()。在此处阅读有关 how to memoize calculations with React Hooks.

的更多信息

举个简单的例子:

总是用户键入 (onChange) 从列表组件的角度来看不相关的状态被更改,因此它重新呈现并且可以重新呈现超过 50 次,这取决于用户键入, 所以它被用来 useMemo() 来记忆列表组件并且它声明只有 todoList 监听。

import List from './List'

const todo = (props) => {
  const [inputIsValid, setInputIsValid] = useState(false)

  const inputValidationHandler = (event) => {
    if(event.target.value.trim() === '') {
      setInputIsValid(false)
    } else {
      setInputIsValid(true)
    }
  }

  return <React.Fragment>
    <input
      type="text"
      placeholder="Todo"
      onChange={inputValidationHandler}
    />
    {
      useMemo(() => (
        <List items={todoList} onClick={todoRemoveHandler} />
      ), [todoList])
    }
  </React.Fragment>

}

export default todo

我们无法在任何挂钩(useLayoutEffect 或 useEffect)中获取快照数据,因为两者都会在触发时提供更新的 DOM 值,捕获数据的最佳位置就在之前设置状态。例如这里我在设置状态之前捕获滚动位置。

function ChatBox(props){

  const [state, setState] = useState({chatFetched:[],isFetching:false});

  const listRef = useRef();
  const previousScrollDiff = useRef(0);
  
  // on mount 
  useEffect(()=>{
    getSomeMessagesApi().then(resp=>{
      const chatFetched = [...state.chatFetched,...resp];
      setState({chatFetched});
    })
  },[]);

  useLayoutEffect(()=>{
   // use the captured snapshot here
   listRef.current.scrollTop = listRef.current.scrollHeight - previousScrollDiff.current;

  },[state.chatFetched])
  
  useEffect(()=>{

    // don't use captured snapshot here ,will cause jerk effect in scroll

  },[state.chatFetched]);


  const onScroll = (event) => {
    const topReached = (event.target.scrollTop === 0);

    if(topReached && !state.isFetching){

      setState({...state, isFetching:true});

      getSomeMessagesApi().then(resp=>{
        const chatFetched = [...resp,...state.chatFetched];

        // here I am capturing the data ie.., scroll position

        previousScrollDiff.current = listRef.current.scrollHeight -listRef.current.scrollTop;
        setState({chatFetched, isFetching:false});
      })
    }
  }

  return (  
    <div className="ui container">
      <div 
        className="ui container chat list" 
        style={{height:'420px', width:'500px',overflow:'auto'}}
        ref={listRef}
        onScroll={onScroll}
        >
          {state.chatFetched.map((message)=>{
           return <ChatLi data ={message} key ={message.key}></ChatLi>
          })}
      </div>  
    </div>
   ); 
};

我们还可以在 dom 更新发生之前使用 Memo 来捕获数据,

function ChatBox(props){

  const [state, setState] = useState({chatFetched:[],isFetching:false});

  const listRef = useRef();
  const previousScrollDiff = useRef(0);
  
  // on mount 
  useEffect(()=>{
    getSomeMessagesApi().then(resp=>{
      const chatFetched = [...state.chatFetched,...resp];
      setState({chatFetched});
    })
  },[]);

  useLayoutEffect(()=>{
   // use the captured snapshot here
   listRef.current.scrollTop = listRef.current.scrollHeight - previousScrollDiff.current;

  },[state.chatFetched])
  
  useEffect(()=>{

    // don't use captured snapshot here ,will cause jerk effect in scroll

  },[state.chatFetched]);

  useMemo(() => {
   // caputure dom info in use effect
    if(scrollUl.current){
       previousScrollDiff.current = scrollUl.current.scrollHeight - scrollUl.current.scrollTop;
    }
    
  }, [state.chatFetched]);

  const onScroll = (event) => {
    const topReached = (event.target.scrollTop === 0);

    if(topReached && !state.isFetching){

      setState({...state, isFetching:true});

      getSomeMessagesApi().then(resp=>{
        const chatFetched = [...resp,...state.chatFetched];
        setState({chatFetched, isFetching:false});
      })
    }
  }

  return (  
    <div className="ui container">
      <div 
        className="ui container chat list" 
        style={{height:'420px', width:'500px',overflow:'auto'}}
        ref={listRef}
        onScroll={onScroll}
        >
          {state.chatFetched.map((message)=>{
           return <ChatLi data ={message} key ={message.key}></ChatLi>
          })}
      </div>  
    </div>
   ); 
};

在上面的示例中,我尝试执行与 getSnapshotBeforeUpdate react doc

中显示的相同的操作

简短回答:没有反应挂钩!但是我们可以创建一个自定义的!

那是在使用 useEffect()useLayoutEffect()!因为它们是关键要素!

最后的例子都在最后!所以一定要检查它(我们的自定义挂钩等价物)。

useEffect() 和 useLayoutEffect()

useEffect => useEffect 运行 是异步的,渲染被绘制到屏幕后。

  1. 您以某种方式导致渲染(更改状态,或父级重新渲染)
  2. React 呈现您的组件(调用它)
  3. 屏幕视觉更新
  4. 然后使用效果 运行s

useEffect() => render() => dom mutation => repaint => useEffect() [access dom new state] (changing dom directly) => repaint

==> 意思是 useEffect() 就像 comonentDidUpdate()!

useLayoutEffect => useLayoutEffect,另一方面,运行s 在渲染之后但在屏幕更新之前同步。就是这样:

  1. 您以某种方式导致渲染(更改状态,或父级重新渲染)
  2. React 呈现您的组件(调用它)
  3. useLayoutEffect 运行s,React 等待它完成。
  4. 屏幕视觉更新

useLayoutEffect() => render => dom mutation [detached] => useLayoutEffec() [access dom new state] (mutate dom) => repaint (commit, attach)

===> 意思是 useLayoutEffect() 运行 喜欢 getSnapshotBeforeUpdate()

知道这个!我们可以创建自定义挂钩,允许我们使用 getSnapshotBeforeUpdate()didComponentUpdate().

做一些事情

这样的例子是更新聊天应用程序中自动更新的滚动条!

usePreviousPropsAndState()

类似于“how to get previous prop and state”中提到的 usePrevious() 钩子

这是一个用于保存和获取以前的道具和状态的钩子实现!

const usePrevPropsAndState = (props, state) => {
  const prevPropsAndStateRef = useRef({ props: null, state: null })
  const prevProps = prevPropsAndStateRef.current.props
  const prevState = prevPropsAndStateRef.current.state

  useEffect(() => {
    prevPropsAndStateRef.current = { props, state }
  })

  return { prevProps, prevState }
}

我们可以看到我们需要如何传递 props 和 state 对象!

你传递的就是你得到的!所以很容易使用!对象会很好!

useGetSnapshotBeforeUpdate & useComponentDidUpdate

这里是完整的解决方案或实施

const useGetSnapshotBeforeUpdate = (cb, props, state) => {
  // get prev props and state
  const { prevProps, prevState } = usePrevPropsAndState(props, state)

  const snapshot = useRef(null)


// getSnapshotBeforeUpdate (execute before the changes are comitted for painting! Before anythingg show on screen) - not run on mount + run on every update
  const componentJustMounted = useRef(true)
  useLayoutEffect(() => {
    if (!componentJustMounted.current) { // skip first run at mount
           snapshot.current = cb(prevProps, prevState)  
    }
    componentJustMounted.current = false
  })


 // ________ a hook construction within a hook with closure __________
 const useComponentDidUpdate = cb => {
    // run after the changes are applied (commited) and apparent on screen
    useEffect(() => {
      if (!componentJustMounted.current) { // skip first run at mount
        cb(prevProps, prevState, snapshot.current)
      }
    })
  }
  // returning the ComponentDidUpdate hook!
  return useComponentDidUpdate
}

您可以注意到我们是如何在另一个钩子中构建钩子的!使用闭包!并直接访问元素!并连接两个钩子!

预提交阶段和提交阶段(和效果挂钩)

我用过那些术语!它究竟意味着什么?

class 例子

来自 the doc

class ScrollingList extends React.Component {
  constructor(props) {
    super(props);
    this.listRef = React.createRef();
  }

  getSnapshotBeforeUpdate(prevProps, prevState) {
    // Are we adding new items to the list?
    // Capture the scroll position so we can adjust scroll later.
    if (prevProps.list.length < this.props.list.length) {
      const list = this.listRef.current;
      return list.scrollHeight - list.scrollTop;
    }
    return null;
  }

  componentDidUpdate(prevProps, prevState, snapshot) {
    // If we have a snapshot value, we've just added new items.
    // Adjust scroll so these new items don't push the old ones out of view.
    // (snapshot here is the value returned from getSnapshotBeforeUpdate)
    if (snapshot !== null) {
      const list = this.listRef.current;
      list.scrollTop = list.scrollHeight - snapshot;
    }
  }

  render() {
    return (
      <div ref={this.listRef}>{/* ...contents... */}</div>
    );
  }
}

我们的自定义挂钩等价物

const App = props => {
  // other stuff ...

  const useComponentDidUpdate = useGetSnapshotBeforeUpdate(
    (prevProps, prevState) => {
      if (prevProps.list.length < props.list.length) {
        const list = listRef.current;
        return list.scrollHeight - list.scrollTop;
      }
      return null;
    },
    props,
    state
  )

  useComponentDidUpdate((prevProps, prevState, snapshot) => {
    if (snapshot !== null) {
      const list = listRef.current;
      list.scrollTop = list.scrollHeight - snapshot;
    }
  })

  // rest ...
}

useGetSnapshotBeforeUpdate 钩子中的 useEffectLayout() 将首先执行!

useComponentDidUpdate中的useEffect()将在!

之后执行

正如生命周期模式中所示!