Skip to content

Commit 714f680

Browse files
authored
Config isEnabled and useIsomorphicEffect Update (#14)
* Update * Revert
1 parent c13ca2e commit 714f680

File tree

8 files changed

+16798
-8052
lines changed

8 files changed

+16798
-8052
lines changed

.eslintrc.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
{
2-
"extends": "./node_modules/@buildinams/lint/react-typescript"
2+
"extends": "./node_modules/@buildinams/lint/eslint/react-typescript"
33
}

README.md

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,22 +46,37 @@ const MyComponent = () => {
4646

4747
## Using 'defaultValue'
4848

49-
If you want to provide a default value for the initial render (and in server), you can pass a second argument to the hook.
49+
If you want to provide a default value for the initial render (and in server), you can pass it as `defaultValue` within the _optional_ config object.
5050

5151
```tsx
5252
import useMatchMedia from "@buildinams/use-match-media";
5353

5454
const MyComponent = () => {
55-
const isMobile = useMatchMedia("(max-width: 768px)", true);
55+
const isMobile = useMatchMedia("(max-width: 768px)", { defaultValue: true });
5656
...
5757
};
5858
```
5959

6060
Couple things to **note**:
6161

62-
- The default value will only be used on the initial render. By the second render, the hook will use the actual value matched.
62+
- The default value will only be used on the initial render and SSR. By the second render, the hook will use the actual value matched.
6363
- If left `undefined`, the default value will be `false`.
6464

65+
## Conditionally Listening to Events
66+
67+
You can conditionally listen to events by passing a `isEnabled` prop in the config object. This accepts a `boolean` value, and will only listen to events if the value is `true` (default). For example:
68+
69+
```tsx
70+
import useMatchMedia from "@buildinams/use-match-media";
71+
72+
const MyComponent = () => {
73+
const [isEnabled, setIsEnabled] = useState(false);
74+
75+
const isMobile = useMatchMedia("(max-width: 768px)", { isEnabled });
76+
...
77+
};
78+
```
79+
6580
## Requirements
6681

6782
This library requires a minimum React version of `17.0.0`.

package-lock.json

Lines changed: 16711 additions & 8017 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@buildinams/use-match-media",
33
"description": "Stateful hook that uses the matchMedia API.",
4-
"version": "0.0.6",
4+
"version": "0.1.0",
55
"license": "MIT",
66
"author": "Build in Amsterdam <development@buildinamsterdam.com> (https://www.buildinamsterdam.com/)",
77
"main": "dist/index.js",
@@ -41,22 +41,22 @@
4141
"react": ">=17.0.0 || 18"
4242
},
4343
"devDependencies": {
44-
"@babel/preset-env": "^7.20.2",
45-
"@babel/preset-typescript": "^7.18.6",
46-
"@buildinams/lint": "^0.0.3",
47-
"@testing-library/react": "^13.4.0",
48-
"@types/jest": "^29.2.5",
49-
"@types/node": "^18.11.18",
50-
"@types/react": "^18.0.26",
51-
"@types/react-dom": "^18.0.10",
44+
"@babel/preset-env": "^7.22.7",
45+
"@babel/preset-typescript": "^7.22.5",
46+
"@buildinams/lint": "^0.2.1",
47+
"@testing-library/react": "^14.0.0",
48+
"@types/jest": "^29.5.3",
49+
"@types/node": "^20.4.1",
50+
"@types/react": "^18.2.14",
51+
"@types/react-dom": "^18.2.6",
5252
"babel": "^6.23.0",
53-
"jest": "^29.3.1",
54-
"jest-environment-jsdom": "^29.3.1",
53+
"jest": "^29.6.1",
54+
"jest-environment-jsdom": "^29.6.1",
5555
"jest-matchmedia-mock": "^1.1.0",
5656
"npm-run-all": "^4.1.5",
5757
"react-dom": "^18.0.0",
58-
"ts-jest": "^29.0.3",
58+
"ts-jest": "^29.1.1",
5959
"ts-node": "^10.9.1",
60-
"typescript": "^4.9.4"
60+
"typescript": "^5.1.6"
6161
}
6262
}

