React挂钩最佳做法是将引用存储到第三方库

时间:2019-08-19 22:31:44

标签: javascript reactjs react-hooks

我正在围绕pusher-js创建一个包装钩子库,以发布到野外。对于每个钩子(即useChannelusePresenceChanneluseTrigger),我需要保留对Pusher实例的引用,即存储在上下文提供程序中的new Pusher()。我允许传递第三方身份验证,因此我需要即时创建Pusher实例。我不确定是否应该将其存储在useState或useRef中。

eslint-plugin-react-hooks规则抱怨useState和useRef的各种组合来存储它。在尝试正确清理每个副作用时,我也看到了不良的副作用。我不确定什么是最佳做法。

这是具有重要细节的简化实现。有关我的问题,请参见下面的注释1. 2.和3.。

// PusherContext.jsx
export const PusherContext = React.createContext();
export function PusherProvider({key, config, ...props}){
  // 1. Should I store third party libraries like this?
  const clientRef = useRef();
  // vs const [client, setClient] = useState();

  // when config changes, i.e. config.auth, re-create instance
  useEffect(() => {
    clientRef.current && clientRef.current.disconnect();
    clientRef.current = new Pusher(key, {...config});
  }, [clientRef, config]);

  return <PusherContext.Provider value={{ client }} {...props} />
}

// useChannel.js
export function useChannel(
  channelName, 
  eventName, 
  callback, 
  callbackDeps
){
  const { client } = useContext(PusherContext);
  const callbackRef = useCallback(callback, callbackDeps);  

  // 2. Or should I be using state instead?
  const [channel, setChannel] = useState();
  // vs. const channelRef = useRef();

  useEffect(() => {
    if(client.current){
      const pusherChannel = client.current.subscribe(channelName);
      pusherChannel.bind(eventName, callbackRef.current);
      setChannel(pusherChannel);
    }

    // can't cleanup here because callbackRef changes often.
    // 3. eslint: Mutable values like 'client.current' aren't valid dependencies because mutating them doesn't re-render the component
  }, [client.current, callbackRef])

  // cleanup for unbinding the event
  // re-bind makes sense with an updated callback ref
  useEffect(() => {
    channel.unbind(eventName)
  }, [client, channel, callbackRef, eventName]);

  // cleanup for unsubscribing from the channel
  useEffect(() => {
    clientRef.unsubscribe(channelName);
  }, [client, channelName])
}

任何建议,过去的例子或模式都非常感谢,因为我想钉上这个!

1 个答案:

答案 0 :(得分:1)

我会使用ref将Pusher的新实例保存为recommended by Dan

您最初不需要通过内部检查来进行空检查和断开连接(clientRef.current && clientRef.current.disconnect()进行清理,因为每当运行useEffect时,React在返回时处理它都会断开连接声明。

export function PusherProvider({ key, config, ...props }) {
  // 1. Should I store third party libraries like this?
  const clientRef = useRef();
  // vs const [client, setClient] = useState();

  // when config changes, i.e. config.auth, re-create instance
  // useEffect(() => {
  //   clientRef.current && clientRef.current.disconnect();
  //   clientRef.current = new Pusher(key, { ...config });
  // }, [clientRef, config]);

  // Create an instance, and disconnect on the next render
  // whenever clientRef or config changes.
  useEffect(() => {
    clientRef.current = new Pusher(key, { ...config });

    // React will take care of disconnect on next effect run.
    return () => clientRef.current.disconnect();
  }, [clientRef, config]);

  return <PusherContext.Provider value={{ client }} {...props} />;
}

对于第二种情况,我尽力在下面的内联中写建议。

要点是un/subscription是相关事件,因此应该以相同的效果进行处理(与PusherProvider的情况一样)。

// useChannel.js
export function useChannel(channelName, eventName, callback, callbackDeps) {
  const { client } = useContext(PusherContext);
  const callbackRef = useCallback(callback, callbackDeps);

  // 2. Or should I be using state instead?
  // I believe a state makes sense as un/subscription depends on the channel name.
  // And it's easier to trigger the effect using deps array below.
  const [channel, setChannel] = useState();

  useEffect(() => {
    // do not run the effect if we don't have the Pusher available.
    if (!client.current) return;

    const pusherChannel = client.current.subscribe(channelName);
    pusherChannel.bind(eventName, callbackRef.current);
    setChannel(pusherChannel);

    // Co-locate the concern by letting React
    // to take care of un/subscription on each channel name changes
    return () => client.current.unsubscribe(channelName);

    // Added `channelName` in the list as the un/subscription occurs on channel name changes.
  }, [client.current, callbackRef, channelName]);

  // This.. I am not sure... ?
  // cleanup for unbinding the event
  // re-bind makes sense with an updated callback ref
  useEffect(() => {
    channel.unbind(eventName);
  }, [client, channel, callbackRef, eventName]);

  // Moved it up to the first `useEffect` to co-locate the logic
  // // cleanup for unsubscribing from the channel
  // useEffect(() => {
  //   clientRef.unsubscribe(channelName);
  // }, [client, channelName]);
}