为什么我对钩子感到失望

本文的翻译是在“ React.js Developer”课程开始之前准备的










钩子有什么用?



在我告诉你什么以及为什么让我失望之前,我想正式宣布一下,实际上,我是一个喜欢钓鱼的人



我经常听到创建钩子来替换类组件的消息。不幸的是,不幸的是,在React官方网站上“ Hooks简介”中宣传了这一创新,但不幸的是:



Hooks是React 16.8中的一项创新,它允许您使用状态和其他React功能而无需编写类。



我在这里看到的消息是这样的:“类不酷!”。不足以激发您使用挂钩。在我看来,钩子使我们能够比以前的方法更优雅地解决跨领域功能问题:mixins高阶组件渲染道具



日志记录和身份验证功能对于所有组件都是通用的,并且使用钩子可以将此类可重用功能附加到组件。



类组件有什么问题?



在无状态组件(即没有内部状态的组件)中,有一些难以理解的美丽,这些组件将props作为输入并返回一个React元素。它是一个纯函数,即没有副作用的函数。



export const Heading: React.FC<HeadingProps> = ({ level, className, tabIndex, children, ...rest }) => {
  const Tag = `h${level}` as Taggable;

  return (
    <Tag className={cs(className)} {...rest} tabIndex={tabIndex}>
      {children}
    </Tag>
  );
};


不幸的是,缺乏副作用限制了无状态组件的使用。毕竟,状态操纵至关重要。在React中,这意味着将副作用添加到有状态的类bean中。它们也称为容器组件。它们执行副作用并将道具传递给纯功能-无状态组件。



基于类的生命周期事件存在一些众所周知的问题。许多人不满意他们必须重复方法componentDidMount方法的逻辑componentDidUpdate



async componentDidMount() {
  const response = await get(`/users`);
  this.setState({ users: response.data });
};

async componentDidUpdate(prevProps) {
  if (prevProps.resource !== this.props.resource) {
    const response = await get(`/users`);
    this.setState({ users: response.data });
  }
};


迟早,所有开发人员都面临这个问题。



可以使用效果钩子在单个组件中执行此副作用代码。



