@@ -15,6 +15,7 @@ import { useLayoutEffect } from '@fictjs/use-layout-effect'
1515
1616type ChangeHandler < T > = ( state : T ) => void
1717type SetStateFn < T > = ( value : T | ( ( prev : T ) => T ) ) => void
18+ type Dispatch < A > = ( action : A ) => void
1819
1920interface UseControllableStateParams < T > {
2021 prop ?: MaybeAccessor < T | undefined >
@@ -23,6 +24,10 @@ interface UseControllableStateParams<T> {
2324 caller ?: string
2425}
2526
27+ type AnyAction = {
28+ type : string
29+ }
30+
2631const noop = ( ) => { }
2732
2833function useControllableState < T > ( {
@@ -81,5 +86,84 @@ function useControllableState<T>({
8186 return [ value , setValue ]
8287}
8388
84- export { useControllableState }
85- export type { ChangeHandler , SetStateFn , UseControllableStateParams }
89+ function useControllableStateReducer < T , S extends object , A extends AnyAction > (
90+ reducer : ( prevState : S & { state : T } , action : A ) => S & { state : T } ,
91+ userArgs : UseControllableStateParams < T > ,
92+ initialState : Omit < S , 'state' > ,
93+ ) : [ ( ) => S & { state : T } , Dispatch < A > ]
94+ function useControllableStateReducer < T , S extends object , I extends object , A extends AnyAction > (
95+ reducer : ( prevState : S & { state : T } , action : A ) => S & { state : T } ,
96+ userArgs : UseControllableStateParams < T > ,
97+ initialArg : I ,
98+ init : ( value : I & { state : T } ) => Omit < S , 'state' > ,
99+ ) : [ ( ) => S & { state : T } , Dispatch < A > ]
100+ function useControllableStateReducer < T , S extends object , I extends object , A extends AnyAction > (
101+ reducer : ( prevState : S & { state : T } , action : A ) => S & { state : T } ,
102+ { prop, defaultProp, onChange = noop as ChangeHandler < T > , caller } : UseControllableStateParams < T > ,
103+ initialArg : Omit < S , 'state' > | I ,
104+ init ?: ( value : I & { state : T } ) => Omit < S , 'state' > ,
105+ ) : [ ( ) => S & { state : T } , Dispatch < A > ] {
106+ const defaultState = ( ) => readValue ( defaultProp )
107+ const controlledState = ( ) => ( prop === undefined ? undefined : readValue ( prop ) )
108+ const emitChange = useEffectEvent ( onChange )
109+ const createInitialState = ( ) : S & { state : T } => {
110+ const state = defaultState ( )
111+ const baseState = init
112+ ? init ( { ...( initialArg as I ) , state } )
113+ : ( initialArg as Omit < S , 'state' > )
114+
115+ return { ...baseState , state } as S & { state : T }
116+ }
117+ const internalState = createSignal < S & { state : T } > ( createInitialState ( ) )
118+
119+ let lastMode = controlledState ( ) !== undefined
120+ useLayoutEffect ( ( ) => {
121+ const nextMode = controlledState ( ) !== undefined
122+
123+ if ( process . env . NODE_ENV !== 'production' && lastMode !== nextMode ) {
124+ const from = lastMode ? 'controlled' : 'uncontrolled'
125+ const to = nextMode ? 'controlled' : 'uncontrolled'
126+ const label = caller ?? 'useControllableStateReducer'
127+ console . warn (
128+ label +
129+ ' is changing from ' +
130+ from +
131+ ' to ' +
132+ to +
133+ '. Components should stay either controlled or uncontrolled for their lifetime.' ,
134+ )
135+ }
136+
137+ lastMode = nextMode
138+ } )
139+
140+ const state = ( ) => {
141+ const currentInternalState = internalState ( )
142+ const currentProp = controlledState ( )
143+
144+ if ( currentProp === undefined ) {
145+ return currentInternalState
146+ }
147+
148+ return {
149+ ...currentInternalState ,
150+ state : currentProp ,
151+ }
152+ }
153+
154+ const dispatch : Dispatch < A > = ( action ) => {
155+ const prevState = state ( )
156+ const nextState = reducer ( prevState , action )
157+
158+ internalState ( nextState )
159+
160+ if ( ! Object . is ( prevState . state , nextState . state ) ) {
161+ emitChange ( nextState . state )
162+ }
163+ }
164+
165+ return [ state , dispatch ]
166+ }
167+
168+ export { useControllableState , useControllableStateReducer }
169+ export type { AnyAction , ChangeHandler , Dispatch , SetStateFn , UseControllableStateParams }
0 commit comments