Skip to content

Commit 25b91d9

Browse files
committed
[IMPL] useColorScheme hook
1 parent 57d5447 commit 25b91d9

12 files changed

Lines changed: 184 additions & 15 deletions

File tree

apps/react-tools-demo/src/App.css

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -575,4 +575,21 @@ button:not(:disabled) {
575575
.docs-md {
576576
width: 100%;
577577
}
578+
}
579+
580+
.container-themed {
581+
border: 1px solid;
582+
padding: 1em;
583+
width: fit-content;
584+
margin: 0 auto;
585+
}
586+
587+
.container-themed[data-color="dark"] {
588+
border-color: lightskyblue;
589+
color: plum
590+
}
591+
592+
.container-themed[data-color="light"] {
593+
border-color: purple;
594+
color: lightskyblue;
578595
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { useCallback } from "react";
2+
import { useColorScheme } from "../../../../../../packages/react-tools/src"
3+
4+
/**
5+
The component uses _useColorScheme_ with these properties:
6+
- _defaultValue_: __"mediaQuery"__, so it read color-scheme with media query.
7+
- _returnValue_: __true__, so it returns current value.
8+
- _getter_: a function that returns value from sessionStorage item with key "color-scheme".
9+
- _setter_: a function that save current value to sessionStorage item with key "color-scheme".
10+
The component renders a div with a class container that changes border and text colors by color-scheme selected and a button to manually change value.
11+
*/
12+
export const UseColorScheme = () => {
13+
const [value, update] = useColorScheme({
14+
defaultValue: "mediaQuery",
15+
returnValue: true,
16+
getter: useCallback(() => sessionStorage.getItem("color-scheme") as "dark" | "light" | null, []),
17+
setter: useCallback((val: "dark" | "light") => sessionStorage.setItem("color-scheme", val), [])
18+
});
19+
20+
return (
21+
<div className="container-themed" data-color={value}>
22+
<p>Current color-scheme is: {value}</p>
23+
<button onClick={() => update(value === "dark" ? "light" : "dark")}>Change color</button>
24+
</div>
25+
);
26+
}

apps/react-tools-demo/src/constants/components.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,8 @@ export const COMPONENTS = [
5757
"useTextSelection",
5858
"useDocumentVisibility",
5959
"useClipboard",
60-
"useMediaQuery"
60+
"useMediaQuery",
61+
"useColorScheme"
6162
]
6263
],
6364
//UTILS
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# useColorScheme
2+
Hook to handle ColorScheme.
3+
4+
## Usage
5+
6+
```tsx
7+
export const UseColorScheme = () => {
8+
const [value, update] = useColorScheme({
9+
defaultValue: "mediaQuery",
10+
returnValue: true,
11+
getter: useCallback(() => sessionStorage.getItem("color-scheme") as "dark" | "light" | null, []),
12+
setter: useCallback((val: "dark" | "light") => sessionStorage.setItem("color-scheme", val), [])
13+
});
14+
15+
return (
16+
<div className="container-themed" data-color={value}>
17+
<button onClick={()=>update(value === "dark" ? "light" : "dark")}>Change color</button>
18+
</div>
19+
);
20+
}
21+
```
22+
23+
> The component uses _useColorScheme_ with these properties:
24+
> - _defaultValue_: __"mediaQuery"__, so it read color-scheme with media query.
25+
> - _returnValue_: __true__, so it returns current value.
26+
> - _getter_: a function that returns value from sessionStorage item with key "color-scheme".
27+
> - _setter_: a function that save current value to sessionStorage item with key "color-scheme".
28+
> The component renders a div with a class container that changes border and text colors by color-scheme selected and a button to manually change value.
29+
30+
31+
## API
32+
33+
```tsx
34+
useColorScheme({ defaultValue, getter, setter, returnValue }: { defaultValue: "dark" | "light" | "mediaQuery", getter?: () => "dark" | "light" | null | undefined, setter?: (schema: "light"|"dark") => void, returnValue: boolean }): ["light" | "dark", (schema: "light" | "dark") => void] | ((schema: "light" | "dark") => void)
35+
```
36+
37+
> ### Params
38+
>
39+
> - __param__: _Object_
40+
> - __param.defaultValue__: _"dark"|"light"|"mediaQuery"_
41+
initial value if _getter_ function isn't present or isn't return a valid value. It can be _dark_ _light_ or _mediaQuery_ which means that must to be used media query prefers-color-scheme to detect initial value.
42+
> - __param.getter?__: _()=>"dark"|"light"|null|undefined_
43+
an optional function used to initialize current value. For example, it can be useful for reading the value from an attribute of an html file or from localStorage.
44+
> - __param.setter?__: _("dark"|"light")=>void_
45+
an optional function, which should work in conjunction with the _getter_ function, to run when the color scheme changes to save the value for future runs.
46+
> - __param.returnValue__: _boolean_
47+
if true returns only a function to manually change the color scheme value.
48+
>
49+
50+
> ### Returns
51+
>
52+
> __result__: if _returnValue_ is true, _result_ is the function to update color scheme value, otherwise is an array where first element is current value and second element is the function to update value.
53+
> - __Union of__:
54+
> - __Array__:
55+
> - _"dark"|"light"_
56+
> - _(schema:"dark"|"light") => void_
57+
> - _(schema:"dark"|"light") => void_
58+
>

