First, you'll need to make this useStateCallback accept a generic parameter that represents your state. You're going to use that parameter a lot. We'll call that S for state.
function useStateCallback<S>(initialState: S) { ... }
Next up is the reducer. It looks like you want just a single action that accepts a Partial of S that gets merged into the state. So for the two generic parameters in Reducer we use S for the state and Partial<S> for the action.
const [state, setState] = useReducer<Reducer<S, Partial<S>>>(
(state, newState) => ({ ...state, ...newState }),
// state is implicitly typed as: S
// newState is implicitly typed as: Partial<S>
initialState
)
Or you could type the arguments of the reducer function, and those types would be inferred, which looks a bit cleaner, IMHO.
const [state, setState] = useReducer(
(state: S, newState: Partial<S>) => ({ ...state, ...newState }),
initialState
)
For creating the ref, we need to give it a type of the callback function, unioned with null since it may not always contain a value:
const cbRef = useRef<((state: S) => void) | null>(null)
for setStateCallback, we need to accept a Partial<S> to merge with the full state, and a callback that has the full state as it's only argument:
function setStateCallback(state: Partial<S>, cb: (state: S) => void) {
cbRef.current = cb
setState(state)
}
Your effect should be good as is.
Last thing to do would be to change your return to:
return [state, setStateCallback] as const
This is required because typescript sees this as an array by default, but you want it to be a tuple. Instead of an array of (S | Callback)[] you want it be a tuple with exactly two elements of type [S, Callback]. Appending as const to the array tells typescript treat the array as a constant and lock those types into the proper positions.
Putting all that together, you get:
import React, { useReducer, useRef, useEffect, Reducer } from 'react'
function useStateCallback<S>(initialState: S) {
const [state, setState] = useReducer<Reducer<S, Partial<S>>>(
(state, newState) => ({ ...state, ...newState }),
initialState
)
const cbRef = useRef<((state: S) => void) | null>(null)
function setStateCallback(state: Partial<S>, cb: (state: S) => void) {
cbRef.current = cb
setState(state)
}
useEffect(() => {
if (cbRef.current) {
cbRef.current(state)
cbRef.current = null
}
}, [state])
return [state, setStateCallback] as const
}
// Type safe usage
function Component() {
const [state, setStateCallback] = useStateCallback({ foo: 'bar' })
console.log(state.foo)
setStateCallback({ foo: 'baz' }, newState => {
console.log(newState.foo)
})
return <div>{state.foo}</div>
}
Playground
useState? And what do you use the callback for? (React already re-renders if state changes, so you typically dont need to trigger anything manually on state change)useEffect(someFunction, [somethingFromState])would runsomeFunctionautomatically anytime thatsomethingFromStatehas finished saving a change.