乐观的渲染和useEffect

时间:2019-08-14 22:01:32

标签: reactjs react-hooks use-effect

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 个答案:

答案 0 :(得分:3)

描述方式,听起来组件的其余部分的结构类似

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

,并且当您单击“导航下一个”或“上一个”时,您不希望分页更新,直到其触发的效果解决之后。如果抓取失败,则您不希望用户看到您需要执行的任何清理工作,然后分页显示分页显示。

要达到这个目的,您是否不能通过设置中间状态“有希望”或“待定”来延迟设置“正向”值?例如,这行得通吗?

[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将触发,但不会运行其他提取。

编辑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效果,以读取和处理由生成的setState创建的值和回调,如果在编写时完全破坏了这些,我就会意识到。

我现在没有时间真正地了解细节,但是我认为必须要有useCallback这样的新钩子。

不过,我确实喜欢这个主意,因此,我将在有时间的时候尝试解决这个问题。如果您在此之前上过工作的工厂班级,或者如果有人有一个完全不同的更好的解决方案,那么我将非常有兴趣看到它。