src/index.ts

Lines changed: 24 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import { useEffect, useState } from "react";
1+
import { useState } from "react";
22

3-
import { EventHandler, Query } from "./types";
3+
import { Config, EventHandler, Query } from "./types";
4+
import { useIsomorphicEffect } from "./useIsomorphicEffect";
45

56
const queries = new Map<string, Query>();
67

@@ -76,8 +77,9 @@ const removeListener = (query: string, listener: EventHandler) => {
7677
*
7778
* @param query - The query to match for.
7879
*
79-
* @param defaultValue - Fallback value that is returned in SSR and first match
80-
* of any unique query. Defaults to `false`.
80+
* @param config - Optional configuration object.
81+
* @param config.defaultValue - Fallback value that is returned in SSR and first match of any unique query. Defaults to `false`.
82+
* @param config.isEnabled - Lets you specify whether to listen for events or not. Defaults to `true`.
8183
*
8284
* @example
8385
* useMatchMedia("(pointer: coarse)") -> Checks if the browser matches coarse;
@@ -89,20 +91,24 @@ const removeListener = (query: string, listener: EventHandler) => {
8991
* @returns matches - `true` / `false` if the device matches the query, or
9092
* `defaultValue` as fallback.
9193
*/
92-
const useMatchMedia = (query: string, defaultValue = false) => {
93-
const [matches, setMatches] = useState(getExistingMatch(query, defaultValue));
94-
95-
useEffect(() => {
96-
const listener = (event: MediaQueryListEvent) => {
97-
setMatches(event.matches);
98-
};
99-
100-
const matches = addListener(query, listener);
101-
setMatches(matches);
102-
103-
return () => {
104-
removeListener(query, listener);
105-
};
94+
const useMatchMedia = (query: string, config?: Config) => {
95+
const [matches, setMatches] = useState(
96+
getExistingMatch(query, config?.defaultValue)
97+
);
98+
99+
useIsomorphicEffect(() => {
100+
if (config?.isEnabled ?? true) {
101+
const listener = (event: MediaQueryListEvent) => {
102+
setMatches(event.matches);
103+
};
104+
105+
const matches = addListener(query, listener);
106+
setMatches(matches);
107+
108+
return () => {
109+
removeListener(query, listener);
110+
};
111+
}
106112
}, [query]);
107113

108114
return matches;

src/types.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,17 @@ export interface Query {
55
existingListeners: EventHandler[];
66
eventHandler: EventHandler;
77
}
8+
9+
export interface Config {
10+
/**
11+
* Fallback value that is returned in SSR and first match of any unique query.
12+
* Defaults to `false`.
13+
*/
14+
defaultValue?: boolean;
15+
16+
/**
17+
* This can be used to conditionally enable / disable the event listener.
18+
* Defaults to `true`.
19+
*/
20+
isEnabled?: boolean;
21+
}

src/useIsomorphicEffect.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { useEffect, useLayoutEffect } from "react";
2+
3+
const IS_BROWSER = typeof window !== "undefined";
4+
5+
/**
6+
* Small hook to run `useLayoutEffect` on the browser and `useEffect` on the
7+
* server. This is useful for SSR, where `useLayoutEffect` will throw an error.
8+
*/
9+
export const useIsomorphicEffect = IS_BROWSER ? useLayoutEffect : useEffect;

tests/index.test.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,4 +50,12 @@ describe("The hook", () => {
5050

5151
expect(matchMedia.getListeners(query).length).toEqual(0);
5252
});
53+
54+
it("doesn't add event listener if listen is set to 'false' on mount", () => {
55+
const query = "(pointer: coarse)";
56+
57+
renderHook(() => useMatchMedia(query, { isEnabled: false }));
58+
59+
expect(matchMedia.getListeners(query).length).toEqual(0);
60+
});
5361
});

0 commit comments

Comments
 (0)