{"id":330125,"date":"2022-02-25T15:00:53","date_gmt":"2022-02-25T15:00:53","guid":{"rendered":"http:\/\/savepearlharbor.com\/?p=330125"},"modified":"-0001-11-30T00:00:00","modified_gmt":"-0001-11-29T21:00:00","slug":"","status":"publish","type":"post","link":"https:\/\/savepearlharbor.com\/?p=330125","title":{"rendered":"<span>\u041f\u0438\u0448\u0435\u043c \u0441\u043e\u0431\u0441\u0442\u0432\u0435\u043d\u043d\u044b\u0435 \u043f\u043e\u043b\u0435\u0437\u043d\u044b\u0435 React-\u0445\u0443\u043a\u0438 \u043d\u0430 TypeScript \u0438 \u0442\u0435\u0441\u0442\u0438\u0440\u0443\u0435\u043c \u0438\u0445 \u0441 React Testing Library (\u0447\u0430\u0441\u0442\u044c 1)<\/span>"},"content":{"rendered":"<div><\/div>\n<div id=\"post-content-body\">\n<div>\n<div class=\"article-formatted-body article-formatted-body_version-2\">\n<div xmlns=\"http:\/\/www.w3.org\/1999\/xhtml\">\n<figure class=\"full-width\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/c03\/be4\/6eb\/c03be46eb2e742519ea4e32bb55dbcd8.png\" width=\"1000\" height=\"420\" data-src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/c03\/be4\/6eb\/c03be46eb2e742519ea4e32bb55dbcd8.png\"\/><figcaption><\/figcaption><\/figure>\n<p><em>\u0414\u0430\u043d\u043d\u0430\u044f \u0441\u0442\u0430\u0442\u044c\u044f \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u043f\u0435\u0440\u0435\u0432\u043e\u0434\u043e\u043c <\/em><a href=\"https:\/\/dev.to\/kirillshvets97\/writing-useful-custom-react-hooks-with-typescript-and-testing-them-with-react-testing-library-2me\" rel=\"noopener noreferrer nofollow\"><em>\u043c\u043e\u0435\u0439 \u0441\u043e\u0431\u0441\u0442\u0432\u0435\u043d\u043d\u043e\u0439 \u0441\u0442\u0430\u0442\u044c\u0438<\/em><\/a><em>, \u0440\u0430\u043d\u0435\u0435 \u043e\u043f\u0443\u0431\u043b\u0438\u043a\u043e\u0432\u0430\u043d\u043d\u043e\u0439 \u043d\u0430 dev.to.<\/em><\/p>\n<p>\u0414\u0443\u043c\u0430\u044e, React-\u0445\u0443\u043a\u0438 \u043d\u0435 \u043d\u0443\u0436\u0434\u0430\u044e\u0442\u0441\u044f \u0432 \u043f\u0440\u0435\u0434\u0441\u0442\u0430\u0432\u043b\u0435\u043d\u0438\u0438, \u043f\u043e\u044d\u0442\u043e\u043c\u0443 \u043c\u043e\u0436\u043d\u043e \u043f\u0440\u043e\u043f\u0443\u0441\u0442\u0438\u0442\u044c \u0438\u0445 \u043e\u043f\u0438\u0441\u0430\u043d\u0438\u0435 \u0438 \u043f\u0440\u0438\u0441\u0442\u0443\u043f\u0438\u0442\u044c \u0441\u0440\u0430\u0437\u0443 \u043a \u0434\u0435\u043b\u0443.<\/p>\n<p>\u0415\u0441\u043b\u0438 \u0432\u044b \u0445\u043e\u0442\u0438\u0442\u0435 \u043f\u043e\u0441\u043c\u043e\u0442\u0440\u0435\u0442\u044c \u0438\u0441\u0445\u043e\u0434\u043d\u044b\u0439 \u043a\u043e\u0434, \u0442\u043e \u043c\u043e\u0436\u0435\u0442\u0435 \u043f\u043e\u0441\u0435\u0442\u0438\u0442\u044c \u044d\u0442\u043e\u0442 <a href=\"https:\/\/github.com\/kirillshvets97\/custom-react-hooks-demo\" rel=\"noopener noreferrer nofollow\">\u0440\u0435\u043f\u043e\u0437\u0438\u0442\u043e\u0440\u0438\u0439<\/a> \u043d\u0430 GitHub.<\/p>\n<h2>\u041c\u0438\u043d\u0438\u043c\u0430\u043b\u044c\u043d\u044b\u0435 \u0442\u0440\u0435\u0431\u043e\u0432\u0430\u043d\u0438\u044f<\/h2>\n<p>\u0414\u0430\u043d\u043d\u044b\u0439 \u0442\u0443\u0442\u043e\u0440\u0438\u0430\u043b \u043f\u0440\u0435\u0434\u043f\u043e\u043b\u0430\u0433\u0430\u0435\u0442, \u0447\u0442\u043e \u0443 \u0432\u0430\u0441 \u0443\u0436\u0435 \u0435\u0441\u0442\u044c (\u0445\u043e\u0442\u044f \u0431\u044b \u043c\u0438\u043d\u0438\u043c\u0430\u043b\u044c\u043d\u044b\u0439) \u043e\u043f\u044b\u0442 \u0440\u0430\u0431\u043e\u0442\u044b \u0441 React-\u0445\u0443\u043a\u0430\u043c\u0438 (\u0442\u0430\u043a\u0438\u043c\u0438, \u043a\u0430\u043a useEffect, useState, useRef), TypeScript, Jest \u0438 React Testing Library.<\/p>\n<p>\u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u0438\u043d\u0441\u0442\u0440\u0443\u043c\u0435\u043d\u0442\u044b \u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f \u0441\u0430\u043c\u043e\u0441\u0442\u043e\u044f\u0442\u0435\u043b\u044c\u043d\u043e, \u043d\u043e \u0434\u043b\u044f \u0431\u044b\u0441\u0442\u0440\u043e\u0433\u043e \u0441\u0442\u0430\u0440\u0442\u0430 \u044f \u0440\u0435\u043a\u043e\u043c\u0435\u043d\u0434\u0443\u044e \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0443\u0442\u0438\u043b\u0438\u0442\u0443 <a href=\"https:\/\/create-react-app.dev\/docs\/getting-started\/\" rel=\"noopener noreferrer nofollow\">Create React App<\/a>, \u043a\u043e\u0442\u043e\u0440\u0430\u044f \u043f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u043b\u044f\u0435\u0442 \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f \u0441 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0435\u043c Jest \u0438 React Testing Library <a href=\"https:\/\/create-react-app.dev\/docs\/running-tests\/\" rel=\"noopener noreferrer nofollow\">&#171;\u0438\u0437 \u043a\u043e\u0440\u043e\u0431\u043a\u0438&#187;<\/a>.<\/p>\n<p>\u0415\u0441\u043b\u0438 \u0432\u044b \u0445\u043e\u0442\u0438\u0442\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c (\u0438\u043b\u0438 \u0443\u0436\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0435) Next.js, \u0442\u043e \u0432\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u0443\u0437\u043d\u0430\u0442\u044c \u043a\u0430\u043a \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c Jest \u0438 React Testing Library \u0434\u043b\u044f Next.js <a href=\"https:\/\/nextjs.org\/docs\/testing\" rel=\"noopener noreferrer nofollow\">\u0437\u0434\u0435\u0441\u044c<\/a>.<\/p>\n<h2>\u0427\u0442\u043e \u043c\u044b \u0431\u0443\u0434\u0435\u043c \u0434\u0435\u043b\u0430\u0442\u044c<\/h2>\n<p>\u041d\u0430\u0448\u0430 \u0433\u043b\u0430\u0432\u043d\u0430\u044f \u0446\u0435\u043b\u044c &#8212; \u043d\u0430\u043f\u0438\u0441\u0430\u0442\u044c \u0441\u043e\u0431\u0441\u0442\u0432\u0435\u043d\u043d\u044b\u0435 React-\u0445\u0443\u043a\u0438 \u043d\u0430 TypeScript \u0438 \u043f\u0440\u043e\u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0438\u0445, \u043f\u043e\u043b\u0443\u0447\u0438\u0432 <strong>100% \u043f\u043e\u043a\u0440\u044b\u0442\u0438\u0435 (coverage)<\/strong>.<\/p>\n<p>\u0414\u0430\u0432\u0430\u0439\u0442\u0435 \u043d\u0430\u0447\u043d\u0435\u043c!<\/p>\n<h2>\u041f\u0440\u0438\u0441\u0442\u0443\u043f\u0430\u0435\u043c \u043a \u0440\u0430\u0431\u043e\u0442\u0435<\/h2>\n<p>\u0414\u043b\u044f \u0434\u0435\u043c\u043e\u043d\u0441\u0442\u0440\u0430\u0446\u0438\u0438 \u044f \u0431\u0443\u0434\u0443 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435, \u0441\u043e\u0437\u0434\u0430\u043d\u043d\u043e\u0435 \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e Create React App.<\/p>\n<pre><code class=\"bash\"># npx npx create-react-app custom-react-hooks-demo --template typescript # yarn yarn create react-app custom-react-hooks-demo --template typescript<\/code><\/pre>\n<p>\u0414\u0430\u043b\u0435\u0435, \u043d\u0430\u043c \u043d\u0443\u0436\u043d\u043e \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c \u043f\u0430\u043a\u0435\u0442 <code>react-hooks-testing-library<\/code>. \u042d\u0442\u043e\u0442 \u043f\u0430\u043a\u0435\u0442 \u043f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u043b\u044f\u0435\u0442 \u043d\u0430\u0431\u043e\u0440 \u0443\u0442\u0438\u043b\u0438\u0442, \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0435\u043d\u043d\u043e \u0443\u043f\u0440\u043e\u0449\u0430\u044e\u0449\u0438\u0445 \u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 React-\u0445\u0443\u043a\u043e\u0432. \u0423\u0437\u043d\u0430\u0442\u044c \u0431\u043e\u043b\u044c\u0448\u0435 \u043e \u0434\u0430\u043d\u043d\u043e\u043c \u043f\u0430\u043a\u0435\u0442\u0435 \u0432\u044b \u043c\u043e\u0436\u0435\u0442\u0435 <a href=\"https:\/\/github.com\/testing-library\/react-hooks-testing-library\" rel=\"noopener noreferrer nofollow\">\u0437\u0434\u0435\u0441\u044c<\/a>.<\/p>\n<pre><code class=\"bash\"># npm npm i @testing-library\/react-hooks --save-dev # yarn yarn add @testing-library\/react-hooks -D<\/code><\/pre>\n<h2>\u041f\u043e\u0434\u043f\u0438\u0441\u043a\u0430 \u043d\u0430 \u043d\u0430\u0436\u0430\u0442\u0438\u0435 \u043a\u043b\u0430\u0432\u0438\u0448\u0438<\/h2>\n<p>\u0414\u0430\u0432\u0430\u0439\u0442\u0435 \u043f\u0440\u0435\u0434\u0441\u0442\u0430\u0432\u0438\u043c, \u0447\u0442\u043e \u043d\u0430\u043c \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0437\u0430\u043a\u0440\u044b\u0432\u0430\u0442\u044c \u043c\u043e\u0434\u0430\u043b\u044c\u043d\u043e\u0435 \u043e\u043a\u043d\u043e \u043f\u043e \u043d\u0430\u0436\u0430\u0442\u0438\u044e \u043a\u043b\u0430\u0432\u0438\u0448\u0438 Escape.<\/p>\n<p>\u0412\u043e\u0442 \u043a\u0430\u043a \u044d\u0442\u043e \u0432\u044b\u0433\u043b\u044f\u0434\u0438\u0442:<\/p>\n<figure class=\"full-width\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/c68\/e14\/72d\/c68e1472da6387f4e8515fa7ccd1bdc8.gif\" width=\"960\" height=\"530\" data-src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/c68\/e14\/72d\/c68e1472da6387f4e8515fa7ccd1bdc8.gif\"\/><figcaption><\/figcaption><\/figure>\n<p>\u0420\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044f \u0445\u0443\u043a\u0430:<\/p>\n<pre><code class=\"javascript\">import { useEffect, useRef } from 'react';  const useKeydown = (key: string, callback: (event: Event) => void) => {   const callbackRef = useRef(callback);    useEffect(() => {     callbackRef.current = callback;   }, [callback]);    useEffect(() => {     const handler: EventListener = (event) => {       if ((event as KeyboardEvent).key === key) {         callbackRef.current(event);       }     }      document.addEventListener('keydown', handler);     return () => document.removeEventListener('keydown', handler);   }, [key]); }  export default useKeydown;<\/code><\/pre>\n<p>\u0412\u043d\u0443\u0442\u0440\u0438 \u044d\u0442\u043e\u0433\u043e \u0445\u0443\u043a\u0430 \u0441\u043e\u0437\u0434\u0430\u0435\u0442\u0441\u044f \u0444\u0443\u043d\u043a\u0446\u0438\u044f, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c\u0430\u044f \u0432 \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0435 \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u0430 \u0441\u043e\u0431\u044b\u0442\u0438\u044f. \u0414\u0430\u043d\u043d\u044b\u0439 \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a \u0441\u0440\u0430\u0432\u043d\u0438\u0432\u0430\u0435\u0442 \u0441\u0432\u043e\u0439\u0441\u0442\u0432\u043e <code>key<\/code> \u0438\u0437 \u043e\u0431\u044a\u0435\u043a\u0442\u0430 \u0441\u043e\u0431\u044b\u0442\u0438\u044f \u0441 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u043e\u043c <code>key<\/code>, \u043f\u0435\u0440\u0435\u0434\u0430\u043d\u043d\u044b\u043c \u0432 \u0441\u0430\u043c \u0445\u0443\u043a, \u0438, \u0435\u0441\u043b\u0438 \u043e\u043d\u0438 \u0438\u043c\u0435\u044e\u0442 \u043e\u0434\u0438\u043d\u0430\u043a\u043e\u0432\u043e\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435, \u0432\u044b\u0437\u044b\u0432\u0430\u0435\u0442 \u043a\u043e\u043b\u043b\u0431\u044d\u043a, \u043f\u0435\u0440\u0435\u0434\u0430\u043d\u043d\u044b\u0439 \u0432 \u0445\u0443\u043a. <\/p>\n<p>\u0422\u0430\u043a\u0436\u0435, \u043c\u044b \u0441\u043e\u0445\u0440\u0430\u043d\u044f\u0435\u043c \u043f\u0435\u0440\u0435\u0434\u0430\u043d\u043d\u044b\u0439 \u043a\u043e\u043b\u043b\u0431\u044d\u043a \u0432 \u0440\u0435\u0444, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044f <code>useRef<\/code>, \u0438 \u043e\u0431\u043d\u043e\u0432\u043b\u044f\u0435\u043c \u0435\u0433\u043e \u043f\u043e \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e\u0441\u0442\u0438 \u0432\u043d\u0443\u0442\u0440\u0438 \u043f\u0435\u0440\u0432\u043e\u0433\u043e \u0432\u044b\u0437\u043e\u0432\u0430 <code>useEffect<\/code>, \u0447\u0442\u043e\u0431\u044b \u0433\u0430\u0440\u0430\u043d\u0442\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0435 \u0432\u0441\u0435\u0433\u0434\u0430 \u0430\u043a\u0442\u0443\u0430\u043b\u044c\u043d\u043e\u0433\u043e \u043a\u043e\u043b\u043b\u0431\u044d\u043a\u0430 \u0431\u0435\u0437 \u043f\u0435\u0440\u0435\u0441\u043e\u0437\u0434\u0430\u043d\u0438\u044f \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u0430 \u0441\u043e\u0431\u044b\u0442\u0438\u044f \u0432\u043d\u0443\u0442\u0440\u0438 \u0432\u0442\u043e\u0440\u043e\u0433\u043e \u0432\u044b\u0437\u043e\u0432\u0430 <code>useEffect<\/code>.<\/p>\n<p>\u0422\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u0445\u0443\u043a\u0430:<\/p>\n<pre><code class=\"javascript\">import { fireEvent } from '@testing-library\/react'; import { renderHook } from '@testing-library\/react-hooks'; import useKeydown from '.\/useKeydown';  describe('useKeydown', () => {   test('should handle keydown event', () => {     const callback = jest.fn();     const event = new KeyboardEvent('keydown', {       key: 'Escape',     });      const view = renderHook(() => useKeydown('Escape', callback));      expect(callback).toHaveBeenCalledTimes(0);     fireEvent(document, event);     expect(callback).toHaveBeenCalledTimes(1);      \/\/ \u0422\u0435\u0441\u0442\u0438\u0440\u0443\u0435\u043c, \u0447\u0442\u043e \"removeEventListener\" \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442 \u043a\u043e\u0440\u0440\u0435\u043a\u0442\u043d\u043e,     \/\/ \u043f\u0440\u043e\u0432\u0435\u0440\u044f\u044f, \u0447\u0442\u043e \u043a\u043e\u043b\u043b\u0431\u044d\u043a \u0432\u044b\u0437\u044b\u0432\u0430\u043b\u0441\u044f \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u0438\u043d \u0440\u0430\u0437 \u043f\u043e\u0441\u043b\u0435 \u0440\u0430\u0437\u043c\u043e\u043d\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f.     jest.spyOn(document, 'removeEventListener');      view.unmount();     expect(document.removeEventListener).toHaveBeenCalledTimes(1);      fireEvent(document, event);     expect(callback).toHaveBeenCalledTimes(1);   });    test('shouldn`t handle unnecessary keydown event', () => {     const callback = jest.fn();     const event = new KeyboardEvent('keydown', {       key: 'Enter',     });      renderHook(() => useKeydown('Escape', callback));      expect(callback).toHaveBeenCalledTimes(0);     fireEvent(document, event);     expect(callback).toHaveBeenCalledTimes(0);   }); });<\/code><\/pre>\n<p>\u041a\u0430\u043a \u0432\u044b \u0437\u043d\u0430\u0435\u0442\u0435, \u0445\u0443\u043a\u0438 \u043c\u043e\u0433\u0443\u0442 \u0431\u044b\u0442\u044c \u0432\u044b\u0437\u0432\u0430\u043d\u044b \u0442\u043e\u043b\u044c\u043a\u043e \u0432\u043d\u0443\u0442\u0440\u0438 \u0442\u0435\u043b\u0430 \u0444\u0443\u043d\u043a\u0446\u0438\u043e\u043d\u0430\u043b\u044c\u043d\u043e\u0433\u043e \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430. \u0414\u0430\u043d\u043d\u044b\u0439 \u0444\u0430\u043a\u0442 \u0437\u0430\u0441\u0442\u0430\u0432\u043b\u044f\u0435\u0442 \u0441\u043e\u0437\u0434\u0430\u0432\u0430\u0442\u044c \u0432\u043d\u0443\u0442\u0440\u0438 \u0442\u0435\u0441\u0442\u0430 \u043b\u0438\u0448\u043d\u0438\u0439 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u043d\u0443\u0436\u0435\u043d \u0442\u043e\u043b\u044c\u043a\u043e \u0440\u0430\u0434\u0438 \u0432\u044b\u0437\u043e\u0432\u0430 \u0442\u0435\u0441\u0442\u0438\u0440\u0443\u0435\u043c\u043e\u0433\u043e \u0445\u0443\u043a\u0430. \u0418\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0435 \u043c\u0435\u0442\u043e\u0434\u0430 <code>renderHook<\/code> \u0438\u0437 \u043f\u0430\u043a\u0435\u0442\u0430 <code>react-hooks-testing-library<\/code> \u043f\u043e\u0437\u0432\u043e\u043b\u044f\u0435\u0442 \u043d\u0430\u043c \u043f\u0440\u043e\u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0445\u0443\u043a \u0431\u0435\u0437 \u043d\u0430\u043f\u0438\u0441\u0430\u043d\u0438\u044f \u0434\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0433\u043e \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430, \u043e\u0431\u043e\u0440\u0430\u0447\u0438\u0432\u0430\u044f \u0442\u0435\u0441\u0442\u0438\u0440\u0443\u0435\u043c\u044b\u0439 \u0445\u0443\u043a \u0442\u0430\u043a, \u043a\u0430\u043a \u0435\u0441\u043b\u0438 \u0431\u044b \u043e\u043d \u0431\u044b\u043b \u0432\u044b\u0437\u0432\u0430\u043d \u0432\u043d\u0443\u0442\u0440\u0438 \u043d\u0430\u0441\u0442\u043e\u044f\u0449\u0435\u0433\u043e \u0444\u0443\u043d\u043a\u0446\u0438\u043e\u043d\u0430\u043b\u044c\u043d\u043e\u0433\u043e \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430.<\/p>\n<p>\u0414\u0430\u043d\u043d\u044b\u0439 \u0442\u0435\u0441\u0442 \u043f\u0440\u043e\u0432\u0435\u0440\u044f\u0435\u0442, \u0447\u0442\u043e:<\/p>\n<ul>\n<li>\n<p>\u043e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a \u0441\u043e\u0431\u044b\u0442\u0438\u044f \u0431\u044b\u043b \u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u043e\u0437\u0434\u0430\u043d \u043f\u0440\u0438 \u043c\u043e\u043d\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0438 \u0438 \u043a\u043e\u0440\u0440\u0435\u043a\u0442\u043d\u043e \u0443\u0434\u0430\u043b\u0435\u043d \u043f\u0440\u0438 \u0440\u0430\u0437\u043c\u043e\u043d\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0438;<\/p>\n<\/li>\n<li>\n<p>\u043f\u0435\u0440\u0435\u0434\u0430\u043d\u043d\u044b\u0439 \u043a\u043e\u043b\u043b\u0431\u044d\u043a \u0431\u044b\u043b \u0432\u044b\u0437\u0432\u0430\u043d, \u0435\u0441\u043b\u0438 \u0431\u044b\u043b\u0430 \u043d\u0430\u0436\u0430\u0442\u0430 \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u0430\u044f \u043a\u043b\u0430\u0432\u0438\u0448\u0430 (\u0434\u043b\u044f \u0438\u043c\u0438\u0442\u0430\u0446\u0438\u0438 \u0441\u043e\u0431\u044b\u0442\u0438\u044f \u043d\u0430\u0436\u0430\u0442\u0438\u044f \u043a\u043b\u0430\u0432\u0438\u0448\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f \u043c\u0435\u0442\u043e\u0434 <code>fireEvent<\/code>);<\/p>\n<\/li>\n<li>\n<p>\u043f\u0435\u0440\u0435\u0434\u0430\u043d\u043d\u044b\u0439 \u043a\u043e\u043b\u043b\u0431\u044d\u043a \u043d\u0435 \u0431\u044b\u043b \u0432\u044b\u0437\u0432\u0430\u043d, \u0435\u0441\u043b\u0438 \u0431\u044b\u043b\u0430 \u043d\u0430\u0436\u0430\u0442\u0430 \u043b\u044e\u0431\u0430\u044f \u0434\u0440\u0443\u0433\u0430\u044f \u043a\u043b\u0430\u0432\u0438\u0448\u0430.<\/p>\n<\/li>\n<\/ul>\n<h2>\u041f\u043e\u043f\u0434\u043f\u0438\u0441\u043a\u0430 \u043d\u0430 \u043a\u043b\u0438\u043a \u0432\u043d\u0435 \u044d\u043b\u0435\u043c\u0435\u043d\u0442\u0430<\/h2>\n<p>\u0425\u043e\u0440\u043e\u0448\u043e, \u0443 \u043d\u0430\u0441 \u0435\u0441\u0442\u044c \u043c\u043e\u0434\u0430\u043b\u044c\u043d\u043e\u0435 \u043e\u043a\u043d\u043e, \u043a\u043e\u0442\u043e\u0440\u043e\u0435 \u043c\u043e\u0436\u043d\u043e \u0437\u0430\u043a\u0440\u044b\u0442\u044c \u043d\u0430\u0436\u0430\u0442\u0438\u0435\u043c \u043a\u043b\u0430\u0432\u0438\u0448\u0438 Escape. \u041d\u043e \u0447\u0442\u043e, \u0435\u0441\u043b\u0438 \u043c\u044b \u0445\u043e\u0442\u0438\u043c \u0437\u0430\u043a\u0440\u044b\u0432\u0430\u0442\u044c \u0435\u0433\u043e \u043f\u043e\u0441\u0440\u0435\u0434\u0441\u0442\u0432\u043e\u043c \u043a\u043b\u0438\u043a\u0430 \u0432\u043d\u0435 \u0434\u0430\u043d\u043d\u043e\u0433\u043e \u043e\u043a\u043d\u0430?<\/p>\n<p>\u0412\u043e\u0442 \u043a\u0430\u043a \u044d\u0442\u043e \u0432\u044b\u0433\u043b\u044f\u0434\u0438\u0442:<\/p>\n<figure class=\"full-width\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/8a9\/fdb\/19d\/8a9fdb19dc577361d52553eef365019e.gif\" width=\"960\" height=\"530\" data-src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/8a9\/fdb\/19d\/8a9fdb19dc577361d52553eef365019e.gif\"\/><figcaption><\/figcaption><\/figure>\n<p>\u0420\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044f \u0445\u0443\u043a\u0430:<\/p>\n<pre><code class=\"javascript\">import { RefObject, useEffect, useRef } from 'react';  const useOutsideClick = (   ref: RefObject&lt;HTMLElement | null>,   callback: (event: Event) => void, ) => {   const callbackRef = useRef(callback);    useEffect(() => {     callbackRef.current = callback;   }, [callback]);    useEffect(() => {     const handler: EventListener = (event) => {       const { current: target } = ref;        if (target &amp;&amp; !target.contains(event.target as HTMLElement)) {         callbackRef.current(event);       }     }      document.addEventListener('click', handler);     return () => document.removeEventListener('click', handler);   }, [ref]); }  export default useOutsideClick;<\/code><\/pre>\n<p>\u0420\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044f \u0434\u0430\u043d\u043d\u043e\u0433\u043e \u0445\u0443\u043a\u0430 \u043e\u0447\u0435\u043d\u044c \u043f\u043e\u0445\u043e\u0436\u0430 \u043d\u0430 \u043f\u0440\u0435\u0434\u044b\u0434\u0443\u0449\u0438\u0439 \u0445\u0443\u043a. \u0417\u0434\u0435\u0441\u044c \u0441\u043d\u043e\u0432\u0430 \u0441\u043e\u0437\u0434\u0430\u0435\u0442\u0441\u044f \u0444\u0443\u043d\u043a\u0446\u0438\u044f, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c\u0430\u044f \u0432 \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0435 \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u0430 \u0441\u043e\u0431\u044b\u0442\u0438\u044f. \u0414\u0430\u043d\u043d\u044b\u0439 \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a \u043f\u0440\u043e\u0432\u0435\u0440\u044f\u0435\u0442, \u0447\u0442\u043e \u044d\u043b\u0435\u043c\u0435\u043d\u0442 <code>event.target<\/code> \u043d\u0435 \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u043f\u043e\u0442\u043e\u043c\u043a\u043e\u043c \u044d\u043b\u0435\u043c\u0435\u043d\u0442\u0430 \u0438\u0437 \u0440\u0435\u0444\u0430, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u043f\u0435\u0440\u0435\u0434\u0430\u043d \u0432 \u0445\u0443\u043a (\u0442\u0430\u043a\u0436\u0435 \u043f\u0440\u043e\u0432\u0435\u0440\u044f\u0435\u0442\u0441\u044f, \u0447\u0442\u043e <code>event.target<\/code> \u0438 <code>ref.current<\/code> \u043d\u0435 \u044f\u0432\u043b\u044f\u044e\u0442\u0441\u044f \u043e\u0434\u043d\u0438\u043c \u0438 \u0442\u0435\u043c \u0436\u0435 \u044d\u043b\u0435\u043c\u0435\u043d\u0442\u043e\u043c), \u0438 \u0432\u044b\u0437\u044b\u0432\u0430\u0435\u0442 \u043f\u0435\u0440\u0435\u0434\u0430\u043d\u043d\u044b\u0439 \u0432 \u0445\u0443\u043a \u043a\u043e\u043b\u043b\u0431\u044d\u043a \u043f\u0440\u0438 \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e\u0441\u0442\u0438.<\/p>\n<p>\u041e\u0431\u0440\u0430\u0442\u0438\u0442\u0435 \u0432\u043d\u0438\u043c\u0430\u043d\u0438\u0435, \u0447\u0442\u043e \u043f\u0435\u0440\u0435\u0434\u0430\u043d\u043d\u044b\u0439 \u043a\u043e\u043b\u043b\u0431\u044d\u043a \u0441\u043d\u043e\u0432\u0430 \u0441\u043e\u0445\u0440\u0430\u043d\u044f\u0435\u0442\u0441\u044f \u0432 \u0440\u0435\u0444. \u0414\u0430\u043d\u043d\u0430\u044f \u0442\u0435\u0445\u043d\u0438\u043a\u0430 \u043f\u043e\u0437\u0432\u043e\u043b\u044f\u0435\u0442 \u043d\u0430\u043c \u0431\u044b\u0442\u044c \u0443\u0432\u0435\u0440\u0435\u043d\u043d\u044b\u043c\u0438 \u0432 \u0430\u043a\u0442\u0443\u0430\u043b\u044c\u043d\u043e\u0441\u0442\u0438 \u0432\u044b\u0437\u044b\u0432\u0430\u0435\u043c\u043e\u0433\u043e \u043a\u043e\u043b\u043b\u0431\u044d\u043a\u0430 \u0431\u0435\u0437 \u043f\u0435\u0440\u0435\u0441\u043e\u0437\u0434\u0430\u043d\u0438\u044f \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u0430 \u0441\u043e\u0431\u044b\u0442\u0438\u044f.<\/p>\n<p>\u0422\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u0445\u0443\u043a\u0430:<\/p>\n<pre><code class=\"javascript\">import { fireEvent } from '@testing-library\/react'; import { renderHook } from '@testing-library\/react-hooks'; import useOutsideClick from '.\/useOutsideClick';  describe('useOutsideClick', () => {   test('should handle outside click', () => {     const target = document.createElement('div');     document.body.appendChild(target);      const outside = document.createElement('div');     document.body.appendChild(outside);      const ref = {       current: target,     };     const callback = jest.fn();      const view = renderHook(() => useOutsideClick(ref, callback));      expect(callback).toHaveBeenCalledTimes(0);     fireEvent.click(outside);     expect(callback).toHaveBeenCalledTimes(1);      \/\/ \u0422\u0435\u0441\u0442\u0438\u0440\u0443\u0435\u043c, \u0447\u0442\u043e \"removeEventListener\" \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442 \u043a\u043e\u0440\u0440\u0435\u043a\u0442\u043d\u043e,     \/\/ \u043f\u0440\u043e\u0432\u0435\u0440\u044f\u044f, \u0447\u0442\u043e \u043a\u043e\u043b\u043b\u0431\u044d\u043a \u0432\u044b\u0437\u044b\u0432\u0430\u043b\u0441\u044f \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u0438\u043d \u0440\u0430\u0437 \u043f\u043e\u0441\u043b\u0435 \u0440\u0430\u0437\u043c\u043e\u043d\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0438.     jest.spyOn(document, 'removeEventListener');      view.unmount();     expect(document.removeEventListener).toHaveBeenCalledTimes(1);      fireEvent.click(outside);     expect(callback).toHaveBeenCalledTimes(1);   });    test('should do nothing after click on the target element', () => {     const target = document.createElement('div');     document.body.appendChild(target);      const ref = {       current: target,     };     const callback = jest.fn();      renderHook(() => useOutsideClick(ref, callback));      expect(callback).toHaveBeenCalledTimes(0);     fireEvent.click(target);     expect(callback).toHaveBeenCalledTimes(0);   }); });<\/code><\/pre>\n<p>\u0414\u043b\u044f \u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f \u044d\u0442\u043e\u0433\u043e \u0445\u0443\u043a\u0430 \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0441\u043e\u0437\u0434\u0430\u0442\u044c \u0434\u0432\u0430 HTML-\u044d\u043b\u0435\u043c\u0435\u043d\u0442\u0430, \u0441\u044b\u043c\u0438\u0442\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0441\u043e\u0431\u044b\u0442\u0438\u0435 \u043a\u043b\u0438\u043a\u0430 \u043d\u0430 \u043e\u0434\u043d\u043e\u043c \u0438\u0437 \u043d\u0438\u0445 (\u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u043d\u0430\u0445\u043e\u0434\u0438\u0442\u0441\u044f \u0432\u043d\u0435 \u0446\u0435\u043b\u0435\u0432\u043e\u0433\u043e \u044d\u043b\u0435\u043c\u0435\u043d\u0442\u0430) \u0438 \u043f\u0440\u043e\u0432\u0435\u0440\u0438\u0442\u044c, \u0447\u0442\u043e \u043f\u0435\u0440\u0435\u0434\u0430\u043d\u043d\u044b\u0439 \u043a\u043e\u043b\u043b\u0431\u044d\u043a \u0431\u044b\u043b \u0432\u044b\u0437\u0432\u0430\u043d. \u0422\u0430\u043a\u0436\u0435 \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u043f\u0440\u043e\u0432\u0435\u0440\u0438\u0442\u044c, \u0447\u0442\u043e \u043a\u043e\u043b\u043b\u0431\u044d\u043a \u043d\u0435 \u0431\u044b\u043b \u0432\u044b\u0437\u0432\u0430\u043d, \u0435\u0441\u043b\u0438 \u0441\u043e\u0431\u044b\u0442\u0438\u0435 \u043a\u043b\u0438\u043a\u0430 \u043f\u0440\u043e\u0438\u0437\u043e\u0448\u043b\u043e \u043f\u043e \u0446\u0435\u043b\u0435\u0432\u043e\u043c\u0443 \u044d\u043b\u0435\u043c\u0435\u043d\u0442\u0443.<\/p>\n<p>\u0414\u0430\u043d\u043d\u044b\u0439 \u0442\u0435\u0441\u0442 \u043f\u0440\u043e\u0432\u0435\u0440\u044f\u0435\u0442, \u0447\u0442\u043e:<\/p>\n<ul>\n<li>\n<p>\u043e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a \u0441\u043e\u0431\u044b\u0442\u0438\u044f \u0431\u044b\u043b \u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u043e\u0437\u0434\u0430\u043d \u043f\u0440\u0438 \u043c\u043e\u043d\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0438 \u0438 \u043a\u043e\u0440\u0440\u0435\u043a\u0442\u043d\u043e \u0443\u0434\u0430\u043b\u0435\u043d \u043f\u0440\u0438 \u0440\u0430\u0437\u043c\u043e\u043d\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0438;<\/p>\n<\/li>\n<li>\n<p>\u043f\u0435\u0440\u0435\u0434\u0430\u043d\u043d\u044b\u0439 \u043a\u043e\u043b\u043b\u0431\u044d\u043a \u0431\u044b\u043b \u0432\u044b\u0437\u0432\u0430\u043d, \u0435\u0441\u043b\u0438 \u0441\u043e\u0431\u044b\u0442\u0438\u0435 \u043a\u043b\u0438\u043a\u0430 \u043f\u0440\u043e\u0438\u0437\u043e\u0448\u043b\u043e \u043d\u0430 \u044d\u043b\u0435\u043c\u0435\u043d\u0442\u0435, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u043d\u0430\u0445\u043e\u0434\u0438\u0442\u0441\u044f \u0432\u043d\u0435 \u0446\u0435\u043b\u0435\u0432\u043e\u0433\u043e \u044d\u043b\u0435\u043c\u0435\u043d\u0442\u0430;<\/p>\n<\/li>\n<li>\n<p>\u043f\u0435\u0440\u0435\u0434\u0430\u043d\u043d\u044b\u0439 \u043a\u043e\u043b\u043b\u0431\u044d\u043a \u043d\u0435 \u0431\u044b\u043b \u0432\u044b\u0437\u0432\u0430\u043d, \u0435\u0441\u043b\u0438 \u0441\u043e\u0431\u044b\u0442\u0438\u0435 \u043a\u043b\u0438\u043a\u0430 \u043f\u0440\u043e\u0438\u0437\u043e\u0448\u043b\u043e \u043d\u0430 \u0446\u0435\u043b\u0435\u0432\u043e\u043c \u044d\u043b\u0435\u043c\u0435\u043d\u0442\u0435.<\/p>\n<\/li>\n<\/ul>\n<h2>\u041f\u043e\u0434\u043f\u0438\u0441\u043a\u0430 \u043d\u0430 \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0435 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044f \u043c\u0435\u0434\u0438\u0430-\u0437\u0430\u043f\u0440\u043e\u0441\u0430<\/h2>\n<p>\u0414\u0430\u0432\u0430\u0439\u0442\u0435 \u043d\u0435\u043c\u043d\u043e\u0433\u043e \u0443\u0441\u043b\u043e\u0436\u043d\u0438\u043c \u0437\u0430\u0434\u0430\u0447\u0443. \u0427\u0442\u043e, \u0435\u0441\u043b\u0438 \u043c\u044b \u0445\u043e\u0442\u0438\u043c \u0443\u0434\u0430\u043b\u044f\u0442\u044c \u043a\u0430\u043a\u043e\u0439-\u0442\u043e \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 \u0438\u0437 DOM-\u0434\u0435\u0440\u0435\u0432\u0430 \u043f\u043e \u0434\u043e\u0441\u0442\u0438\u0436\u0435\u043d\u0438\u044e \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u043d\u043e\u0439 \u0448\u0438\u0440\u0438\u043d\u044b \u0432\u044c\u044e\u043f\u043e\u0440\u0442\u0430? \u0412 \u0434\u0430\u043d\u043d\u043e\u043c \u0441\u043b\u0443\u0447\u0430\u0435 \u043c\u044b \u043c\u043e\u0436\u0435\u043c \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c <code>window.matchMedia<\/code> \u0434\u043b\u044f \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0438 \u043c\u0435\u0434\u0438\u0430-\u0437\u0430\u043f\u0440\u043e\u0441\u0430 \u0438 \u043f\u043e\u0434\u043f\u0438\u0441\u043a\u0438 \u043d\u0430 \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0435 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044f \u0432\u043e\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u043c\u043e\u0433\u043e \u043e\u0431\u044a\u0435\u043a\u0442\u0430 <code>MediaQueryList<\/code>.<\/p>\n<p>\u0412\u043e\u0442 \u043a\u0430\u043a \u044d\u0442\u043e \u0432\u044b\u0433\u043b\u044f\u0434\u0438\u0442:<\/p>\n<figure class=\"full-width\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/d4f\/676\/ccd\/d4f676ccd8b7ffa4727f9a064083ee70.gif\" width=\"960\" height=\"530\" data-src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/d4f\/676\/ccd\/d4f676ccd8b7ffa4727f9a064083ee70.gif\"\/><figcaption><\/figcaption><\/figure>\n<p>\u0420\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044f \u0445\u0443\u043a\u0430:<\/p>\n<pre><code class=\"javascript\">import { useEffect, useState } from 'react';  const useMediaQuery = (query: string) => {   const [matches, setMatches] = useState(false);    useEffect(() => {     let mounted = true;      const mediaQueryList = window.matchMedia(query);     setMatches(mediaQueryList.matches);      const handler = (event: MediaQueryListEvent) => {       if (!mounted) {         return;       }       setMatches(event.matches);     }      if (mediaQueryList.addListener) {       \/\/ \u041c\u0435\u0442\u043e\u0434\u044b `addListener` \u0438 `removeListener` \u043f\u043e\u043c\u0435\u0447\u0435\u043d\u044b \u043a\u0430\u043a \u0443\u0441\u0442\u0440\u0430\u0435\u0432\u0448\u0438\u0435 \u043d\u0430 MDN,       \/\/ \u043d\u043e \u0438\u0445 \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c, \u0442.\u043a. \u0432 Safari &lt; 14 \u043c\u0435\u0442\u043e\u0434\u044b       \/\/ `addEventListener` \u0438 `removeEventListener` \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u044e\u0442\u0441\u044f.       \/\/ https:\/\/caniuse.com\/mdn-api_mediaquerylist        mediaQueryList.addListener(handler);     } else {       mediaQueryList.addEventListener('change', handler);     }      return () => {       mounted = false;       if (mediaQueryList.removeListener) {         mediaQueryList.removeListener(handler);       } else {         mediaQueryList.removeEventListener('change', handler);       }     }   }, [query]);    return Boolean(matches); }  export default useMediaQuery;<\/code><\/pre>\n<p>\u0421\u043e\u0437\u0434\u0430\u043d\u043d\u044b\u0439 \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a \u0432\u043d\u0443\u0442\u0440\u0438 \u0434\u0430\u043d\u043d\u043e\u0433\u043e \u0445\u0443\u043a\u0430 \u0431\u0443\u0434\u0435\u0442 \u0432\u044b\u0437\u0432\u0430\u043d \u043f\u0440\u0438 \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0438 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044f \u043c\u0435\u0434\u0438\u0430-\u0437\u0430\u043f\u0440\u043e\u0441\u0430 (\u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440, \u043f\u0440\u0438 \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0438 \u0448\u0438\u0440\u0438\u043d\u044b \u0432\u044c\u044e\u043f\u043e\u0440\u0442\u0430, \u0435\u0441\u043b\u0438 \u043e\u0442\u0441\u043b\u0435\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f \u0435\u0433\u043e \u0448\u0438\u0440\u0438\u043d\u0430). \u041f\u0440\u0438 \u0441\u043c\u0435\u043d\u0435 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044f \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u0441\u0432\u043e\u0439\u0441\u0442\u0432\u0430 <code>matches<\/code> \u0438\u0437 \u043e\u0431\u044a\u0435\u043a\u0442\u0430 <code>MediaQueryListEvent<\/code> \u0431\u0443\u0434\u0435\u0442 \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u043e \u043a\u0430\u043a \u0442\u0435\u043a\u0443\u0449\u0435\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u0438 \u0432\u043e\u0437\u0432\u0440\u0430\u0449\u0435\u043d\u043e \u0445\u0443\u043a\u043e\u043c.<\/p>\n<p>\u0412\u0430\u0436\u043d\u043e \u043e\u0431\u043d\u043e\u0432\u043b\u044f\u0442\u044c \u0442\u0435\u043a\u0443\u0449\u0435\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u0432\u043d\u0443\u0442\u0440\u0438 \u0445\u0443\u043a\u0430 \u0442\u043e\u043b\u044c\u043a\u043e \u0435\u0441\u043b\u0438 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044e\u0449\u0438\u0439 \u0445\u0443\u043a, \u0441\u043c\u043e\u043d\u0442\u0438\u0440\u043e\u0432\u0430\u043d. \u0412 \u043f\u0440\u043e\u0442\u0438\u0432\u043d\u043e\u043c \u0441\u043b\u0443\u0447\u0430\u0435 React \u0441\u043e\u043e\u0431\u0449\u0438\u0442 \u043e\u0431 \u043e\u0448\u0438\u0431\u043a\u0435 \u043f\u0440\u0438 \u043f\u043e\u043f\u044b\u0442\u043a\u0435 \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044f \u0432\u043d\u0443\u0442\u0440\u0438 \u0440\u0430\u0437\u043c\u043e\u043d\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u043e\u0433\u043e \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430.<\/p>\n<figure class=\"full-width\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/f2e\/003\/17a\/f2e00317a62704e8ed604beca977cd2f.png\" width=\"1244\" height=\"332\" data-src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/f2e\/003\/17a\/f2e00317a62704e8ed604beca977cd2f.png\"\/><figcaption><\/figcaption><\/figure>\n<p>\u0422\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u0445\u0443\u043a\u0430:<\/p>\n<pre><code class=\"javascript\">import { act } from '@testing-library\/react'; import { renderHook } from '@testing-library\/react-hooks'; import useMediaQuery from '.\/useMediaQuery';  const mockImplementation = {   handlers: [] as Array&lt;(event: MediaQueryListEvent) => void>,    create(matches: boolean) {     return {       matches,       addEventListener: (event: string, handler: (event: MediaQueryListEvent) => void) => {         this.handlers.push(handler);       },       removeEventListener: jest.fn(),     }   },    dispatchEvent(event: MediaQueryListEvent) {     this.handlers.forEach((handler) => handler(event));   }, }  describe('useMediaQuery', () => {   afterAll(() => {     jest.clearAllMocks();   });    describe('with \"addEventListener\" and \"addRemoveListener\"', () => {     test('should return true if media query matches', () => {       window.matchMedia = jest.fn().mockImplementation(() => mockImplementation.create(true));        const view = renderHook(() => useMediaQuery('(min-width: 1024px)'));       expect(view.result.current).toEqual(true);     });      test('should return false if media query doesn`t match', () => {       window.matchMedia = jest.fn().mockImplementation(() => mockImplementation.create(false));        const view = renderHook(() => useMediaQuery('(min-width: 1024px)'));       expect(view.result.current).toEqual(false);     });      test('should handle change event', () => {       window.matchMedia = jest.fn().mockImplementation(() => mockImplementation.create(true));        const view = renderHook(() => useMediaQuery('(min-width: 1024px)'));       expect(view.result.current).toEqual(true);        act(() => {         mockImplementation.dispatchEvent({ matches: false } as MediaQueryListEvent);       });       expect(view.result.current).toEqual(false);     });   });    describe('with \"addListener\" and \"removeListener\"', () => {     test('should return true if media query matches', () => {       window.matchMedia = jest.fn().mockImplementation(() => ({         matches: true,         addListener: jest.fn(),         removeListener: jest.fn(),       }));        const view = renderHook(() => useMediaQuery('(min-width: 768px)'));       expect(view.result.current).toEqual(true);     });      test('should return false if media query doesn`t match', () => {       window.matchMedia = jest.fn().mockImplementation(() => ({         matches: false,         addListener: jest.fn(),         removeListener: jest.fn(),       }));        const view = renderHook(() => useMediaQuery('(min-width: 1024px)'));       expect(view.result.current).toEqual(false);     });   }); });<\/code><\/pre>\n<p>\u041f\u043e\u0441\u043a\u043e\u043b\u044c\u043a\u0443 Jest \u043d\u0435 \u0440\u0430\u0441\u043f\u043e\u0437\u043d\u0430\u0435\u0442 \u043c\u0435\u0442\u043e\u0434 <code>window.matchMedia<\/code> (\u0434\u0430\u0436\u0435 \u043f\u0440\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0438 JSDOM \u0432 \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0435 \u043e\u043a\u0440\u0443\u0436\u0435\u043d\u0438\u044f), \u043d\u0430\u043c \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0445\u0435\u043b\u043f\u0435\u0440, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0441\u043e\u0437\u0434\u0430\u0435\u0442 \u043c\u043e\u043a \u044d\u043a\u0437\u0435\u043c\u043f\u043b\u044f\u0440\u0430 <code>MediaQueryList<\/code> \u0441 \u043f\u0440\u0438\u043d\u0443\u0434\u0438\u0442\u0435\u043b\u044c\u043d\u043e \u0437\u0430\u0434\u0430\u043d\u043d\u044b\u043c \u0441\u0432\u043e\u0439\u0441\u0442\u0432\u043e\u043c <code>matches<\/code>.  \u0421\u043e\u0437\u0434\u0430\u043d\u043d\u044b\u0439 \u043c\u043e\u043a \u0441\u043e\u0434\u0435\u0440\u0436\u0438\u0442 \u043c\u0435\u0442\u043e\u0434 <code>addEventListener<\/code>, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0441\u043e\u0445\u0440\u0430\u043d\u044f\u0435\u0442 \u043a\u0430\u0436\u0434\u044b\u0439 \u043f\u0435\u0440\u0435\u0434\u0430\u043d\u043d\u044b\u0439 \u0432 \u043d\u0435\u0433\u043e \u043a\u043e\u043b\u043b\u0431\u044d\u043a, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u0432\u044b\u0437\u0432\u0430\u043d \u043f\u0440\u0438 \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0438 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044f \u043c\u0435\u0434\u0438\u0430-\u0437\u0430\u043f\u0440\u043e\u0441\u0430. \u0425\u0435\u043b\u043f\u0435\u0440 \u0434\u043b\u044f \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u044f \u043c\u043e\u043a\u0430 \u0442\u0430\u043a\u0436\u0435 \u0441\u043e\u0434\u0435\u0440\u0436\u0438\u0442 \u043c\u0435\u0442\u043e\u0434 <code>dispatchEvent<\/code>, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0438\u043d\u0438\u0446\u0438\u0438\u0440\u0443\u0435\u0442 \u0432\u044b\u0437\u043e\u0432 \u0432\u0441\u0435\u0445 \u0441\u043e\u0445\u0440\u0430\u043d\u0435\u043d\u043d\u044b\u0445 \u0440\u0430\u043d\u0435\u0435 \u043a\u043e\u043b\u043b\u0431\u044d\u043a\u043e\u0432, \u043f\u0435\u0440\u0435\u0434\u0430\u0432\u0430\u044f \u0438\u043c \u0432 \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0435 \u0430\u0440\u0433\u0443\u043c\u0435\u043d\u0442\u0430 \u043e\u0431\u044a\u0435\u043a\u0442, \u0440\u0435\u0430\u043b\u0438\u0437\u0443\u044e\u0449\u0438\u0439 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441 <code>MediaQueryListEvent<\/code>. <\/p>\n<p>\u0414\u0430\u043d\u043d\u044b\u0439 \u0442\u0435\u0441\u0442 \u043f\u0440\u043e\u0432\u0435\u0440\u044f\u0435\u0442, \u0447\u0442\u043e:<\/p>\n<ul>\n<li>\n<p>\u043e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a \u0441\u043e\u0431\u044b\u0442\u0438\u044f \u0431\u044b\u043b \u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u043e\u0437\u0434\u0430\u043d \u043f\u0440\u0438 \u043c\u043e\u043d\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0438;<\/p>\n<\/li>\n<li>\n<p>\u0432\u043e\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u043c\u043e\u0435 \u0445\u0443\u043a\u043e\u043c \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u043a\u043e\u0440\u0440\u0435\u043a\u0442\u043d\u043e \u043e\u0431\u043d\u043e\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u043f\u0440\u0438 \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0438 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044f \u043c\u0435\u0434\u0438\u0430-\u0437\u0430\u043f\u0440\u043e\u0441\u0430;<\/p>\n<\/li>\n<li>\n<p>\u0445\u0443\u043a \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442 \u043c\u0435\u0442\u043e\u0434\u044b <code>addListener<\/code> \u0438 <code>removeListener<\/code> (\u0432\u043c\u0435\u0441\u0442\u043e <code>addEventListener<\/code> \u0438 <code>removeEventListener<\/code>), \u0435\u0441\u043b\u0438 \u043e\u043d\u0438 \u0440\u0435\u0430\u043b\u0438\u0437\u043e\u0432\u0430\u043d\u044b \u0432 \u044d\u043a\u0437\u0435\u043c\u043f\u043b\u044f\u0440\u0435 <code>MediaQueryList<\/code>.<\/p>\n<\/li>\n<\/ul>\n<h2>\u0421\u0447\u0438\u0442\u0430\u0435\u043c \u043f\u043e\u043a\u0440\u044b\u0442\u0438\u0435<\/h2>\n<p>\u0412 \u043a\u043e\u043d\u0446\u0435 \u0441\u0442\u0430\u0442\u044c\u0438 \u043e\u0441\u0442\u0430\u043b\u043e\u0441\u044c \u0432\u044b\u0441\u0447\u0438\u0442\u0430\u0442\u044c \u043f\u043e\u043a\u0440\u044b\u0442\u0438\u0435 \u0442\u0435\u0441\u0442\u0430\u043c\u0438 \u0434\u043b\u044f \u0434\u0430\u043d\u043d\u044b\u0445 \u0445\u0443\u043a\u043e\u0432, \u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0432 Jest \u0441 \u043e\u043f\u0446\u0438\u0435\u0439 <code>--coverage<\/code>.  \u041a\u0430\u043a \u043f\u0440\u0430\u0432\u0438\u043b\u043e, \u0432\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u0441\u0434\u0435\u043b\u0430\u0442\u044c \u044d\u0442\u043e, \u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0432 NPM-\u0441\u043a\u0440\u0438\u043f\u0442 \u0441 \u0438\u043c\u0435\u043d\u0435\u043c <code>test<\/code> \u0432 \u0432\u0430\u0448\u0435\u043c <code>package.json<\/code>.<\/p>\n<pre><code class=\"bash\"># npm npm run test --coverage # yarn yarn test --coverage<\/code><\/pre>\n<p>\u0420\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442:<\/p>\n<figure class=\"full-width\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/193\/256\/252\/193256252d801854d6fb53a4b3aee7b0.png\" width=\"586\" height=\"272\" data-src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/193\/256\/252\/193256252d801854d6fb53a4b3aee7b0.png\"\/><figcaption><\/figcaption><\/figure>\n<p>\u0426\u0435\u043b\u044c \u0434\u043e\u0441\u0442\u0438\u0433\u043d\u0443\u0442\u0430 &#8212; \u043c\u044b \u043f\u043e\u043b\u0443\u0447\u0438\u043b\u0438 <strong>100% \u043f\u043e\u043a\u0440\u044b\u0442\u0438\u0435<\/strong> \u0442\u0435\u0441\u0442\u0430\u043c\u0438.<\/p>\n<h2>\u0417\u0430\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435<\/h2>\n<p>\u0412 \u0434\u0430\u043d\u043d\u043e\u0439 \u0441\u0442\u0430\u0442\u044c\u0435 \u044f \u043f\u043e\u0441\u0442\u0430\u0440\u0430\u043b\u0441\u044f \u043f\u0440\u043e\u0434\u0435\u043c\u043e\u043d\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u043a\u0430\u043a \u043f\u0438\u0441\u0430\u0442\u044c \u0441\u043e\u0431\u0441\u0442\u0432\u0435\u043d\u043d\u044b\u0435 React-\u0445\u0443\u043a\u0438 \u043d\u0430 TypeScript, \u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0438\u0445, \u0438 \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c 100% \u043f\u043e\u043a\u0440\u044b\u0442\u0438\u0435. \u041a\u0430\u043a \u0432\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u0432\u0438\u0434\u0435\u0442\u044c, \u044d\u0442\u043e \u043d\u0435 \u0442\u0430\u043a \u0443\u0436 \u0441\u043b\u043e\u0436\u043d\u043e, \u043a\u0430\u043a \u043a\u0430\u0436\u0435\u0442\u0441\u044f \u043d\u0430 \u043f\u0435\u0440\u0432\u044b\u0439 \u0432\u0437\u0433\u043b\u044f\u0434.<\/p>\n<p>\u0412 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0438\u0445 \u0441\u0442\u0430\u0442\u044c\u044f\u0445 \u0434\u0430\u043d\u043d\u043e\u0439 \u0441\u0435\u0440\u0438\u0438 \u0431\u0443\u0434\u0435\u0442 \u0440\u0430\u0441\u0441\u043c\u043e\u0442\u0440\u0435\u043d\u043e \u0435\u0449\u0435 \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u043d\u043e\u0432\u044b\u0445 \u0445\u0443\u043a\u043e\u0432 \u0441 \u0438\u0445 \u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435\u043c.<\/p>\n<p>\u041d\u0430\u043f\u0438\u0448\u0438\u0442\u0435 \u0432 \u043a\u043e\u043c\u043c\u0435\u043d\u0442\u0430\u0440\u0438\u044f\u0445, \u043a\u0430\u043a\u0438\u0435 \u0445\u0443\u043a\u0438 \u0432\u044b \u0445\u043e\u0442\u0435\u043b\u0438 \u0431\u044b \u0432\u0438\u0434\u0435\u0442\u044c \u0432 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0435\u0439 \u0441\u0442\u0430\u0442\u044c\u0435.<\/p>\n<p>\u0421\u043f\u0430\u0441\u0438\u0431\u043e \u0432\u0441\u0435\u043c \u0437\u0430 \u0432\u043d\u0438\u043c\u0430\u043d\u0438\u0435.<\/p>\n<\/div>\n<\/div>\n<\/div>\n<div class=\"v-portal\" style=\"display:none;\"><\/div>\n<\/div>\n<p> <!----> <!----><br \/> \u0441\u0441\u044b\u043b\u043a\u0430 \u043d\u0430 \u043e\u0440\u0438\u0433\u0438\u043d\u0430\u043b \u0441\u0442\u0430\u0442\u044c\u0438 <a href=\"https:\/\/habr.com\/ru\/post\/653431\/\"> https:\/\/habr.com\/ru\/post\/653431\/<\/a><\/p>\n","protected":false},"excerpt":{"rendered":"<div><\/div>\n<div id=\"post-content-body\">\n<div>\n<div class=\"article-formatted-body article-formatted-body_version-2\">\n<div xmlns=\"http:\/\/www.w3.org\/1999\/xhtml\">\n<figure class=\"full-width\"><figcaption><\/figcaption><\/figure>\n<p><em>\u0414\u0430\u043d\u043d\u0430\u044f \u0441\u0442\u0430\u0442\u044c\u044f \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u043f\u0435\u0440\u0435\u0432\u043e\u0434\u043e\u043c <\/em><a href=\"https:\/\/dev.to\/kirillshvets97\/writing-useful-custom-react-hooks-with-typescript-and-testing-them-with-react-testing-library-2me\" rel=\"noopener noreferrer nofollow\"><em>\u043c\u043e\u0435\u0439 \u0441\u043e\u0431\u0441\u0442\u0432\u0435\u043d\u043d\u043e\u0439 \u0441\u0442\u0430\u0442\u044c\u0438<\/em><\/a><em>, \u0440\u0430\u043d\u0435\u0435 \u043e\u043f\u0443\u0431\u043b\u0438\u043a\u043e\u0432\u0430\u043d\u043d\u043e\u0439 \u043d\u0430 dev.to.<\/em><\/p>\n<p>\u0414\u0443\u043c\u0430\u044e, React-\u0445\u0443\u043a\u0438 \u043d\u0435 \u043d\u0443\u0436\u0434\u0430\u044e\u0442\u0441\u044f \u0432 \u043f\u0440\u0435\u0434\u0441\u0442\u0430\u0432\u043b\u0435\u043d\u0438\u0438, \u043f\u043e\u044d\u0442\u043e\u043c\u0443 \u043c\u043e\u0436\u043d\u043e \u043f\u0440\u043e\u043f\u0443\u0441\u0442\u0438\u0442\u044c \u0438\u0445 \u043e\u043f\u0438\u0441\u0430\u043d\u0438\u0435 \u0438 \u043f\u0440\u0438\u0441\u0442\u0443\u043f\u0438\u0442\u044c \u0441\u0440\u0430\u0437\u0443 \u043a \u0434\u0435\u043b\u0443.<\/p>\n<p>\u0415\u0441\u043b\u0438 \u0432\u044b \u0445\u043e\u0442\u0438\u0442\u0435 \u043f\u043e\u0441\u043c\u043e\u0442\u0440\u0435\u0442\u044c \u0438\u0441\u0445\u043e\u0434\u043d\u044b\u0439 \u043a\u043e\u0434, \u0442\u043e \u043c\u043e\u0436\u0435\u0442\u0435 \u043f\u043e\u0441\u0435\u0442\u0438\u0442\u044c \u044d\u0442\u043e\u0442 <a href=\"https:\/\/github.com\/kirillshvets97\/custom-react-hooks-demo\" rel=\"noopener noreferrer nofollow\">\u0440\u0435\u043f\u043e\u0437\u0438\u0442\u043e\u0440\u0438\u0439<\/a> \u043d\u0430 GitHub.<\/p>\n<h2>\u041c\u0438\u043d\u0438\u043c\u0430\u043b\u044c\u043d\u044b\u0435 \u0442\u0440\u0435\u0431\u043e\u0432\u0430\u043d\u0438\u044f<\/h2>\n<p>\u0414\u0430\u043d\u043d\u044b\u0439 \u0442\u0443\u0442\u043e\u0440\u0438\u0430\u043b \u043f\u0440\u0435\u0434\u043f\u043e\u043b\u0430\u0433\u0430\u0435\u0442, \u0447\u0442\u043e \u0443 \u0432\u0430\u0441 \u0443\u0436\u0435 \u0435\u0441\u0442\u044c (\u0445\u043e\u0442\u044f \u0431\u044b \u043c\u0438\u043d\u0438\u043c\u0430\u043b\u044c\u043d\u044b\u0439) \u043e\u043f\u044b\u0442 \u0440\u0430\u0431\u043e\u0442\u044b \u0441 React-\u0445\u0443\u043a\u0430\u043c\u0438 (\u0442\u0430\u043a\u0438\u043c\u0438, \u043a\u0430\u043a useEffect, useState, useRef), TypeScript, Jest \u0438 React Testing Library.<\/p>\n<p>\u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u0438\u043d\u0441\u0442\u0440\u0443\u043c\u0435\u043d\u0442\u044b \u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f \u0441\u0430\u043c\u043e\u0441\u0442\u043e\u044f\u0442\u0435\u043b\u044c\u043d\u043e, \u043d\u043e \u0434\u043b\u044f \u0431\u044b\u0441\u0442\u0440\u043e\u0433\u043e \u0441\u0442\u0430\u0440\u0442\u0430 \u044f \u0440\u0435\u043a\u043e\u043c\u0435\u043d\u0434\u0443\u044e \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0443\u0442\u0438\u043b\u0438\u0442\u0443 <a href=\"https:\/\/create-react-app.dev\/docs\/getting-started\/\" rel=\"noopener noreferrer nofollow\">Create React App<\/a>, \u043a\u043e\u0442\u043e\u0440\u0430\u044f \u043f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u043b\u044f\u0435\u0442 \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f \u0441 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0435\u043c Jest \u0438 React Testing Library <a href=\"https:\/\/create-react-app.dev\/docs\/running-tests\/\" rel=\"noopener noreferrer nofollow\">&#171;\u0438\u0437 \u043a\u043e\u0440\u043e\u0431\u043a\u0438&#187;<\/a>.<\/p>\n<p>\u0415\u0441\u043b\u0438 \u0432\u044b \u0445\u043e\u0442\u0438\u0442\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c (\u0438\u043b\u0438 \u0443\u0436\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0435) Next.js, \u0442\u043e \u0432\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u0443\u0437\u043d\u0430\u0442\u044c \u043a\u0430\u043a \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c Jest \u0438 React Testing Library \u0434\u043b\u044f Next.js <a href=\"https:\/\/nextjs.org\/docs\/testing\" rel=\"noopener noreferrer nofollow\">\u0437\u0434\u0435\u0441\u044c<\/a>.<\/p>\n<h2>\u0427\u0442\u043e \u043c\u044b \u0431\u0443\u0434\u0435\u043c \u0434\u0435\u043b\u0430\u0442\u044c<\/h2>\n<p>\u041d\u0430\u0448\u0430 \u0433\u043b\u0430\u0432\u043d\u0430\u044f \u0446\u0435\u043b\u044c &#8212; \u043d\u0430\u043f\u0438\u0441\u0430\u0442\u044c \u0441\u043e\u0431\u0441\u0442\u0432\u0435\u043d\u043d\u044b\u0435 React-\u0445\u0443\u043a\u0438 \u043d\u0430 TypeScript \u0438 \u043f\u0440\u043e\u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0438\u0445, \u043f\u043e\u043b\u0443\u0447\u0438\u0432 <strong>100% \u043f\u043e\u043a\u0440\u044b\u0442\u0438\u0435 (coverage)<\/strong>.<\/p>\n<p>\u0414\u0430\u0432\u0430\u0439\u0442\u0435 \u043d\u0430\u0447\u043d\u0435\u043c!<\/p>\n<h2>\u041f\u0440\u0438\u0441\u0442\u0443\u043f\u0430\u0435\u043c \u043a \u0440\u0430\u0431\u043e\u0442\u0435<\/h2>\n<p>\u0414\u043b\u044f \u0434\u0435\u043c\u043e\u043d\u0441\u0442\u0440\u0430\u0446\u0438\u0438 \u044f \u0431\u0443\u0434\u0443 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435, \u0441\u043e\u0437\u0434\u0430\u043d\u043d\u043e\u0435 \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e Create React App.<\/p>\n<pre><code class=\"bash\"># npx npx create-react-app custom-react-hooks-demo --template typescript # yarn yarn create react-app custom-react-hooks-demo --template typescript<\/code><\/pre>\n<p>\u0414\u0430\u043b\u0435\u0435, \u043d\u0430\u043c \u043d\u0443\u0436\u043d\u043e \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c \u043f\u0430\u043a\u0435\u0442 <code>react-hooks-testing-library<\/code>. \u042d\u0442\u043e\u0442 \u043f\u0430\u043a\u0435\u0442 \u043f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u043b\u044f\u0435\u0442 \u043d\u0430\u0431\u043e\u0440 \u0443\u0442\u0438\u043b\u0438\u0442, \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0435\u043d\u043d\u043e \u0443\u043f\u0440\u043e\u0449\u0430\u044e\u0449\u0438\u0445 \u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 React-\u0445\u0443\u043a\u043e\u0432. \u0423\u0437\u043d\u0430\u0442\u044c \u0431\u043e\u043b\u044c\u0448\u0435 \u043e \u0434\u0430\u043d\u043d\u043e\u043c \u043f\u0430\u043a\u0435\u0442\u0435 \u0432\u044b \u043c\u043e\u0436\u0435\u0442\u0435 <a href=\"https:\/\/github.com\/testing-library\/react-hooks-testing-library\" rel=\"noopener noreferrer nofollow\">\u0437\u0434\u0435\u0441\u044c<\/a>.<\/p>\n<pre><code class=\"bash\"># npm npm i @testing-library\/react-hooks --save-dev # yarn yarn add @testing-library\/react-hooks -D<\/code><\/pre>\n<h2>\u041f\u043e\u0434\u043f\u0438\u0441\u043a\u0430 \u043d\u0430 \u043d\u0430\u0436\u0430\u0442\u0438\u0435 \u043a\u043b\u0430\u0432\u0438\u0448\u0438<\/h2>\n<p>\u0414\u0430\u0432\u0430\u0439\u0442\u0435 \u043f\u0440\u0435\u0434\u0441\u0442\u0430\u0432\u0438\u043c, \u0447\u0442\u043e \u043d\u0430\u043c \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0437\u0430\u043a\u0440\u044b\u0432\u0430\u0442\u044c \u043c\u043e\u0434\u0430\u043b\u044c\u043d\u043e\u0435 \u043e\u043a\u043d\u043e \u043f\u043e \u043d\u0430\u0436\u0430\u0442\u0438\u044e \u043a\u043b\u0430\u0432\u0438\u0448\u0438 Escape.<\/p>\n<p>\u0412\u043e\u0442 \u043a\u0430\u043a \u044d\u0442\u043e \u0432\u044b\u0433\u043b\u044f\u0434\u0438\u0442:<\/p>\n<figure class=\"full-width\"><figcaption><\/figcaption><\/figure>\n<p>\u0420\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044f \u0445\u0443\u043a\u0430:<\/p>\n<pre><code class=\"javascript\">import { useEffect, useRef } from 'react';  const useKeydown = (key: string, callback: (event: Event) => void) => {   const callbackRef = useRef(callback);    useEffect(() => {     callbackRef.current = callback;   }, [callback]);    useEffect(() => {     const handler: EventListener = (event) => {       if ((event as KeyboardEvent).key === key) {         callbackRef.current(event);       }     }      document.addEventListener('keydown', handler);     return () => document.removeEventListener('keydown', handler);   }, [key]); }  export default useKeydown;<\/code><\/pre>\n<p>\u0412\u043d\u0443\u0442\u0440\u0438 \u044d\u0442\u043e\u0433\u043e \u0445\u0443\u043a\u0430 \u0441\u043e\u0437\u0434\u0430\u0435\u0442\u0441\u044f \u0444\u0443\u043d\u043a\u0446\u0438\u044f, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c\u0430\u044f \u0432 \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0435 \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u0430 \u0441\u043e\u0431\u044b\u0442\u0438\u044f. \u0414\u0430\u043d\u043d\u044b\u0439 \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a \u0441\u0440\u0430\u0432\u043d\u0438\u0432\u0430\u0435\u0442 \u0441\u0432\u043e\u0439\u0441\u0442\u0432\u043e <code>key<\/code> \u0438\u0437 \u043e\u0431\u044a\u0435\u043a\u0442\u0430 \u0441\u043e\u0431\u044b\u0442\u0438\u044f \u0441 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u043e\u043c <code>key<\/code>, \u043f\u0435\u0440\u0435\u0434\u0430\u043d\u043d\u044b\u043c \u0432 \u0441\u0430\u043c \u0445\u0443\u043a, \u0438, \u0435\u0441\u043b\u0438 \u043e\u043d\u0438 \u0438\u043c\u0435\u044e\u0442 \u043e\u0434\u0438\u043d\u0430\u043a\u043e\u0432\u043e\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435, \u0432\u044b\u0437\u044b\u0432\u0430\u0435\u0442 \u043a\u043e\u043b\u043b\u0431\u044d\u043a, \u043f\u0435\u0440\u0435\u0434\u0430\u043d\u043d\u044b\u0439 \u0432 \u0445\u0443\u043a. <\/p>\n<p>\u0422\u0430\u043a\u0436\u0435, \u043c\u044b \u0441\u043e\u0445\u0440\u0430\u043d\u044f\u0435\u043c \u043f\u0435\u0440\u0435\u0434\u0430\u043d\u043d\u044b\u0439 \u043a\u043e\u043b\u043b\u0431\u044d\u043a \u0432 \u0440\u0435\u0444, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044f <code>useRef<\/code>, \u0438 \u043e\u0431\u043d\u043e\u0432\u043b\u044f\u0435\u043c \u0435\u0433\u043e \u043f\u043e \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e\u0441\u0442\u0438 \u0432\u043d\u0443\u0442\u0440\u0438 \u043f\u0435\u0440\u0432\u043e\u0433\u043e \u0432\u044b\u0437\u043e\u0432\u0430 <code>useEffect<\/code>, \u0447\u0442\u043e\u0431\u044b \u0433\u0430\u0440\u0430\u043d\u0442\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0435 \u0432\u0441\u0435\u0433\u0434\u0430 \u0430\u043a\u0442\u0443\u0430\u043b\u044c\u043d\u043e\u0433\u043e \u043a\u043e\u043b\u043b\u0431\u044d\u043a\u0430 \u0431\u0435\u0437 \u043f\u0435\u0440\u0435\u0441\u043e\u0437\u0434\u0430\u043d\u0438\u044f \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u0430 \u0441\u043e\u0431\u044b\u0442\u0438\u044f \u0432\u043d\u0443\u0442\u0440\u0438 \u0432\u0442\u043e\u0440\u043e\u0433\u043e \u0432\u044b\u0437\u043e\u0432\u0430 <code>useEffect<\/code>.<\/p>\n<p>\u0422\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u0445\u0443\u043a\u0430:<\/p>\n<pre><code class=\"javascript\">import { fireEvent } from '@testing-library\/react'; import { renderHook } from '@testing-library\/react-hooks'; import useKeydown from '.\/useKeydown';  describe('useKeydown', () => {   test('should handle keydown event', () => {     const callback = jest.fn();     const event = new KeyboardEvent('keydown', {       key: 'Escape',     });      const view = renderHook(() => useKeydown('Escape', callback));      expect(callback).toHaveBeenCalledTimes(0);     fireEvent(document, event);     expect(callback).toHaveBeenCalledTimes(1);      \/\/ \u0422\u0435\u0441\u0442\u0438\u0440\u0443\u0435\u043c, \u0447\u0442\u043e \"removeEventListener\" \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442 \u043a\u043e\u0440\u0440\u0435\u043a\u0442\u043d\u043e,     \/\/ \u043f\u0440\u043e\u0432\u0435\u0440\u044f\u044f, \u0447\u0442\u043e \u043a\u043e\u043b\u043b\u0431\u044d\u043a \u0432\u044b\u0437\u044b\u0432\u0430\u043b\u0441\u044f \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u0438\u043d \u0440\u0430\u0437 \u043f\u043e\u0441\u043b\u0435 \u0440\u0430\u0437\u043c\u043e\u043d\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f.     jest.spyOn(document, 'removeEventListener');      view.unmount();     expect(document.removeEventListener).toHaveBeenCalledTimes(1);      fireEvent(document, event);     expect(callback).toHaveBeenCalledTimes(1);   });    test('shouldn`t handle unnecessary keydown event', () => {     const callback = jest.fn();     const event = new KeyboardEvent('keydown', {       key: 'Enter',     });      renderHook(() => useKeydown('Escape', callback));      expect(callback).toHaveBeenCalledTimes(0);     fireEvent(document, event);     expect(callback).toHaveBeenCalledTimes(0);   }); });<\/code><\/pre>\n<p>\u041a\u0430\u043a \u0432\u044b \u0437\u043d\u0430\u0435\u0442\u0435, \u0445\u0443\u043a\u0438 \u043c\u043e\u0433\u0443\u0442 \u0431\u044b\u0442\u044c \u0432\u044b\u0437\u0432\u0430\u043d\u044b \u0442\u043e\u043b\u044c\u043a\u043e \u0432\u043d\u0443\u0442\u0440\u0438 \u0442\u0435\u043b\u0430 \u0444\u0443\u043d\u043a\u0446\u0438\u043e\u043d\u0430\u043b\u044c\u043d\u043e\u0433\u043e \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430. \u0414\u0430\u043d\u043d\u044b\u0439 \u0444\u0430\u043a\u0442 \u0437\u0430\u0441\u0442\u0430\u0432\u043b\u044f\u0435\u0442 \u0441\u043e\u0437\u0434\u0430\u0432\u0430\u0442\u044c \u0432\u043d\u0443\u0442\u0440\u0438 \u0442\u0435\u0441\u0442\u0430 \u043b\u0438\u0448\u043d\u0438\u0439 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u043d\u0443\u0436\u0435\u043d \u0442\u043e\u043b\u044c\u043a\u043e \u0440\u0430\u0434\u0438 \u0432\u044b\u0437\u043e\u0432\u0430 \u0442\u0435\u0441\u0442\u0438\u0440\u0443\u0435\u043c\u043e\u0433\u043e \u0445\u0443\u043a\u0430. \u0418\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0435 \u043c\u0435\u0442\u043e\u0434\u0430 <code>renderHook<\/code> \u0438\u0437 \u043f\u0430\u043a\u0435\u0442\u0430 <code>react-hooks-testing-library<\/code> \u043f\u043e\u0437\u0432\u043e\u043b\u044f\u0435\u0442 \u043d\u0430\u043c \u043f\u0440\u043e\u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0445\u0443\u043a \u0431\u0435\u0437 \u043d\u0430\u043f\u0438\u0441\u0430\u043d\u0438\u044f \u0434\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0433\u043e \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430, \u043e\u0431\u043e\u0440\u0430\u0447\u0438\u0432\u0430\u044f \u0442\u0435\u0441\u0442\u0438\u0440\u0443\u0435\u043c\u044b\u0439 \u0445\u0443\u043a \u0442\u0430\u043a, \u043a\u0430\u043a \u0435\u0441\u043b\u0438 \u0431\u044b \u043e\u043d \u0431\u044b\u043b \u0432\u044b\u0437\u0432\u0430\u043d \u0432\u043d\u0443\u0442\u0440\u0438 \u043d\u0430\u0441\u0442\u043e\u044f\u0449\u0435\u0433\u043e \u0444\u0443\u043d\u043a\u0446\u0438\u043e\u043d\u0430\u043b\u044c\u043d\u043e\u0433\u043e \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430.<\/p>\n<p>\u0414\u0430\u043d\u043d\u044b\u0439 \u0442\u0435\u0441\u0442 \u043f\u0440\u043e\u0432\u0435\u0440\u044f\u0435\u0442, \u0447\u0442\u043e:<\/p>\n<ul>\n<li>\n<p>\u043e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a \u0441\u043e\u0431\u044b\u0442\u0438\u044f \u0431\u044b\u043b \u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u043e\u0437\u0434\u0430\u043d \u043f\u0440\u0438 \u043c\u043e\u043d\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0438 \u0438 \u043a\u043e\u0440\u0440\u0435\u043a\u0442\u043d\u043e \u0443\u0434\u0430\u043b\u0435\u043d \u043f\u0440\u0438 \u0440\u0430\u0437\u043c\u043e\u043d\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0438;<\/p>\n<\/li>\n<li>\n<p>\u043f\u0435\u0440\u0435\u0434\u0430\u043d\u043d\u044b\u0439 \u043a\u043e\u043b\u043b\u0431\u044d\u043a \u0431\u044b\u043b \u0432\u044b\u0437\u0432\u0430\u043d, \u0435\u0441\u043b\u0438 \u0431\u044b\u043b\u0430 \u043d\u0430\u0436\u0430\u0442\u0430 \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u0430\u044f \u043a\u043b\u0430\u0432\u0438\u0448\u0430 (\u0434\u043b\u044f \u0438\u043c\u0438\u0442\u0430\u0446\u0438\u0438 \u0441\u043e\u0431\u044b\u0442\u0438\u044f \u043d\u0430\u0436\u0430\u0442\u0438\u044f \u043a\u043b\u0430\u0432\u0438\u0448\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f \u043c\u0435\u0442\u043e\u0434 <code>fireEvent<\/code>);<\/p>\n<\/li>\n<li>\n<p>\u043f\u0435\u0440\u0435\u0434\u0430\u043d\u043d\u044b\u0439 \u043a\u043e\u043b\u043b\u0431\u044d\u043a \u043d\u0435 \u0431\u044b\u043b \u0432\u044b\u0437\u0432\u0430\u043d, \u0435\u0441\u043b\u0438 \u0431\u044b\u043b\u0430 \u043d\u0430\u0436\u0430\u0442\u0430 \u043b\u044e\u0431\u0430\u044f \u0434\u0440\u0443\u0433\u0430\u044f \u043a\u043b\u0430\u0432\u0438\u0448\u0430.<\/p>\n<\/li>\n<\/ul>\n<h2>\u041f\u043e\u043f\u0434\u043f\u0438\u0441\u043a\u0430 \u043d\u0430 \u043a\u043b\u0438\u043a \u0432\u043d\u0435 \u044d\u043b\u0435\u043c\u0435\u043d\u0442\u0430<\/h2>\n<p>\u0425\u043e\u0440\u043e\u0448\u043e, \u0443 \u043d\u0430\u0441 \u0435\u0441\u0442\u044c \u043c\u043e\u0434\u0430\u043b\u044c\u043d\u043e\u0435 \u043e\u043a\u043d\u043e, \u043a\u043e\u0442\u043e\u0440\u043e\u0435 \u043c\u043e\u0436\u043d\u043e \u0437\u0430\u043a\u0440\u044b\u0442\u044c \u043d\u0430\u0436\u0430\u0442\u0438\u0435\u043c \u043a\u043b\u0430\u0432\u0438\u0448\u0438 Escape. \u041d\u043e \u0447\u0442\u043e, \u0435\u0441\u043b\u0438 \u043c\u044b \u0445\u043e\u0442\u0438\u043c \u0437\u0430\u043a\u0440\u044b\u0432\u0430\u0442\u044c \u0435\u0433\u043e \u043f\u043e\u0441\u0440\u0435\u0434\u0441\u0442\u0432\u043e\u043c \u043a\u043b\u0438\u043a\u0430 \u0432\u043d\u0435 \u0434\u0430\u043d\u043d\u043e\u0433\u043e \u043e\u043a\u043d\u0430?<\/p>\n<p>\u0412\u043e\u0442 \u043a\u0430\u043a \u044d\u0442\u043e \u0432\u044b\u0433\u043b\u044f\u0434\u0438\u0442:<\/p>\n<figure class=\"full-width\"><figcaption><\/figcaption><\/figure>\n<p>\u0420\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044f \u0445\u0443\u043a\u0430:<\/p>\n<pre><code class=\"javascript\">import { RefObject, useEffect, useRef } from 'react';  const useOutsideClick = (   ref: RefObject&lt;HTMLElement | null>,   callback: (event: Event) => void, ) => {   const callbackRef = useRef(callback);    useEffect(() => {     callbackRef.current = callback;   }, [callback]);    useEffect(() => {     const handler: EventListener = (event) => {       const { current: target } = ref;        if (target &amp;&amp; !target.contains(event.target as HTMLElement)) {         callbackRef.current(event);       }     }      document.addEventListener('click', handler);     return () => document.removeEventListener('click', handler);   }, [ref]); }  export default useOutsideClick;<\/code><\/pre>\n<p>\u0420\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044f \u0434\u0430\u043d\u043d\u043e\u0433\u043e \u0445\u0443\u043a\u0430 \u043e\u0447\u0435\u043d\u044c \u043f\u043e\u0445\u043e\u0436\u0430 \u043d\u0430 \u043f\u0440\u0435\u0434\u044b\u0434\u0443\u0449\u0438\u0439 \u0445\u0443\u043a. \u0417\u0434\u0435\u0441\u044c \u0441\u043d\u043e\u0432\u0430 \u0441\u043e\u0437\u0434\u0430\u0435\u0442\u0441\u044f \u0444\u0443\u043d\u043a\u0446\u0438\u044f, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c\u0430\u044f \u0432 \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0435 \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u0430 \u0441\u043e\u0431\u044b\u0442\u0438\u044f. \u0414\u0430\u043d\u043d\u044b\u0439 \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a \u043f\u0440\u043e\u0432\u0435\u0440\u044f\u0435\u0442, \u0447\u0442\u043e \u044d\u043b\u0435\u043c\u0435\u043d\u0442 <code>event.target<\/code> \u043d\u0435 \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u043f\u043e\u0442\u043e\u043c\u043a\u043e\u043c \u044d\u043b\u0435\u043c\u0435\u043d\u0442\u0430 \u0438\u0437 \u0440\u0435\u0444\u0430, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u043f\u0435\u0440\u0435\u0434\u0430\u043d \u0432 \u0445\u0443\u043a (\u0442\u0430\u043a\u0436\u0435 \u043f\u0440\u043e\u0432\u0435\u0440\u044f\u0435\u0442\u0441\u044f, \u0447\u0442\u043e <code>event.target<\/code> \u0438 <code>ref.current<\/code> \u043d\u0435 \u044f\u0432\u043b\u044f\u044e\u0442\u0441\u044f \u043e\u0434\u043d\u0438\u043c \u0438 \u0442\u0435\u043c \u0436\u0435 \u044d\u043b\u0435\u043c\u0435\u043d\u0442\u043e\u043c), \u0438 \u0432\u044b\u0437\u044b\u0432\u0430\u0435\u0442 \u043f\u0435\u0440\u0435\u0434\u0430\u043d\u043d\u044b\u0439 \u0432 \u0445\u0443\u043a \u043a\u043e\u043b\u043b\u0431\u044d\u043a \u043f\u0440\u0438 \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e\u0441\u0442\u0438.<\/p>\n<p>\u041e\u0431\u0440\u0430\u0442\u0438\u0442\u0435 \u0432\u043d\u0438\u043c\u0430\u043d\u0438\u0435, \u0447\u0442\u043e \u043f\u0435\u0440\u0435\u0434\u0430\u043d\u043d\u044b\u0439 \u043a\u043e\u043b\u043b\u0431\u044d\u043a \u0441\u043d\u043e\u0432\u0430 \u0441\u043e\u0445\u0440\u0430\u043d\u044f\u0435\u0442\u0441\u044f \u0432 \u0440\u0435\u0444. \u0414\u0430\u043d\u043d\u0430\u044f \u0442\u0435\u0445\u043d\u0438\u043a\u0430 \u043f\u043e\u0437\u0432\u043e\u043b\u044f\u0435\u0442 \u043d\u0430\u043c \u0431\u044b\u0442\u044c \u0443\u0432\u0435\u0440\u0435\u043d\u043d\u044b\u043c\u0438 \u0432 \u0430\u043a\u0442\u0443\u0430\u043b\u044c\u043d\u043e\u0441\u0442\u0438 \u0432\u044b\u0437\u044b\u0432\u0430\u0435\u043c\u043e\u0433\u043e \u043a\u043e\u043b\u043b\u0431\u044d\u043a\u0430 \u0431\u0435\u0437 \u043f\u0435\u0440\u0435\u0441\u043e\u0437\u0434\u0430\u043d\u0438\u044f \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u0430 \u0441\u043e\u0431\u044b\u0442\u0438\u044f.<\/p>\n<p>\u0422\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u0445\u0443\u043a\u0430:<\/p>\n<pre><code class=\"javascript\">import { fireEvent } from '@testing-library\/react'; import { renderHook } from '@testing-library\/react-hooks'; import useOutsideClick from '.\/useOutsideClick';  describe('useOutsideClick', () => {   test('should handle outside click', () => {     const target = document.createElement('div');     document.body.appendChild(target);      const outside = document.createElement('div');     document.body.appendChild(outside);      const ref = {       current: target,     };     const callback = jest.fn();      const view = renderHook(() => useOutsideClick(ref, callback));      expect(callback).toHaveBeenCalledTimes(0);     fireEvent.click(outside);     expect(callback).toHaveBeenCalledTimes(1);      \/\/ \u0422\u0435\u0441\u0442\u0438\u0440\u0443\u0435\u043c, \u0447\u0442\u043e \"removeEventListener\" \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442 \u043a\u043e\u0440\u0440\u0435\u043a\u0442\u043d\u043e,     \/\/ \u043f\u0440\u043e\u0432\u0435\u0440\u044f\u044f, \u0447\u0442\u043e \u043a\u043e\u043b\u043b\u0431\u044d\u043a \u0432\u044b\u0437\u044b\u0432\u0430\u043b\u0441\u044f \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u0438\u043d \u0440\u0430\u0437 \u043f\u043e\u0441\u043b\u0435 \u0440\u0430\u0437\u043c\u043e\u043d\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0438.     jest.spyOn(document, 'removeEventListener');      view.unmount();     expect(document.removeEventListener).toHaveBeenCalledTimes(1);      fireEvent.click(outside);     expect(callback).toHaveBeenCalledTimes(1);   });    test('should do nothing after click on the target element', () => {     const target = document.createElement('div');     document.body.appendChild(target);      const ref = {       current: target,     };     const callback = jest.fn();      renderHook(() => useOutsideClick(ref, callback));      expect(callback).toHaveBeenCalledTimes(0);     fireEvent.click(target);     expect(callback).toHaveBeenCalledTimes(0);   }); });<\/code><\/pre>\n<p>\u0414\u043b\u044f \u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f \u044d\u0442\u043e\u0433\u043e \u0445\u0443\u043a\u0430 \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0441\u043e\u0437\u0434\u0430\u0442\u044c \u0434\u0432\u0430 HTML-\u044d\u043b\u0435\u043c\u0435\u043d\u0442\u0430, \u0441\u044b\u043c\u0438\u0442\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0441\u043e\u0431\u044b\u0442\u0438\u0435 \u043a\u043b\u0438\u043a\u0430 \u043d\u0430 \u043e\u0434\u043d\u043e\u043c \u0438\u0437 \u043d\u0438\u0445 (\u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u043d\u0430\u0445\u043e\u0434\u0438\u0442\u0441\u044f \u0432\u043d\u0435 \u0446\u0435\u043b\u0435\u0432\u043e\u0433\u043e \u044d\u043b\u0435\u043c\u0435\u043d\u0442\u0430) \u0438 \u043f\u0440\u043e\u0432\u0435\u0440\u0438\u0442\u044c, \u0447\u0442\u043e \u043f\u0435\u0440\u0435\u0434\u0430\u043d\u043d\u044b\u0439 \u043a\u043e\u043b\u043b\u0431\u044d\u043a \u0431\u044b\u043b \u0432\u044b\u0437\u0432\u0430\u043d. \u0422\u0430\u043a\u0436\u0435 \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u043f\u0440\u043e\u0432\u0435\u0440\u0438\u0442\u044c, \u0447\u0442\u043e \u043a\u043e\u043b\u043b\u0431\u044d\u043a \u043d\u0435 \u0431\u044b\u043b \u0432\u044b\u0437\u0432\u0430\u043d, \u0435\u0441\u043b\u0438 \u0441\u043e\u0431\u044b\u0442\u0438\u0435 \u043a\u043b\u0438\u043a\u0430 \u043f\u0440\u043e\u0438\u0437\u043e\u0448\u043b\u043e \u043f\u043e \u0446\u0435\u043b\u0435\u0432\u043e\u043c\u0443 \u044d\u043b\u0435\u043c\u0435\u043d\u0442\u0443.<\/p>\n<p>\u0414\u0430\u043d\u043d\u044b\u0439 \u0442\u0435\u0441\u0442 \u043f\u0440\u043e\u0432\u0435\u0440\u044f\u0435\u0442, \u0447\u0442\u043e:<\/p>\n<ul>\n<li>\n<p>\u043e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a \u0441\u043e\u0431\u044b\u0442\u0438\u044f \u0431\u044b\u043b \u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u043e\u0437\u0434\u0430\u043d \u043f\u0440\u0438 \u043c\u043e\u043d\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0438 \u0438 \u043a\u043e\u0440\u0440\u0435\u043a\u0442\u043d\u043e \u0443\u0434\u0430\u043b\u0435\u043d \u043f\u0440\u0438 \u0440\u0430\u0437\u043c\u043e\u043d\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0438;<\/p>\n<\/li>\n<li>\n<p>\u043f\u0435\u0440\u0435\u0434\u0430\u043d\u043d\u044b\u0439 \u043a\u043e\u043b\u043b\u0431\u044d\u043a \u0431\u044b\u043b \u0432\u044b\u0437\u0432\u0430\u043d, \u0435\u0441\u043b\u0438 \u0441\u043e\u0431\u044b\u0442\u0438\u0435 \u043a\u043b\u0438\u043a\u0430 \u043f\u0440\u043e\u0438\u0437\u043e\u0448\u043b\u043e \u043d\u0430 \u044d\u043b\u0435\u043c\u0435\u043d\u0442\u0435, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u043d\u0430\u0445\u043e\u0434\u0438\u0442\u0441\u044f \u0432\u043d\u0435 \u0446\u0435\u043b\u0435\u0432\u043e\u0433\u043e \u044d\u043b\u0435\u043c\u0435\u043d\u0442\u0430;<\/p>\n<\/li>\n<li>\n<p>\u043f\u0435\u0440\u0435\u0434\u0430\u043d\u043d\u044b\u0439 \u043a\u043e\u043b\u043b\u0431\u044d\u043a \u043d\u0435 \u0431\u044b\u043b \u0432\u044b\u0437\u0432\u0430\u043d, \u0435\u0441\u043b\u0438 \u0441\u043e\u0431\u044b\u0442\u0438\u0435 \u043a\u043b\u0438\u043a\u0430 \u043f\u0440\u043e\u0438\u0437\u043e\u0448\u043b\u043e \u043d\u0430 \u0446\u0435\u043b\u0435\u0432\u043e\u043c \u044d\u043b\u0435\u043c\u0435\u043d\u0442\u0435.<\/p>\n<\/li>\n<\/ul>\n<h2>\u041f\u043e\u0434\u043f\u0438\u0441\u043a\u0430 \u043d\u0430 \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0435 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044f \u043c\u0435\u0434\u0438\u0430-\u0437\u0430\u043f\u0440\u043e\u0441\u0430<\/h2>\n<p>\u0414\u0430\u0432\u0430\u0439\u0442\u0435 \u043d\u0435\u043c\u043d\u043e\u0433\u043e \u0443\u0441\u043b\u043e\u0436\u043d\u0438\u043c \u0437\u0430\u0434\u0430\u0447\u0443. \u0427\u0442\u043e, \u0435\u0441\u043b\u0438 \u043c\u044b \u0445\u043e\u0442\u0438\u043c \u0443\u0434\u0430\u043b\u044f\u0442\u044c \u043a\u0430\u043a\u043e\u0439-\u0442\u043e \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 \u0438\u0437 DOM-\u0434\u0435\u0440\u0435\u0432\u0430 \u043f\u043e \u0434\u043e\u0441\u0442\u0438\u0436\u0435\u043d\u0438\u044e \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u043d\u043e\u0439 \u0448\u0438\u0440\u0438\u043d\u044b \u0432\u044c\u044e\u043f\u043e\u0440\u0442\u0430? \u0412 \u0434\u0430\u043d\u043d\u043e\u043c \u0441\u043b\u0443\u0447\u0430\u0435 \u043c\u044b \u043c\u043e\u0436\u0435\u043c \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c <code>window.matchMedia<\/code> \u0434\u043b\u044f \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0438 \u043c\u0435\u0434\u0438\u0430-\u0437\u0430\u043f\u0440\u043e\u0441\u0430 \u0438 \u043f\u043e\u0434\u043f\u0438\u0441\u043a\u0438 \u043d\u0430 \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0435 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044f \u0432\u043e\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u043c\u043e\u0433\u043e \u043e\u0431\u044a\u0435\u043a\u0442\u0430 <code>MediaQueryList<\/code>.<\/p>\n<p>\u0412\u043e\u0442 \u043a\u0430\u043a \u044d\u0442\u043e \u0432\u044b\u0433\u043b\u044f\u0434\u0438\u0442:<\/p>\n<figure class=\"full-width\"><figcaption><\/figcaption><\/figure>\n<p>\u0420\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044f \u0445\u0443\u043a\u0430:<\/p>\n<pre><code class=\"javascript\">import { useEffect, useState } from 'react';  const useMediaQuery = (query: string) => {   const [matches, setMatches] = useState(false);    useEffect(() => {     let mounted = true;      const mediaQueryList = window.matchMedia(query);     setMatches(mediaQueryList.matches);      const handler = (event: MediaQueryListEvent) => {       if (!mounted) {         return;       }       setMatches(event.matches);     }      if (mediaQueryList.addListener) {       \/\/ \u041c\u0435\u0442\u043e\u0434\u044b `addListener` \u0438 `removeListener` \u043f\u043e\u043c\u0435\u0447\u0435\u043d\u044b \u043a\u0430\u043a \u0443\u0441\u0442\u0440\u0430\u0435\u0432\u0448\u0438\u0435 \u043d\u0430 MDN,       \/\/ \u043d\u043e \u0438\u0445 \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c, \u0442.\u043a. \u0432 Safari &lt; 14 \u043c\u0435\u0442\u043e\u0434\u044b       \/\/ `addEventListener` \u0438 `removeEventListener` \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u044e\u0442\u0441\u044f.       \/\/ https:\/\/caniuse.com\/mdn-api_mediaquerylist        mediaQueryList.addListener(handler);     } else {       mediaQueryList.addEventListener('change', handler);     }      return () => {       mounted = false;       if (mediaQueryList.removeListener) {         mediaQueryList.removeListener(handler);       } else {         mediaQueryList.removeEventListener('change', handler);       }     }   }, [query]);    return Boolean(matches); }  export default useMediaQuery;<\/code><\/pre>\n<p>\u0421\u043e\u0437\u0434\u0430\u043d\u043d\u044b\u0439 <\/p>\n<\/div>\n<\/div>\n<\/div>\n<\/div>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[],"tags":[],"class_list":["post-330125","post","type-post","status-publish","format-standard","hentry"],"_links":{"self":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts\/330125","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=330125"}],"version-history":[{"count":0,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts\/330125\/revisions"}],"wp:attachment":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=330125"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=330125"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=330125"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}