10

如何实现 getSnapshotBeforeUpdate 使用反应钩子为我提供的相同逻辑?

4

4 回答 4

7

根据React Hooks FAQgetSnapshotBeforeUpdate,还没有一种方法可以ComponentDidCatch使用钩子实现生命周期方法

Hooks 是否涵盖类的所有用例?

我们的目标是让 Hooks 尽快涵盖类的所有用例。目前还没有与 uncommon getSnapshotBeforeUpdatecomponentDidCatch生命周期等效的 Hook,但我们计划很快添加它们。

对于 Hooks 来说,现在还处于非常早期的阶段,因此一些集成(例如 DevTools 支持或 Flow/TypeScript 类型)可能还没有准备好。目前,一些第三方库也可能与 Hooks 不兼容。

于 2018-11-30T05:57:52.770 回答
1

我们无法在任何钩子(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 反应文档中显示的相同操作

于 2020-08-19T17:31:55.173 回答
0

您可以使用useMemo()而不是getSnapshotBeforeUpdate(). 在此处阅读有关如何使用 React Hooks 记忆计算的更多信息。

这里有一个简单的例子:

从列表组件的角度来看,用户输入(onChange)一个不相关的状态总是会改变,因此它会重新渲染并且可以重新渲染超过 50 次,这取决于用户输入,所以它useMemo()习惯于memoize List 组件,它说只是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
于 2019-04-08T20:37:20.013 回答
0

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

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

最后一个例子是最后一个!所以一定要检查它(我们的自定义钩子等效)。

useEffect() 和 useLayoutEffect()

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

  1. 您以某种方式导致渲染(更改状态或父级重新渲染)
  2. React 渲染你的组件(调用它)
  3. 画面视觉更新
  4. 然后 useEffect 运行

useEffect() => render() => dom mutation => repaint => useEffect() [访问dom新状态](直接改变dom)=> repaint

==> 含义 useEffect() 就像comonentDidUpdate()

另一方面, useLayoutEffect => useLayoutEffect 在渲染之后但屏幕更新之前同步运行。这些都不在了:

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

useLayoutEffect() => render => dom 变异 [分离] => useLayoutEffec() [访问 dom 新状态] (mutate dom) => repaint (commit, attach)

===> 意思useLayoutEffect()跑起来像getSnapshotBeforeUpdate()

知道这一点!getSnapshotBeforeUpdate()我们可以创建自定义钩子,允许我们使用和来做一些事情didComponentUpdate()

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

使用PreviousPropsAndState()

类似于“如何获取先前的道具和状态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 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 ...
}

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

useEffect() inuseComponentDidUpdate将在之后执行!

正如刚刚在生命周期模式中显示的那样!

于 2021-07-06T20:48:05.027 回答