React Hooks 使用实践 | 七日打卡

834 阅读4分钟

前言

目前来说,每个前端团队都有自己对 React Hooks 的使用偏好,在基于 hooks 的数据流管理或许没有所谓的最佳实践,但是开发过程中还是有一些准则可以参考。当你不熟悉一个新东西的时候,你就该去看它的文档。

React Hooks 是完全不同以往的心智模型,从使用开始我们就不应该以之前的思维去强行套用。要理解 Function Component 的最大特点就是更加彻底的状态驱动,虽然有很多文章写了如何用 Hooks 模拟生命周期,但是其实这里生命周期的概念早已名存实亡。

在实际使用中,我们只要牢牢记住“状态/流”的思想,所有的事件、请求和数据变化都依赖于该作用的“输入流”。把握住这点,我们再去考虑数据流变化后的触发效果,考虑作用是否需要触发?“输入流”是否可变?想清楚后就放心大胆地写吧。

流的思想

人不能两次踏进同一条河流

怎么通过流的思想去理解 Hooks 的使用呢?这里我们可以举几个例子:

逝者如斯夫,不舍昼夜

props 和 state 都是不可变的常量(const)!每次 render 的时候,Function Component 相当于重新执行了一遍。每次都是新的 state,新的 effect,历史的状态则被保留在 React 的 “快照”中 。这里不再是this.statetihs能够保留不变性,多次修改;而是可以理解成多次渲染生成多份状态。

正如官方文档的这个例子,不指定依赖的情况下,effect 在每次渲染的时候都会执行。这就是为什么 React 会在执行当前 effect 之前对上一个 effect 进行清除。

function FriendStatus(props) {
  const [isOnline, setIsOnline] = useState(null);

  useEffect(() => {
    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }
    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    // Specify how to clean up after this effect:
    return function cleanup() {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });

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

水之形,避高而趋下

正如水流只能从上游流向下游,在 React 中,状态也是单向的。因此,对于跨组件数据共享,由于数据只能从组件树的父节点传递给子节点,需要我们提供特殊的策略,比如 Context。

const ThemeContext = React.createContext(themes.default);
// 生产者
function App() {
  const [color, setColor] = useState(themes.light);
  return (
    <ThemeContext.Provider value={color}>
      <Toolbar1 />
      <Toolbar2 />
    </ThemeContext.Provider>
  );
}
// 消费者
function Toolbar(props) {
  const theme = useContext(ThemeContext);
  return (
    <div>
    	<button style={{ background: theme.background, color: theme.foreground }}>
        I am styled by theme context!
    	</button>
    </div>
  );
}

水是有流向性的,hooks 的也依赖于调用的顺序,我们可以把一个 state 作为参数传递给下一个 hooks。

function ChatRecipientPicker() {
  const [recipientID, setRecipientID] = useState(1);
  const isRecipientOnline = useFriendStatus(recipientID);

  return (
  //...
  )
}

流的实践

其实用 hooks 在一些应用场景中,可以实现很简洁的写法。比如请求一个 List,我们把“分页”、“查询”、“重刷”都当作状态来处理,请求操作只要依赖这些状态即可。这样看来,这里的用法其实很像 Rxjs 的combineLatest

const App = () => {
  const [pagination, setPagination] = useState<object>(defaultPagination);
  const [searchValue, setSearchValue] = useState<object>(defaultSearchValue);
  const [refresh, setRefresh] = useState<number>(0);
  useEffect(() => {
    // fetch
  }, [pagination, searchValue, refresh])
};

Hooks 优化实践

个人实践总结,hooks 的优化一般可以从两个方面入手:

  • 一是通过比较,需要的时候才进行渲染
  • 二是使用不依赖/不变量跳过渲染

compare 优化

由于父组件每次 render 都可能引起 props 的改变,因此传递的值尽量使用 useMemo 或者 useCallback 优化。

  • useCompareEffect
// https://github.com/kentcdodds/use-deep-compare-effect
type Effect = typeof React.useEffect;
type EffectDeps = Parameters<Effect>[1];

export const useDeepCompareEffect: Effect = (effect, deps?) => {
  const prevRef = useRef<EffectDeps>();
  const deepDep = useRef<number>(0);
  if (!deepEqual(prevRef.current, deps)) {
    prevRef.current = deps;
    deepDep.current = deepDep.current + 1;
  }
  // eslint-disable-next-line react-hooks/exhaustive-deps
  return useEffect(effect, [deepDep.current]);
};
  • useDeepCompareMemo
type Memo = typeof React.useMemo;
type MemoDeps = Parameters<Memo>[1];

export const useDeepCompareMemo: Memo = (factory, deps?) => {
  const prevRef = useRef<MemoDeps>();
  const deepDep = useRef<number>(0);
  if (!deepEqual(prevRef.current, deps)) {
    prevRef.current = deps;
    deepDep.current = deepDep.current + 1;
  }
  // eslint-disable-next-line react-hooks/exhaustive-deps
  return useMemo(factory, [deepDep.current]);
};
  • usePrevious 如果我们想拿到上次渲染的状态,再进行比较,可以用usePrevious
// https://github.com/streamich/react-use
export default function usePrevious<T>(state: T): T | undefined {
  const ref = useRef<T>();
  useEffect(() => {
    ref.current = state;
  });
  return ref.current;
}

// compare
const App = () => {
  const [count, setCount] = useState<number>(0);
  const prevCount = usePrevious<number>(count);
  useEffect(() => {
    if(count > prevCount + 5) {
      //...
    }
  }, [count])
}

不依赖/不变量

  • defaultProps 对于不变的默认参数,我们可以写在函数以外
const defaultValue = { status: "OPEN" };
const App = ({ value = defaultValue }) => {
	//...
};
// or
App.defaultProps = {
  value: { status: "OPEN" }
}
  • useRef 对于某个数据,如果不需要被依赖又要保证最新的值可以保存在ref
const App = ({ onChange }) => {
  const callback = useRef(onChange);
  callback.current = onChange;
  useEffect(() => {
    //...
    dom.addEventListener("click", callback.current);
    //...
  }, [])
};
  • 函数式 setState
const App = () => {
  const [count, setCount] = useState<number>(0);
  useEffect(() => {
  	setCount(count => count + 1);
  }, [])
};

总结

主要还是讲了个人的一些实践经验,前端数据流这部分还有很多内容需要理解和学习,先做下记录吧~

参考文章