apps/react-tools-demo/src/markdown/useMediaQuery.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ export const UseMediaQuery = () => {
2424
## API
2525

2626
```tsx
27-
useMediaQuery (mediaQuery: string, onChange?: (evt: MediaQueryListEvent) => void )
27+
useMediaQuery (mediaQuery: string, onChange?: (evt: MediaQueryListEvent) => void ): {matches: boolean, media: string}
2828
```
2929

3030
> ### Params

packages/react-tools/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@
6767
- [x] useDocumentVisibility
6868
- [x] useClipboard
6969
- [x] useMediaQuery
70-
- [ ] useColorScheme
70+
- [x] useColorScheme
7171
- [ ] useTitle (change document.title but also document.head.title nodeElement)
7272
- [ ] useFetch
7373
- [ ] useAsync

packages/react-tools/src/hooks/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,4 +38,5 @@ export { useInterval } from './useInterval';
3838
export { useTextSelection } from './useTextSelection';
3939
export { useDocumentVisibility } from './useDocumentVisibility';
4040
export { useClipboard } from './useClipboard';
41-
export { useMediaQuery } from './useMediaQuery';
41+
export { useMediaQuery } from './useMediaQuery';
42+
export { useColorScheme } from './useColorScheme';
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { useCallback, useMemo, useRef } from "react";
2+
import { useSyncExternalStore } from "."
3+
4+
/**
5+
* **`useColorScheme`**: Hook to handle ColorScheme.
6+
* @param {Object} param
7+
* @param {"dark"|"light"|"mediaQuery"} param.defaultValue - initial value if _getter_ function isn't present or isn't return a valid value. It can be _dark_ _light_ or _mediaQuery_ which means that must to be used media query prefers-color-scheme to detect initial value.
8+
* @param {()=>"dark"|"light"|null|undefined} [param.getter] - an optional function used to initialize current value. For example, it can be useful for reading the value from an attribute of an html file or from localStorage.
9+
* @param {("dark"|"light")=>void} [param.setter] - an optional function, which should work in conjunction with the _getter_ function, to run when the color scheme changes to save the value for future runs.
10+
* @param {boolean} param.returnValue - if true returns only a function to manually change the color scheme value.
11+
* @returns {["dark"|"light", (schema:"dark"|"light") => void] | (schema:"dark"|"light") => void} result - if _returnValue_ is true, _result_ is the function to update color scheme value, otherwise is an array where first element is current value and second element is the function to update value.
12+
*/
13+
function useColorScheme({ defaultValue, getter, setter, returnValue }: { defaultValue: "dark" | "light" | "mediaQuery", getter?: () => "dark" | "light" | null | undefined, setter?: (schema: "light" | "dark") => void, returnValue: true }): ["light" | "dark", (schema: "light" | "dark") => void]
14+
function useColorScheme({ defaultValue, getter, setter, returnValue }: { defaultValue: "dark" | "light" | "mediaQuery", getter?: () => "dark" | "light" | null | undefined, setter?: (schema: "light" | "dark") => void, returnValue: false }): ((schema: "light" | "dark") => void)
15+
function useColorScheme({ defaultValue, getter, setter, returnValue }: { defaultValue: "dark" | "light" | "mediaQuery", getter?: () => "dark" | "light" | null | undefined, setter?: (schema: "light"|"dark") => void, returnValue: boolean }): ["light" | "dark", (schema: "light" | "dark") => void] | ((schema: "light" | "dark") => void) {
16+
const currentValue = useRef<"dark" | "light">(useMemo(() => {
17+
let val = getter ? getter() : null;
18+
if (!val) {
19+
val = defaultValue === "mediaQuery" ? window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light" : defaultValue;
20+
setter && setter(val);
21+
}
22+
return val;
23+
},[]));
24+
const cacheValue = useRef(currentValue.current);
25+
const notifRef = useRef<(() => void) | null>();
26+
27+
const updateValue = useCallback((schema: "dark" | "light") => {
28+
currentValue.current = schema;
29+
setter && setter(currentValue.current);
30+
notifRef.current && notifRef.current();
31+
}, [setter])
32+
33+
const value = useSyncExternalStore(
34+
useCallback(notif => {
35+
notifRef.current = notif;
36+
const mq = window.matchMedia("(prefers-color-scheme: dark)")
37+
const listener = (evt: MediaQueryListEvent) => {
38+
updateValue(evt.matches ? "dark" : "light");
39+
};
40+
41+
defaultValue === "mediaQuery" && mq.addEventListener("change", listener)
42+
return () => {
43+
defaultValue === "mediaQuery" && mq.removeEventListener("change", listener)
44+
notifRef.current = null;
45+
}
46+
}, [defaultValue, updateValue]),
47+
useCallback(() => {
48+
if (currentValue.current !== cacheValue.current) {
49+
cacheValue.current = currentValue.current;
50+
}
51+
return cacheValue.current
52+
}, [])
53+
);
54+
55+
if (returnValue) {
56+
return [
57+
value,
58+
updateValue
59+
]
60+
}
61+
62+
return updateValue
63+
}
64+
65+
export {useColorScheme}

packages/react-tools/src/hooks/useLocalStorageState.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Dispatch, SetStateAction, useRef, useSyncExternalStore } from "react"
1+
import { Dispatch, SetStateAction, useMemo, useRef, useSyncExternalStore } from "react"
22
import { useEvents, useMemoizedFunction } from ".";
33

