bigbigbo / issue-blog

bloooooooooog
15 stars 0 forks source link

Recompose 及 React Hooks 介绍 #6

Open bigbigbo opened 5 years ago

bigbigbo commented 5 years ago

Recompose 及 React Hooks 介绍

Recompose篇

React 中的组件形式

在认识Recompose之前,先重温几个概念。

类组件和函数式组件

概念上的东西不再赘述。简单说一下函数式组件的特点和优点:

特点:
优点:

有状态组件和无状态组件

我们将一个组件是否拥有this.state作为判断有无状态组件的唯一标准。广义上无状态的类组件也可以被称为无状态组件,但考虑到你很容易改写一个无状态的类组件为有状态组件,所以提倡由函数式组件来创建无状态组件

展示组件和容器组件

何为展示组件,顾名思义:只关心组件的展示。它一般是是无状态组件但也有可能是有状态(状态也只能是ui相关的状态)组件,但它一定是很纯的一个组件,即没有任何副作用,就像你用咖啡豆加水到咖啡机里去,出来的就是咖啡,不会是82年拉菲。这边的咖啡机就是展示组件。

容器组件则关心事物如何运作,通常说明数据是如何加载和变化的。如同现在多了一个咖啡师,他用不同的咖啡豆和其他的材料能通过咖啡机制作出不同口味的咖啡一样。但是容器组件一定不涉及展示的事,也就是说我虽然会做咖啡,但不代表我能造一个咖啡机出来呀。

记住一句话:有状态的组件没有渲染,有渲染的组件没有状态

展示组件+容器组件这样的实践一直都是社区提倡的最佳实践,这样的模式有几个好处:


什么是 Recompose?

Recompose 是一個 React 工具库。用于function componenthigher-order-component。可以把它想象为是给 React 使用的 lodash。

Recompose 旨在让使用的人忘记类组件,全部通过function component + hoc来构建我们的应用。

Recompose 能做什么?

提升状态到 function wrapper

借用 withState或者withStateHandlers这类helper提供一个更好的方式来描述state的更新:

const enhance = withState('counter', 'setCounter', 0)
const Counter = enhance(({ counter, setCounter }) =>
  <div>
    Count: {counter}
    <button onClick={() => setCounter(n => n + 1)}>Increment</button>
    <button onClick={() => setCounter(n => n - 1)}>Decrement</button>
  </div>
)

执行大部分React常见的pattern

像是lifecylecomponentFromPropwithContext,几乎所有的类组件可以实现的东西都可以通过recompose helper + 函数式组件实现。

优化render性能

与其他的library共同使用

像是 Relay、Redux 和 RxJS

建立你自己的 library

可以使用recompose提供的utility来构建自己的library,像是像是 shallowEqual()getDisplayName()

更好的实践最佳实践

Recompose借助高阶组件,使我们可以将逻辑从视图中抽离出来,做到了有状态的组件没有渲染,有渲染的组件没有状态,并且可以将代码安置到对应的logic文件夹下,即在文件目录层面上也做到了视图与逻辑分离。

现在我们要实现一个点击按钮切换模态框显隐的场景,通常情况下,遵循容器组件+展示组件的最佳实践我们会这么做:

class Wrapper extends React.Component {
  state = {
    show: false
  }

  handleToggleShow = () => {
    this.setState(({show}) => ({
      show: !show
    }))
  }

  render() {
    return <ToggleModal 
        show={this.state.show} 
        handleToggleShow={this.handleToggleShow}
      />
  }
}

// ./ToggleModal.js
const ToggleModal = (props) => {

  const { show = false, handleToggleShow } = props;

  return <React.Fragment>
    <button onClick={handleToggleShow}>切换显隐</button>
    <Modal show={show} />
  </React.Fragment>
}

这边扯一个不是今天的主角,通过render props我们也可以这样实现:

class Toggle extends React.Component {
  constructor(props) {
    super(props)

    this.state = {
      show: props.initialState
    }
  }

  toggle = () => {
    this.setState(({show}) => ({show: !show}))
  }

