Ste*_*son 5 reactjs react-hooks use-effect
TL'DR:有没有办法在对 API 调用使用 useEffect 时悲观地操作状态更改?
假设您编写了一个显示分页/排序数据网格的组件,并且您为该组件编写了一个 useEffect 类似于这个假示例:
useEffect(() => {
fetch(`https://some-random-api.com/data/page/{paging}/sort/{sorting}`)
.then(response => response.json())
.then(data => setState({
data
}));
}, [paging, sorting]);Run Code Online (Sandbox Code Playgroud)
所以,如果我没记错的话,这将在分页或排序状态/道具(whatev)更新时获取。这意味着它在加载数据之前乐观地呈现新的排序和分页状态。基本上,该应用程序会告诉用户“我在请求的页面上,但我仍在加载它”。换句话说,应用程序状态的一部分(分页和排序)表示“我完成了”,状态的另一部分(加载)表示“我正在处理它”。
相比之下,如果我悲观地更新状态,我只会在获取之前将加载状态设置为 true,然后在收到响应时(并且仅在那时)设置分页、排序(& 数据和加载)。因此,该应用程序会告诉用户“我听到了您的意见,我正在处理它”,然后当收到成功响应时,该应用程序会说“这是请求的数据和更新的上下文(更新的页面/排序状态)”。
另一个问题是请求是否失败。在 optimisitic/useEffect 方法中,我现在需要恢复在尝试加载这些状态更改的数据之前更新的分页和排序状态。这种复杂性随着依赖项的数量而增加。另外,这里是关键(再次,如果我没记错的话)恢复这些状态将导致另一个获取(这可能会再次失败)。在悲观方法中,您只需在发生错误时设置 loading = false 并且不会进行其他状态更改,因为分页和排序仅在成功接收数据时更新。数据以悲观的方式(不乐观)保持同步,这就是为什么我认为当博客和视频在使用 useEffect 时说“从同步的角度思考”是具有讽刺意味的。
还有一件事,比如说从 useEffect 内部调用的 API(在其他一些例子中)实际上正在改变服务器端的某些东西。在这种情况下,当使用 useEffect 并应用它的乐观方法时,只会对用户撒谎。该应用程序说“好的,我更新了”。也许不吧。如果没有(如果有错误),希望有状态更新来恢复对用户撒谎的状态更改,并在恢复该状态时以某种方式避免额外的 API 调用。希望用户看到状态变回并希望有一条消息......即便如此,这是一个好的用户体验吗?
我并不是说乐观渲染总是不好的。我的意思是“我大部分时间都喜欢悲观。我如何在使用 useEffect 时做到这一点?” :)
似乎 useEffect 正在被用于和推广应用程序的大多数副作用(很多)。我并不经常发现它很有用。我是一个悲观主义者,希望获得替代方案。
按照您描述的方式,听起来组件的其余部分的结构如下
[state, setState] => useState(initialState)
[paging, setPaging] => useState(initialPaging)
[sorting, setSorting] => useState(initialSort)
useEffect(() => {
fetch(`https://some-random-api.com/data/page/{paging}/sort/{sorting}`)
.then(response => response.json())
.then(data => setState({
data
}));
}, [paging, sorting]);
return <div>
<span>Current page is {paging}</span>
<Button onClick={() => setPaging(page => page + 1)}>Nav Next</Button>
<Button onClick={() => setPaging(page => page - 1)}>Nav Prev</Button>
// ... and so on and so forth
</div>
Run Code Online (Sandbox Code Playgroud)
当您单击“导航下一个”或“上一个”时,您不希望在其触发的效果解决之前更新分页。如果获取失败,您不希望用户看到分页从您需要执行的任何清理中向上然后向下移动。
为了实现这一目标,您不能通过中间的“希望”或“待定”状态值来延迟设置“前向”值吗?例如,这行得通吗?
[state, setState] => useState(initialState)
// Component state values
[paging, setPaging] => useState(initialPaging)
[sorting, setSorting] => useState(initialSort)
// "Optimist's" state values
[pendingPaging, setPendingPaging] => useState(paging)
[pendingSorting, setPendingSorting] => useState(sorting)
useEffect(() => {
// checks if current state values differ from pending,
// meaning app wants to do something
// If pending and "actual" values match, do nothing
if( paging !== pendingPaging
|| sorting !== pendingSorting
){
fetch(`https://some-random-api.com/data/page/{paging}/sort/{sorting}`)
.then(response => response.json())
.then(data => {
// things worked, updated component state to match pending
setPaging(pendingPaging);
setSorting(pendingSorting);
setState({data});
})
.catch(err => {
// ugh, I knew things would fail. Let me reset my hopes
setPendingPaging(paging);
setpendingSorting(sorting);
setState({data});
});
}
}, [pendingPaging, pendingSorting, paging, sorting, setState]);
return <div>
// show user "actual" page number
<span>Current page is {paging}</span>
// but update only pending values in response to events
<Button onClick={() => setPendingPaging(page + 1)}>Nav Next</Button>
<Button onClick={() => setPendingPaging(page - 1)}>Nav Prev</Button>
// ... and so on and so forth
</div>
Run Code Online (Sandbox Code Playgroud)
通过这种方式,你可以设置你希望的状态,做一些事情,然后设置你的状态和期望相匹配。如果成功,显示的状态将更新以匹配乐观值;如果失败,待定值将设置回实际值。
我看到的最大问题是,当异步调用解析时,会同时进行多个 setState 调用。React 应该批处理这些调用,但您可能想查找在单个调用中处理此问题的其他方法。
无论哪种方式,挂起值和实际值都是相等的,因此虽然 useEffect 将触发,但它不会运行另一次提取。
编辑 1 感谢您的积极反馈,很高兴您喜欢这个概念。我同意复杂性可能会变得难以处理,但我认为您可以设置一个工厂类来更清晰地管理事物。
例如,您可以使用一个makePessimistic最终输出“实际”状态、“希望”状态和至少一个usePessimisticEffect回调的类。该类可能如下所示:
class pessimistFactory {
constructor(){
this.state = {};
this.effects = [];
}
// util from https://dzone.com/articles/how-to-capitalize-the-first-letter-of-a-string-in
ucfirst = (string) => string.charAt(0).toUpperCase() + string.slice(1);
registerState(param, initialVal){
this.state[param] = initialVal;
}
makePessimisticEffect = (callback, dependencies) => useEffect(() => {
async function handlePessimistically(){
try {
const result = await callback(...dependencies);
// update theState with result
setState({...hopefulState, result})
}catch(err){
// reset hopefulState and handle error
setHopefulState({...theState})
}
}
// Do something if some action pending
if(/* pending and current state not the same */) handlePessimistically();
}, [callback, dependencies]);
registerEffect(callback, dependencies = []){
this.effects.push(
this.makePessimisticEffect(callback, dependencies)
);
}
makeHooks = () => {
const initialState = useState(this.state);
return {
theState: initialState,
hopefulState: initialState,
effects: this.effects
}
}
}
Run Code Online (Sandbox Code Playgroud)
在实践中:
// Make factory instance
const factory = new pessimistFactory();
// Register state and effects
factory.registerState('paging', initialPaging)
factory.registerState('sorting', initialSorting)
factory.registerEffect('fetchData', () => fetch(`https://some-random-api.com/data/page/{paging}/sort/{sorting}`))
// Make the hooks
const {theState, hopefulState, effects} = factory.makeHooks();
// Implement hooks in component
const SomeComponent = () => {
const [state, setState] = theState();
const [pendingState, setPendingState] = hopefulState();
effects.forEach(useEffect => useEffect());
return <div>displayed stuff</div>
}
Run Code Online (Sandbox Code Playgroud)
最棘手的部分是设置makePessimisticEffect效果来读取和处理由 generated s 创建的值和回调setState,我意识到如果我编写它时完全损坏了。
我现在没有时间真正弄清楚细节,但我认为使用像useCallback.
不过,我确实喜欢这个想法,所以稍后当我有时间时我会尝试解决这个问题。如果您在此之前制作了一个工作工厂类,或者如果有人有一个完全不同的更好的解决方案,我将非常有兴趣看到它。
| 归档时间: |
|
| 查看次数: |
894 次 |
| 最近记录: |