const UsersContainer: React.FC = () => {
  const [ users, setUsers ] = useState([]);
  const [ showDetails, setShowDetails ] = useState(false);

 const fetchUsers = async () => {
   const response = await get('/users');
   setUsers(response.data);
 };

 useEffect( () => {
    fetchUsers(users)
  }, [ users ]
 );

 // etc.


HookuseEffect使生活变得更加轻松,但是却剥夺了我们之前使用的纯函数-无状态组件。这是令我失望的第一件事。



另一个需要了解的JavaScript范例



我今年49岁,是React的粉丝。使用此观察器开发了ember应用程序并计算出属性疯狂之后,我将始终对单向数据流产生热情。



钩子useEffect之类的问题是,它们在JavaScript环境中的任何其他地方都没有使用它。他很不寻常,而且很怪异。我只看到一种驯服它的方法-在实践中使用此钩子并遭受痛苦。而且没有计数器的例子会诱使我整夜无私地编码。我是自由职业者,不仅使用React,还使用其他库,我已经很累了跟踪所有这些创新。一旦我认为我需要安装eslint插件,这将使我走上正确的道路,那么这种新的范例就开始使我感到压力。



依赖数组是地狱



useEffect钩可以称为一个可选的第二个参数的依赖性阵列,它可以让你当你需要它回调的影响。为了确定是否发生了更改,React使用Object.is方法将这些值相互比较如果自上一个渲染周期以来任何元素已更改,则该效果将应用于新值。



比较非常适合处理原始数据类型。但是,如果元素之一是对象或数组,则会出现问题。Object.is通过引用比较对象和数组,您对此无能为力。自定义比较算法无法应用。



通过引用验证对象是已知的绊脚石。让我们看一下我最近遇到的问题的简化版本。



const useFetch = (config: ApiOptions) => {
  const  [data, setData] = useState(null);

  useEffect(() => {
    const { url, skip, take } = config;
    const resource = `${url}?$skip=${skip}&take=${take}`;
    axios({ url: resource }).then(response => setData(response.data));
  }, [config]); // <-- will fetch on each render

  return data;
};

const App: React.FC = () => {
  const data = useFetch({ url: "/users", take: 10, skip: 0 });
  return <div>{data.map(d => <div>{d})}</div>;
};


第14行,每个useFetch对象都会将一个新对象传递给函数,除非我们这样做,以便每次都使用相同的对象。在这种情况下,您需要检查对象的字段,而不是对其的引用。



我知道为什么React不会像这种解决方案那样进行深层对象比较。因此,您需要小心使用挂钩,否则可能会导致应用程序性能严重问题。我一直在想可以对此做些什么,并且已经找到了几种选择。对于更多动态对象,您将不得不寻找更多解决方法。



一个eslint插件可以自动修复错误在代码验证期间找到。它适用于任何文本编辑器。老实说,所有这些需要安装外部插件进行测试的新功能使我很烦恼。



诸如use-deep-object-compareuse-memo-one之类的插件的存在确实表明确实存在问题(或至少是一团糟)。



React依赖于钩子被调用的顺序



最早的自定义挂钩是useFetch用于向远程API发出请求的功能的多种实现。它们中的大多数不能解决从事件处理程序发出远程API请求的问题,因为挂钩只能在功能组件的开头使用。



但是,如果数据中有指向分页站点的链接,而我们想在用户单击链接时重新运行效果怎么办?这是一个简单的用例useFetch



const useFetch = (config: ApiOptions): [User[], boolean] => {
  const [data, setData] = useState<User[]>([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    const { skip, take } = config;

    api({ skip, take }).then(response => {
      setData(response);
      setLoading(false);
    });
  }, [config]);

  return [data, loading];
};

const App: React.FC = () => {
  const [currentPage, setCurrentPage] = useState<ApiOptions>({
    take: 10,
    skip: 0
  });

  const [users, loading] = useFetch(currentPage);

  if (loading) {
    return <div>loading....</div>;
  }

  return (
    <>
      {users.map((u: User) => (
        <div>{u.name}</div>
      ))}
      <ul>
        {[...Array(4).keys()].map((n: number) => (
          <li>
            <button onClick={() => console.log(' ?')}>{n + 1}</button>
          </li>
        ))}
      </ul>
    </>
  );
};


在第23行,该钩子useFetch将在第一个渲染器上被调用一次。在第35-38行中,我们渲染了分页按钮。但是我们如何useFetch从事件处理程序中为这些按钮调用钩子呢?



挂钩规则清楚地表明:



不要在循环,条件或嵌套函数内部使用挂钩;相反,始终仅在React函数的顶层使用挂钩。



每次渲染组件时,挂钩都以相同的顺序调用。造成这种情况的原因有很多,您可以从这篇出色的文章中了解到。



您不能这样做:



<button onClick={() => useFetch({ skip: n + 1 * 10, take: 10 })}>
  {n + 1}
</button>


useFetch从事件处理程序中 调用钩子违反了钩子规则,因为它们的调用顺序随每个渲染而改变。



从钩子返回可执行函数



我熟悉此问题的两种解决方案。他们采用相同的方法,我都喜欢。react-async-hook插件从钩子返回一个函数execute



import { useAsyncCallback } from 'react-async-hook';

const AppButton = ({ onClick, children }) => {
  const asyncOnClick = useAsyncCallback(onClick);
  return (
    <button onClick={asyncOnClick.execute} disabled={asyncOnClick.loading}>
      {asyncOnClick.loading ? '...' : children}
    </button>
  );
};

const CreateTodoButton = () => (
  <AppButton
    onClick={async () => {
      await createTodoAPI('new todo text');
    }}
  >
    Create Todo
  </AppButton>
);


调用该挂钩useAsyncCallback将返回具有预期负载,错误和结果属性的对象,以及execute可以从事件处理程序中调用的函数。



React-hooks-async是具有类似方法的插件。它使用一个功能useAsyncTask



这是带有简化版本的完整示例useAsyncTask



const createTask = (func, forceUpdateRef) => {
  const task = {
    start: async (...args) => {
      task.loading = true;
      task.result = null;
      forceUpdateRef.current(func);
      try {
        task.result = await func(...args);
      } catch (e) {
        task.error = e;
      }
      task.loading = false;
      forceUpdateRef.current(func);
    },
    loading: false,
    result: null,
    error: undefined
  };
  return task;
};

export const useAsyncTask = (func) => {
  const forceUpdate = useForceUpdate();
  const forceUpdateRef = useRef(forceUpdate);
  const task = useMemo(() => createTask(func, forceUpdateRef), [func]);

  useEffect(() => {
    forceUpdateRef.current = f => {
      if (f === func) {
        forceUpdate({});
      }
    };
    const cleanup = () => {
      forceUpdateRef.current = () => null;
    };
    return cleanup;
  }, [func, forceUpdate]);

  return useMemo(
    () => ({
      start: task.start,
      loading: task.loading,
      error: task.error,
      result: task.result
    }),
    [task.start, task.loading, task.error, task.result]
  );
};


createTask函数以以下形式返回任务对象。



interface Task {
  start: (...args: any[]) => Promise<void>;
  loading: boolean;
  result: null;
  error: undefined;
}


该作业状态,这是我们期望的。但是该函数还返回一个start可以稍后调用的函数使用该功能创建的作业createTask不会影响更新。该更新由forceUpdateforceUpdateRef的函数触发useAsyncTask



现在start我们有了一个可以从事件处理程序或另一段代码中调用的函数,而不必从功能组件的开头开始。



但是我们失去了在功能组件的第一次运行中调用该挂钩的能力。最好的是,react-hooks-async插件包含一个函数useAsyncRun-这使事情变得更容易:



export const useAsyncRun = (
  asyncTask: ReturnType<typeof useAsyncTask>,
  ...args: any[]
) => {
  const { start } = asyncTask;
  useEffect(() => {
    start(...args);
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [asyncTask.start, ...args]);
  useEffect(() => {
    const cleanup = () => {
      //   
    };
    return cleanup;
  });
};


该函数start将在任何参数更改时执行args现在,带有钩子的代码如下所示:



const App: React.FC = () => {
  const asyncTask = useFetch(initialPage);
  useAsyncRun(asyncTask);

  const { start, loading, result: users } = asyncTask;

  if (loading) {
    return <div>loading....</div>;
  }

  return (
    <>
      {(users || []).map((u: User) => (
        <div>{u.name}</div>
      ))}

      <ul>
        {[...Array(4).keys()].map((n: number) => (
          <li key={n}>
            <button onClick={() => start({ skip: 10 * n, take: 10 })}>
              {n + 1}
            </button>
          </li>
        ))}
      </ul>
    </>
  );
};


根据挂钩的规则,我们useFetch在功能组件的开头使用挂钩该函数useAsyncRun从一开始就调用API,start我们onClick在分页按钮的处理程序中使用该函数



现在,该挂钩useFetch可以用于其预期的目的,但是,不幸的是,您必须采用变通方法。我们也使用闭包,我必须承认,这使我有些害怕。



控制应用程序中的钩子



在应用程序中,所有内容均应按预期工作。如果计划跟踪与组件相关的问题以及用户与特定组件的交互,则可以使用LogRocket







LogRocket是一种Web应用程序录像机,几乎可以记录网站上发生的所有事件。用于React的LogRocket插件允许您查找用户会话,在此会话中用户单击了应用程序的特定组件。您将了解用户如何与组件交互以及为什么某些组件不呈现任何内容。



LogRocket记录Redux存储中的所有操作和状态。它是用于您的应用程序的一组工具,允许您记录带有标题和正文的请求/响应。他们在页面上编写HTML和CSS,甚至为最复杂的单页面应用程序提供逐像素渲染。



LogRocket提供了一种调试React应用程序的现代方法-免费试用



结论



我认为c示例可以useFetch最好地解释为什么我对钩子感到沮丧。



事实证明,实现所需的结果并不像我预期的那么容易,但是我仍然理解为什么按特定顺序使用钩子如此重要。不幸的是,由于只能在功能组件的开头调用钩子,因此我们的功能受到严重限制,我们将不得不进一步寻找解决方法。解决方案useFetch相当复杂。另外,使用钩子时,不能没有闭包。封闭是不断的惊喜,给我的心灵留下了很多伤疤。



闭包(例如传递给useEffect和的闭包useCallback)可以获取较旧版本的道具和状态值。例如,由于某种原因,当捕获的变量之一在输入数组中丢失时,就会发生这种情况-会出现困难。



在闭包中执行代码后出现的过时状态是钩子短绒旨在解决的问题之一。有很多的问题,堆栈溢出有关过时钩useEffect等。我将函数包装起来useCallback并以这种方式扭曲了依赖项数组,从而摆脱了过时的状态或渲染问题的无限重复。可以这样,但是有点烦人。这是一个真正的问题,您必须解决才能证明自己的价值。



在这篇文章的开头,我说过我总体上喜欢钩子。但是它们似乎非常复杂。在当前的JavaScript环境中,没有什么比这更好的了。每次呈现功能组件时调用钩子都会产生混合问题,而混合问题则不会。使用短绒棉来使用这种模式的需求不是很可信,并且关闭是一个问题。



我希望我只是误解了这种方法。如果是这样,请在评论中写出来。





阅读更多:






All Articles