  render() {
    const params = {
      show: this.state.show,
      toggle: this.toggle
    }
    return this.props.children(params)
  }
}

const ToggleModal = (props) => {
  return <Toggle>
    {
      ({show, toggle}) => {
        return <React.Fragment>
          <button onClick={toggle}>切换显隐</button>
          <Modal show={show} />
        </React.Fragment>
      }
    }
  </Toggle>
}

现在我们通过Recompose来改写这个例子:

// ./logic/index.js
const withToggle = compose(
  withState('show', 'setShow', false),
  withHandlers({
    handleToggleShow: ({setShow}) => () => setShow(show => !show)
  })
)

// ./ToggleModal.js
const ToggleModal = (props) => {

  const { show = false, handleToggleShow } = props;

  return <React.Fragment>
    <button onClick={handleToggleShow}>切换显隐</button>
    <Modal show={show} />
  </React.Fragment>
}

export default withToggle(ToggleModal)

使用Recompose有什么优点呢?

更完备的例子

React Hooks 篇

React Hooks和Recompose的关联

A Note from the Author (acdlite, Oct 25 2018): Hi! I created Recompose about three years ago. About a year after that, I joined the React team. Today, we announced a proposal for Hooks. Hooks solves all the problems I attempted to address with Recompose three years ago, and more on top of that. I will be discontinuing active maintenance of this package (excluding perhaps bugfixes or patches for compatibility with future React releases), and recommending that people use Hooks instead. Your existing code with Recompose will still work, just don't expect any new features. Thank you so, so much to @wuct and @istarkov for their heroic work maintaining Recompose over the last few years.

Read more discussion about this decision here.

这是Recompose作者acdlite在10月25号更新的内容,这里大致翻译一下:

嗨!创建Recompose距离现在已经有三年时间啦。在此的一年后,我加入了React开发团队。今天我们推出了React Hooks,Hooks解决了我三年前试图用Recompose解决的所有问题,此外还有更多问题。我将停止对Recompose的维护(但是解决一些bug和兼容未来的React这些工作还是会进行的),并且推荐你使用Hooks替代Recompose。不过请放心,之前使用Recompose构建的代码依旧可以运行,只是不要期待会有新功能出现了[捂脸哭]

这段话不难看出,作者似乎准备放弃Recompose了,因为Recompose 能做的事React Hooks都能做...

初识 React Hooks ?

Hooks是React v16.7.0-alpha中加入的新特性。它可以让你在class以外使用state和其他React特性。

使用Hooks改写一下上面那个例子:

import { useState } from 'react';

function Toggle() {

  const [show, setShow] = useState(false);

  return <React.Fragment>
    <button onClick={() => setShow(!open)}>切换显隐</button>
    <Modal show={show} />
  </React.Fragment>
}

为什么会有 React Hooks ?

包含逻辑的状态复用解决方案

高阶组件和上面简单提到的render props都是为了解决逻辑复用的问题,但这两种方案都不可避免的会碰到一个问题:我们的组件会被层层叠叠的provider包裹着: image

这时候,就需要一个更底层的方案来解决逻辑状态复用的问题,所以Hooks应运而生。

复杂的组件难以理解

我们在刚开始构建我们的组件时它们往往很简单,然而随着开发的进展它们会变得越来越大、越来越混乱,各种逻辑在组件中散落的到处都是。每个生命周期钩子中都包含了一堆互不相关的逻辑。比如我们常常在componentDidMount 和 componentDidUpdate 中拉取数据,同时compnentDidMount 方法可能又包含一些不相干的逻辑,比如设置事件监听(之后需要在 componentWillUnmount 中清除)。最终的结果是强相关的代码被分离,反而是不相关的代码被组合在了一起。这显然会导致大量错误。

为了解决这个问题,Hooks允许您根据相关部分(例如设置订阅或获取数据)将一个组件分割成更小的函数,而不是强制基于生命周期方法进行分割。您还可以选择使用一个reducer来管理组件的本地状态,以使其更加可预测。

类组件所带来的问题

