使用react挂钩的getSnapshotBeforeUpdate

时间:2018-11-29 11:02:25

标签: reactjs react-hooks

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

4 个答案:

答案 0 :(得分:1)

根据React Hooks FAQ,还没有一种方法可以实现带有钩子的getSnapshotBeforeUpdateComponentDidCatch生命周期方法

  

挂钩涵盖了类的所有用例吗?

     

我们的目标是让Hooks尽快涵盖类的所有用例   可能。没有钩子等同于罕见   getSnapshotBeforeUpdatecomponentDidCatch生命周期还没有,但是我们   计划很快添加它们。

     

对于Hooks来说还为时过早,因此一些集成如DevTools   支持或Flow / TypeScript键入可能尚未准备好。一些   第三方库也可能与Hooks不兼容   片刻。

答案 1 :(得分: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更新发生之前捕获数据,

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

中显示的相同操作

答案 2 :(得分:0)

您可以使用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

答案 3 :(得分:0)

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

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

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

useEffect() 和 useLayoutEffect()

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

  1. 您以某种方式导致渲染(更改状态或父级重新渲染)
  2. React 呈现您的组件(调用它)
  3. 屏幕经过视觉更新
  4. 然后 useEffect 运行
<块引用>

useEffect() => render() => dom 突变 => 重绘 => useEffect() [访问 dom 新状态](直接改变 dom)=> 重绘

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

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

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

useLayoutEffect() => 渲染 => dom 突变 [分离] => useLayoutEffec() [访问 dom 新状态](改变 dom)=> 重绘(提交,附加)

===> 含义 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
}

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

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

我用过这些术语!它实际上意味着什么?

enter image description here

类示例

来自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() 将在之后执行!

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