44

@@ -19,7 +19,7 @@ function useLocalStorageState<T>({ key, initialState, opts }: { key: string, ini
1919
const serializer = useRef(opts?.serializer || JSON.stringify);
2020
const deserializer = useRef(opts?.deserializer || JSON.parse);
2121
const mode = useRef(opts?.mode || "read/write");
22-
const cachedState = useRef((() => {
22+
const cachedState = useRef(useMemo(() => {
2323
if (mode.current === "write") {
2424
return null as T;
2525
}
@@ -35,7 +35,7 @@ function useLocalStorageState<T>({ key, initialState, opts }: { key: string, ini
3535
} else {
3636
return null as T;
3737
}
38-
})());
38+
}, []));
3939

4040
const subscribeRef = useRef((cb: ()=> void) => {
4141
const listener = (evt: Event) => {

packages/react-tools/src/hooks/useMediaQuery.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useCallback, useRef } from "react"
1+
import { useCallback, useMemo, useRef } from "react"
22
import { useSyncExternalStore } from "."
33

44
/**
@@ -8,13 +8,13 @@ import { useSyncExternalStore } from "."
88
* @returns {{matches: boolean, media: string}} result - object with __matches__, boolean value that returns true if the document currently matches the media query, __media__, string that represents media query.
99
*/
1010
export const useMediaQuery = (mediaQuery: string, onChange?: (evt: MediaQueryListEvent) => void ): {matches: boolean, media: string} => {
11-
const cachedMediaQuery = useRef((() => {
12-
const {media, matches} = window.matchMedia(mediaQuery);
11+
const cachedMediaQuery = useRef(useMemo(() => {
12+
const { media, matches } = window.matchMedia(mediaQuery);
1313
return {
1414
matches,
1515
media
1616
}
17-
})());
17+
}, []));
1818

1919
return useSyncExternalStore(
2020
useCallback(notif => {

0 commit comments

Comments
 (0)