然而我们发现class组件可能会导致一些让我们做的这些优化白费的编码模式。类也为今天的工具带来了不少的issue。比如,classes不能很好的被minify,同时他们也造成了太多不必要的组件更新。我们想要提供一种便于优化的API。

内置Hook介绍

useState

这个hook可以让你在函数式组件中使用state

API:

const [state, setState] = useState(initialState);
// ...
setState(newState);

// lazy initialization
const [state, setState] = useState(() => {
  const initialState = someExpensiveComputation(props);
  return initialState;
});

Demo:

function Counter({initialCount}) {
  const [count, setCount] = useState(initialCount);
  return (
    <>
      Count: {count}
      <button onClick={() => setCount(0)}>Reset</button>
      <button onClick={() => setCount(prevCount => prevCount + 1)}>+</button>
      <button onClick={() => setCount(prevCount => prevCount - 1)}>-</button>
    </>
  );
}

useEffect

useEffect允许你在函数式组件做一些原本在类组件生命周期里做的事,包括订阅、定时器等其他副作用。

API:

useEffect(didUpdate, didOnValueChange: []);

如果你返回了一个函数,React会在组件卸载的时候调用它:

useEffect(() => {
  const subscription = props.source.subscribe();
  return () => {
    // Clean up the subscription
    subscription.unsubscribe();
  };
});

由于每次render都会触发useEffect的执行,所以你还可以指定第二个参数didOnValueChange

useEffect(
  () => {
    const subscription = props.source.subscribe();
    return () => {
      subscription.unsubscribe();
    };
  },
  [props.source],
);

这时候只有在props.source改变时才会重新执行useEffect

useContext

另一种使用context的方式 API:

const context = useContext(Context);

Demo:

export const themes = {
  light: {
    foreground: '#ffffff',
    background: '#222222',
  },
  dark: {
    foreground: '#000000',
    background: '#eeeeee',
  },
};

export const ThemeContext = React.createContext(
  themes.dark // 默认值
);

const ThemedButton = (props) => {
    const theme = useContext(ThemeContext);

    return <button
          {...props}
          style={{backgroundColor: theme.background}}
    />
}

useReducer

useState类似,更适合更复杂的状态改变使用。 API:

const [state, dispatch] = useReducer(reducer, initialState, initialAction);

Demo:

const initialState = {count: 0};

function reducer(state, action) {
  switch (action.type) {
    case 'reset':
      return {count: action.payload};
    case 'increment':
      return {count: state.count + 1};
    case 'decrement':
      return {count: state.count - 1};
  }
}

function Counter({initialCount}) {
  const [state, dispatch] = useReducer(
    reducer,
    initialState,
    {type: 'reset', payload: initialCount},
  );

  return (
    <>
      Count: {state.count}
      <button
        onClick={() => dispatch({type: 'reset', payload: initialCount})}>
        Reset
      </button>
      <button onClick={() => dispatch({type: 'increment'})}>+</button>
      <button onClick={() => dispatch({type: 'decrement'})}>-</button>
    </>
  );
}

第三个参数initialAction会在初始渲染的时候调用。

有一点需要注意的是,这里并不会持久化数据,如果要正在实现一个Redux功能,可以同useContext一起使用。

useReducer相比useState更适合处理更复杂的逻辑状态中使用,虽然你可以使用多个useState

useCallback

优化相关。不会重新生成function API:

const memoizedCallback = useCallback(
  () => {
    doSomething(a, b);
  },
  [a, b],
);

Demo:

// before
const Item = ({id, onClick}) => {
    return <button onClick={() => onClick(id)}>{id}</button>
}

// now
const Item = ({id, onClick}) => {
    const cb = useCallback(() => onClick(id), [id]);
    return <button onClick={cb}>{id}</button>
}

以往在这种场景,每次render都会生成新的function,可能带来潜在的性能问题,现在通过useCallback可以优化它。

useMemo

优化相关,缓存计算的值 API:

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

参考reselect的使用场景,当你的某个属性是多个其他几个props计算得来的,为了避免每次render都带来不必要的计算,可以使用useMemo:

