通过 Redux 和 Hooks 无缝获取数据

自从 react 在16.7版本发布了试验性的 Hooks 特性之后,它已经风靡 react 社区。 我特别喜欢将数据从组件中提取出来。平时我通过一个 SomeEntitiesLoader 高阶组件来管理数据获取以及在需要的时候重新获取。我们来看一个简单的实现:

import { Component } from 'react';

export class ItemsLoader extends Component {
  componentDidMount() {
    const { loadItems, sort, sortDirection } = this.props;

    loadItems(sort, sortDirection);
  }

  componentDidUpdate(prevProps) {
    const { sort, sortDirection, loadItems } = this.props;

    if (prevProps.sort !== sort || prevProps.sortDirection !== sortDirection) {
      loadItems(sort, sortDirection);
    }
  }

  render () {
    const { search, sort, sortDirection, render, items, isLoading } = this.props;

    return render({
      items: items.filter(item => item.includes(search)), 
      isLoading
    });
  }
}

export default connect(
  state => ({
    items: state.items,
    isLoading: state.isLoading
  }),
  {
    loadItems: loadItems
  }
)(ItemsLoader)

看起来很简单是不是,假设我们在后端排序,在前端搜索,那么当排序字段和排序方向变了之后我们需要重新加载。但是很可能你会有多个数据源头需要去加载,而且我们想要重用这个高阶组件。那我们就需要重写这个组件。

为了重用我把 redux 的 connect 去掉了,同时新加了一些属性:

  • loadProps - 数据获取所需的参数
  • shouldReload - 将现有属性和接收到的属性进行对比,决定是否需要重新加载
  • renderProps - 传递的 render 方法
import { Component } from 'react';

export class SharedLoader extends Component {
  componentDidMount() {
    const { loadItems, ...rest } = this.props;

    loadItems(loadProps(rest));
  }

  componentDidUpdate(prevProps) {
    const { loadItems, loadProps, shouldReload, ...rest } = this.props;

    if (shouldReload(rest, prevProps)) {
      loadItems(loadProps(rest));
    }
  }

  render () {
    const { render, renderProps } = this.props;

    return render(...renderProps);
  }
}

现在我们可以在任何加载器上应用这个组件了。当我们想重用这个组件,只需要通过 redux connect 或任何其他方法重新定义这些属性就好了。

import { connect } from 'react-redux';
import * as R from 'ramda';
import { SharedLoader } from './SharedLoader';

const pickLoadProps = R.pick(['sort', 'sortDirection']);

export default connect(
  // map state to props
  state => ({
    items: state.items,
    isLoading: state.isLoading,
  }),
  // map dispatch to props
  {
    loadItems: loadItems,
  },
  // merge props
  (stateProps, dispatchProps, ownProps) => ({
    ...stateProps,
    ...dispatchProps,
    ...ownProps,
    loadProps: R.pick(['sort', 'sortDirection']),
    shouldReload: (current, prev) => !R.equals(pickLoadProps(current), pickLoadProps(prev)),
    renderProps: R.pick(['sort', 'sortDirection', 'search', 'items', 'isLoading']),
  })
)(SharedLoader);

这里有一个使用这个组件的例子。我们把属性传给加载器,然后根据传递给加载器的属性来渲染结果。

	import React from 'react';
  import ProductsLoader from './ProductsLoader';
  import { ProductsList } from './ProductsList';

  export const ProductsPage = ({ productsSort, productsSortDirection, productsSearch }) => (
    <ProductsLoader
      sort={productsSort}
      sortDirection={productsSortDirection}
      search={productsSearch}
      render={({ items, isLoading, sort, sortDirection, search }) => (
        <ProductsList 
          products={items} 
          isLoading={isLoading} 
          sort={sort} 
          sortDirection={sortDirection} 
          search={search} 
        />
      )}
    />
  );

试试 React Hooks ?

我们的 SharedLoader 可以工作,但是对于这么一个小用途来说太过于笨重了,我们不得不调用两个生命周期函数然后再对属性执行对比。它也允许我们把不相关的逻辑放到这些方法里。

我们想要做的就是把组件分割成小一点的相关的函数。根据 React Hooks 文档

使用 Hooks,你可以把状态逻辑从组件提取出来,这样就会方便复用以及测试。Hooks 让你可以复用有状态的逻辑而不用改变组件层级。这样就可以很容易地在许多组件之间或者社区分享 Hooks了。

** useEffect hook ** 接受一个包含命令式的,可能有副作用代码的函数为参数。

Mutations, subscriptions, timers, logging, 以及其它副作用是不被允许在函数组件内的(在 React 的 render 阶段被引用)。这样会导致令人困惑的 bug 以及 UI 层面的不一致性。

根据文档,传递给 useEffect 的函数在布局和绘制完成之后触发。当组件改变执行完了后执行副作用比较合适,然后我们可以用 hook 来替换 componentDidMount 和 componentDidUpdate。

effect 依赖的值作为数组传递给 useEffect 的第二个参数,因此我们可以把这些值传递给 useEffect 的第二个参数来替换属性比较的逻辑。

我们来重写下 SharedLoader:

	import { useEffect } from 'react';

export default function SharedLoader({ loadItems, loadProps, memoProps, renderProps, render, ...rest }) {
  // when we pass empty array effect runs only once after mount
  useEffect(() => {
    loadItems(loadProps(rest));
  }, []);

  // run effect when memoProps output is change
  useEffect(() => {
    loadItems(loadProps(rest));
  }, memoProps(rest));

  return render(renderProps(rest));
}

如你所见,我们用两个 effect 调用替换了 componentDidMount 和 componentDidUpdate 。组件 API 唯一的改变是移除了 shouldReload 属性,增加了 memoProps 函数。

memoProps 是一个接受 loadProps 参数并将对象转换为值的数组简单方法。

const mempProps = R.compose(R.values, loadProps);

总结

最后我们有了一个相对较小的组件,专门用来在属性改变的时候加载数据。我们可以完全控制这个逻辑,而且在我们需要加载数据的大多数常用场景中复用。

现在我们是在前端搜索数据,但如果需要将参数传递给后端,我们也只需要重写我们的 loadProps 和 memoProps ,当需求真的要改变时我们也很容易做出改变。

原文地址:https://medium.com/@egorsapronov/seamless-data-fetching-with-redux-and-react-hooks-108ece925d92