乐观渲染 & useEffect

Posted

技术标签:

【中文标题】乐观渲染 & useEffect【英文标题】:Optimistic rendering & useEffect 【发布时间】:2019-12-21 11:28:49 【问题描述】:

TL'DR:有没有办法在使用 useEffect 进行 API 调用时悲观地操作状态变化?

假设您编写了一个显示分页/排序数据网格的组件,并且您为该组件编写了一个 useEffect,类似于这个假示例:

useEffect(() => 
  fetch(`https://some-random-api.com/data/page/paging/sort/sorting`)
    .then(response => response.json())
    .then(data => setState(
      data
    ));
, [paging, sorting]);

所以,如果我没记错的话,这将在分页或排序状态/道具 (whatev) 更新时获取。这意味着它在加载数据之前乐观地呈现新的排序和分页状态。基本上,该应用程序告诉用户“我在请求的页面上,但我仍在加载它”。换句话说,应用程序状态的一部分(分页和排序)说“我完成了”,而状态的另一部分(加载)说“我正在处理它”。

相比之下,如果我悲观地更新状态,我只会在获取之前将加载状态设置为 true,然后在收到响应时(并且仅在那时)设置分页、排序(& 数据和加载)。所以应用程序告诉用户“我听到了你的声音,我现在正在处理它”,然后当收到成功的响应时,应用程序会说“这是请求的数据和更新的上下文(更新的页面/排序状态)”。

另一个问题是请求是否失败。在 optimisitic/useEffect 方法中,我现在需要恢复在尝试加载这些状态更改的数据之前更新的分页和排序状态。这种复杂性随着依赖项的数量而增加。另外,这里是踢球者(再次,如果我没记错的话)恢复这些状态将导致另一个获取(可能再次失败)。在悲观的方法中,您只需在发生错误时设置 loading = false 并且无需进行其他状态更改,因为仅在成功接收数据时才更新分页和排序。数据以悲观的方式(不乐观)保持同步,这就是为什么当博客和视频在使用 useEffect 时说“从同步的角度考虑”时,我认为这很讽刺。

还有一件事,比如从 useEffect 中调用的 API(在其他示例中)实际上正在更改服务器端的某些内容。在那种情况下,使用 useEffect 并应用它的乐观方法只是在欺骗用户。该应用程序说“好的,我更新了”。也许不吧。如果没有(如果有错误),希望有状态更新来恢复对用户撒谎的状态更改,并在恢复该状态时以某种方式避免额外的 API 调用。希望用户看到状态变回来,希望有消息......即使那样,这是一个好的用户体验吗?

我并不是说乐观渲染总是不好的。我说的是“我大部分时间都喜欢悲观。在使用 useEffect 时我该怎么做?” :)

似乎 useEffect 被用于和推广应用程序的大多数副作用(很多)。我发现它经常没有用。我是一个悲观主义者,并希望启用另一种选择。

【问题讨论】:

【参考方案1】:

按照您的描述方式,您组件的其余部分的结构听起来像

[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>

当您单击 Nav Next 或 Prev 时,您不希望在它触发的效果解决之前更新分页。如果获取失败,您不希望用户看到分页在您需要执行的任何清理操作中上升然后下降。

要实现这一点,您难道不能通过设置中间的“有希望”或“待定”状态值来延迟设置“前向”值吗?例如,这行得通吗?

[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>

通过这种方式,您可以设置您希望的状态,做某事,然后设置您的状态和期望以匹配。如果成功,显示的状态会更新以匹配乐观值,如果失败,挂起的值会被设置回现实。

我看到的最大问题是,当异步调用解决时,会同时进行多个 setState 调用。 React 应该 batch these calls,但您可能想在单个调用中查找处理此问题的其他方法。

无论哪种方式,挂起的值和实际值是相等的,所以虽然 useEffect 会触发,但它不会运行另一个 fetch。

编辑 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
    
  

在实践中:

// 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>

最棘手的部分是设置makePessimisticEffect 效果来读取和处理由生成的setStates 创建的值和回调,我意识到如果我编写它时完全损坏了。

我现在没有时间真正弄清楚细节,但我认为使用useCallback 等可用的新钩子必须是可能的。

不过,我确实喜欢这个想法,所以我会在以后有时间的时候尝试解决这个问题。如果您在此之前制作了一个工作工厂课程,或者如果有人有一个完全不同的更好的解决方案,我会非常有兴趣看到它。

【讨论】:

聪明!我可能会采用这个,然后将状态放在一个 useState 中,并使用回调 setState API 进行更新。或者使用Reducer。唯一的问题是,我不想重复这种复杂性(对于同一应用程序中的其他 API 调用)或如果依赖项列表要增长则维护它。我想知道是否有一种方法可以提取/抽象/封装一个可重复使用的钩子。一个 usePessimisticEffect 钩子,如果你愿意的话……可能有办法。我需要玩一会儿。谢谢!!!如果没有其他人很快有其他内容,我会将其标记为答案。 我同意您关于复杂性和封装性的反馈 - 我更新了我的回答作为回应。

以上是关于乐观渲染 & useEffect的主要内容,如果未能解决你的问题,请参考以下文章

探索Mysql锁机制——乐观锁&悲观锁

探索Mysql锁机制——乐观锁&悲观锁

基于redis的乐观锁

Mysql锁机制--乐观锁 & 悲观锁

MySQL悲观锁&乐观锁

02-mybatis-plus更新操作&自动填充&乐观锁