1
1
import { useCallback , useEffect , useRef , useState } from 'react' ;
2
- import { z } from 'zod'
3
-
4
- let isSubscribed = false ;
2
+ import { z } from 'zod' ;
3
+ import { urlParamsToObject } from './utils' ;
5
4
6
5
type StateUpdateNotifier = ( searchString : string ) => void ;
6
+
7
+ let isSubscribed = false ;
7
8
const subscribers = new Map < StateUpdateNotifier , StateUpdateNotifier > ( ) ;
8
9
9
- let currentStateString = ''
10
+ let currentStateString = '' ;
10
11
11
12
function update ( ) {
12
- if ( document . location . search !== currentStateString ) {
13
- currentStateString = document . location . search ;
14
- subscribers . forEach ( ( subscriber ) => subscriber ( currentStateString ) ) ;
15
- }
13
+ if ( document . location . search !== currentStateString ) {
14
+ currentStateString = document . location . search ;
15
+ subscribers . forEach ( ( subscriber ) => subscriber ( currentStateString ) ) ;
16
+ }
16
17
}
17
18
18
19
function subscribeToHistory ( fn : StateUpdateNotifier ) {
@@ -22,57 +23,145 @@ function subscribeToHistory(fn: StateUpdateNotifier) {
22
23
return ;
23
24
}
24
25
25
- window . addEventListener ( " popstate" , update ) ;
26
+ window . addEventListener ( ' popstate' , update ) ;
26
27
}
27
28
28
29
function unsubscribeFromHistory ( fn : StateUpdateNotifier ) {
29
30
subscribers . delete ( fn ) ;
30
31
31
32
if ( subscribers . size === 0 ) {
32
- window . removeEventListener ( " popstate" , update ) ;
33
+ window . removeEventListener ( ' popstate' , update ) ;
33
34
isSubscribed = false ;
34
35
}
35
36
}
36
37
37
- const schema = z . object ( {
38
- name : z . string ( ) ,
39
- age : z . number ( ) ,
40
- } ) ;
38
+ // const schema = z.object({
39
+ // name: z.string(),
40
+ // age: z.number(),
41
+ // });
41
42
42
43
export type UrlStateOptions = {
43
44
preserveUnknown : boolean ;
44
- }
45
+ } ;
45
46
46
- export type UrlState < T extends z . ZodObject < any > > = {
47
- data : T [ '_type' ] ;
47
+ export type UrlState < T extends z . ZodObject < Record < string , z . ZodTypeAny > > > = {
48
+ data : z . infer < T > | null ;
48
49
isError : boolean ;
50
+ error : z . ZodError | null ;
51
+ } ;
52
+
53
+ // export type UrlStateValue =
54
+ // | string
55
+ // | { toString: () => string }
56
+ // | (string | { toString: () => string })[];
57
+
58
+ export type UrlStateMethods <
59
+ T extends z . ZodObject < Record < string , z . ZodTypeAny > > ,
60
+ > = {
61
+ reset : ( ) => void ;
62
+ replace : ( data : z . infer < T > ) => void ;
63
+ setValue : < K extends keyof z . infer < T > > ( key : K , value : z . infer < T > [ K ] ) => void ;
64
+ } ;
65
+
66
+ function usePush ( ) : ( href : string ) => void {
67
+ return useCallback ( ( href : string ) => {
68
+ window . history . pushState ( { } , '' , href ) ;
69
+ } , [ ] ) ;
49
70
}
50
71
51
- export function useUrlState < T extends z . ZodObject < any > > ( schema : T , options ?: UrlStateOptions ) : UrlState < T > {
72
+ export function useUrlState <
73
+ T extends z . ZodObject < Record < string , z . ZodTypeAny > > ,
74
+ > ( schema : T , options ?: UrlStateOptions ) : UrlState < T > & UrlStateMethods < T > {
52
75
options = {
53
76
preserveUnknown : false ,
54
77
...options ,
55
- }
78
+ } ;
56
79
57
80
const schemaRef = useRef ( schema ) ;
58
- const rawDataRef = useRef < Record < string , unknown > > ( { } ) ;
59
81
60
- const recalculateState = useCallback ( ( searchString : string ) => {
61
- const params = new URLSearchParams ( searchString ) ;
62
-
63
- } , [ ] )
82
+ const [ state , setState ] = useState < UrlState < T > > ( {
83
+ data : null ,
84
+ isError : false ,
85
+ error : null ,
86
+ } ) ;
87
+ const stateRef = useRef ( state ) ;
88
+ stateRef . current = state ;
89
+
90
+ const recalculateState = useCallback (
91
+ ( searchString : string ) => {
92
+ const params = new URLSearchParams ( searchString ) ;
93
+ const object = urlParamsToObject ( params ) ;
94
+
95
+ const validationResult = schemaRef . current . safeParse ( object ) ;
96
+
97
+ const result = validationResult . success
98
+ ? { success : true , data : validationResult . data , error : null }
99
+ : { success : false , data : null , error : validationResult . error } ;
100
+
101
+ if ( options ?. preserveUnknown ) {
102
+ result . data = { ...object , ...result . data } ;
103
+ }
104
+
105
+ setState ( {
106
+ data : result . data ,
107
+ isError : ! result . success ,
108
+ error : result . error ,
109
+ } ) ;
110
+ } ,
111
+ [ options ?. preserveUnknown ] ,
112
+ ) ;
64
113
65
114
useEffect ( ( ) => {
66
- subscribeToHistory ( recalculateState )
115
+ subscribeToHistory ( recalculateState ) ;
67
116
68
117
return ( ) => {
69
- unsubscribeFromHistory ( recalculateState )
70
- }
71
- } , [ ] ) ;
118
+ unsubscribeFromHistory ( recalculateState ) ;
119
+ } ;
120
+ } , [ recalculateState ] ) ;
121
+
122
+ const push = usePush ( ) ;
123
+
124
+ const reset = useCallback < UrlStateMethods < T > [ 'reset' ] > ( ( ) => {
125
+ const href = `?${ new URLSearchParams ( { } ) . toString ( ) } ` ;
126
+ push ( href ) ;
127
+ } , [ push ] ) ;
128
+
129
+ const replace = useCallback < UrlStateMethods < T > [ 'replace' ] > (
130
+ ( data ) => {
131
+ const href = `?${ new URLSearchParams ( data ) . toString ( ) } ` ;
132
+ push ( href ) ;
133
+ } ,
134
+ [ push ] ,
135
+ ) ;
136
+
137
+ const setValue = useCallback < UrlStateMethods < T > [ 'setValue' ] > (
138
+ ( key , value ) => {
139
+ const href = `?${ new URLSearchParams ( {
140
+ ...stateRef . current . data ,
141
+ [ key ] : value ,
142
+ } ) . toString ( ) } `;
143
+ push ( href ) ;
144
+ } ,
145
+ [ push ] ,
146
+ ) ;
147
+
148
+ return { ...state , replace, setValue, reset } ;
149
+ }
72
150
151
+ function useTest ( ) {
152
+ const rez = useUrlState (
153
+ z . object ( {
154
+ name : z . string ( ) ,
155
+ age : z . number ( ) ,
156
+ birthDate : z . date ( ) . optional ( ) ,
157
+ } ) ,
158
+ ) ;
73
159
74
- return {
75
- data : schema . parse ( { name : 'John' , age : 42 } ) ,
76
- isError : false ,
77
- }
160
+ rez . setValue ( 'age' , 10 ) ;
161
+ rez . setValue ( 'birthDate' , new Date ( ) ) ;
162
+
163
+ console . log ( rez . data ?. birthDate ) ;
164
+
165
+ rez . replace ( { name : 'test' , age : 10 , birthDate : new Date ( ) } ) ;
166
+ rez . replace ( { name : 'test' , age : 10 } ) ;
78
167
}
0 commit comments