const Demo = ({a, b, c}) => {
    const name = useMemo(() => doSomething(a,b,c), [a,b,c])
    return <p>{name}</p>
}

useRef

另一种使用ref的方式 API:

const refContainer = useRef(initialValue);

Demo:

function TextInputWithFocusButton() {
  const inputEl = useRef(null);
  const onButtonClick = () => {
    // `current` points to the mounted text input element
    inputEl.current.focus();
  };
  return (
    <>
      <input ref={inputEl} type="text" />
      <button onClick={onButtonClick}>Focus the input</button>
    </>
  );
}

useImperativeMethods

API:

useImperativeMethods(ref, createInstance, [inputs])

需要同forwardRef一同使用:

function FancyInput(props, ref) {
  const inputRef = useRef();
  useImperativeMethods(ref, () => ({
    focus: () => {
      inputRef.current.focus();
    }
  }));
  return <input ref={inputRef} ... />;
}
FancyInput = forwardRef(FancyInput);

// use
<FancyInput ref={fancyInputRef} />

// some handler
handleClick = () => {
    fancyInputRef.current.focus()
}

useMutationEffect

useEffect一致的接口,但是,在更新兄弟组件之前,它会在React执行其DOM突变的同一阶段同步触发。 使用它来执行自定义DOM突变。
API:

useMutationEffect(didUpdate, didOnValueChange: []);

使用场景有待更进一步了解,官方推荐尽可能选择useEffect

useLayoutEffect

useEffect一致的接口,但它在所有DOM突变后同步执行。使用它来从DOM读取布局并同步重新渲染。useLayoutEffect在浏览器绘制之前,将同步刷新内部计划的更新。

useLayoutEffect(didUpdate, didOnValueChange: []);

组合&自定义Hook

Hook 可以引用其他 Hook,允许我们组合和自定Hook,官网的例子:

import { useState, useEffect } from "react";

// 底层 Hooks, 返回布尔值:是否在线
function useFriendStatusBoolean(friendID) {
  const [isOnline, setIsOnline] = useState(null);

  function handleStatusChange(status) {
    setIsOnline(status.isOnline);
  }

  useEffect(() => {
    ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
    };
  });

  return isOnline;
}

// 上层 Hooks,根据在线状态返回字符串:Loading... or Online or Offline
function useFriendStatusString(props) {
  const isOnline = useFriendStatusBoolean(props.friend.id);

  if (isOnline === null) {
    return "Loading...";
  }
  return isOnline ? "Online" : "Offline";
}

// 使用了底层 Hooks 的 UI
function FriendListItem(props) {
  const isOnline = useFriendStatusBoolean(props.friend.id);

  return (
    <li style={{ color: isOnline ? "green" : "black" }}>{props.friend.name}</li>
  );
}

// 使用了上层 Hooks 的 UI
function FriendListStatus(props) {
  const statu = useFriendStatusString(props.friend.id);

  return <li>{statu}</li>;
}

这其中,useFriendStatusBooleanuseFriendStatusString 是有状态的组件,他们并没有涉及到具体的render,而FriendListItemFriendListStatus涉及到了具体的render。同样实现了刚才说的那句:有状态的组件没有渲染,有渲染的组件没有状态

但是相比Recompose,抛开性能和jsx嵌套的问题不开,Hook还是需要将logic侵入到函数式组件,不能像Recompose一样借助高阶组件通过props注入的形式来保证函数式组件的纯粹性。

其他一些Hook的使用指南

参考文章

cike8899 commented 4 years ago

如果要实现recompose方式不把逻辑侵入到展示组件,可能还是需要借助高阶组件或者将被入侵的函数组件作为容器组件,再嵌套一个纯的展示组件

bigbigbo commented 4 years ago

@cike8899 你可以参考一下这个repo的做法。 要实现不把逻辑侵入到展示组件很简单,smart 和 dumb 组件的配合使用一直也都是社区推崇的。我这边最开始使用 recompose 的初衷是逻辑也能很好地实现分层,但是当作者推荐使用 hooks 的时候我也一直在使用 hooks 了。