如何实现 getSnapshotBeforeUpdate 使用反应钩子为我提供的相同逻辑?
4 回答
根据React Hooks FAQgetSnapshotBeforeUpdate
,还没有一种方法可以ComponentDidCatch
使用钩子实现生命周期方法
Hooks 是否涵盖类的所有用例?
我们的目标是让 Hooks 尽快涵盖类的所有用例。目前还没有与 uncommon
getSnapshotBeforeUpdate
和componentDidCatch
生命周期等效的 Hook,但我们计划很快添加它们。对于 Hooks 来说,现在还处于非常早期的阶段,因此一些集成(例如 DevTools 支持或 Flow/TypeScript 类型)可能还没有准备好。目前,一些第三方库也可能与 Hooks 不兼容。
我们无法在任何钩子(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 反应文档中显示的相同操作
您可以使用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
简短的回答:没有反应钩子!但是我们可以创建一个自定义的!
那是使用useEffect()
and useLayoutEffect()
!因为它们是关键元素!
最后一个例子是最后一个!所以一定要检查它(我们的自定义钩子等效)。
useEffect() 和 useLayoutEffect()
useEffect => useEffect 在渲染被绘制到屏幕后异步运行。
- 您以某种方式导致渲染(更改状态或父级重新渲染)
- React 渲染你的组件(调用它)
- 画面视觉更新
- 然后 useEffect 运行
useEffect() => render() => dom mutation => repaint => useEffect() [访问dom新状态](直接改变dom)=> repaint
==> 含义 useEffect() 就像comonentDidUpdate()
!
另一方面, useLayoutEffect => useLayoutEffect 在渲染之后但屏幕更新之前同步运行。这些都不在了:
- 您以某种方式导致渲染(更改状态或父级重新渲染)
- React 渲染你的组件(调用它)
- useLayoutEffect 运行,React 等待它完成。
- 画面视觉更新
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
将在之后执行!
正如刚刚在生命周期模式中显示的那样!