Joppe
Joppe
TTCTheo's Typesafe Cult
Created by Joppe on 3/4/2025 in #questions
Feedback on Generated Redux hooks
Hey, I'm new to Redux/RTK and would like some feedback regarding two utility functions I wrote. I'm a fan of custom hooks and saw that RTK can auto generate those. I'd like to emulate something similar but for a regular slice's selectors and actions. I'd like to know: - Is what I'm doing unconventional? If it is, is it bad practice (and why) or just not common but fine. - Did I overlook some pitfalls? Like something that might cause unneeded/excessive re-renders? - Just general thoughts on the overall approach or tips regarding the typings. What I have right now can be used like this:
// todoSlice.ts
// ------------
const todoSlice = createSlice(/* stuff */)

// generated hooks match props in initialState but with the "use" prefix
export const { useTodos } = createSelectorHooks(todoSlice)
// hook that binds useDispatch to action creators when called in a component
export const useTodoActions = createActionHook(todoSlice)


// Component.tsx
// -------------
const Component = () => {
const todos = useTodos()
const { addTodo, toggleTodo } = useTodoActions()
}
// todoSlice.ts
// ------------
const todoSlice = createSlice(/* stuff */)

// generated hooks match props in initialState but with the "use" prefix
export const { useTodos } = createSelectorHooks(todoSlice)
// hook that binds useDispatch to action creators when called in a component
export const useTodoActions = createActionHook(todoSlice)


// Component.tsx
// -------------
const Component = () => {
const todos = useTodos()
const { addTodo, toggleTodo } = useTodoActions()
}
The utility functions look like this:
function createActionHook<
S extends Slice,
A extends S["actions"],
R extends {
[K in keyof A]: (
...args: Parameters<A[K]>
) => ReturnType<Dispatch<ReturnType<A[K]>>>;
}
>(slice: S) {
return () => {
const dispatch = useDispatch();
return useMemo(
() => bindActionCreators(slice.actions, dispatch),
[dispatch]
) as unknown as R;
};
}

function createSelectorHooks<
S extends Slice,
I extends ReturnType<S["getInitialState"]>,
R extends { [key in Slice["reducerPath"]]: I },
H extends { [K in keyof I as `use${Capitalize<string & K>}`]: () => I[K] }
>(slice: S) {
return Object.fromEntries(
Object.keys(slice.getInitialState()).map((key) => [
`use${key[0].toUpperCase()}${key.slice(1)}`,
() => useSelector((rootState: R) => rootState[slice.reducerPath][key]),
])
) as H;
}
function createActionHook<
S extends Slice,
A extends S["actions"],
R extends {
[K in keyof A]: (
...args: Parameters<A[K]>
) => ReturnType<Dispatch<ReturnType<A[K]>>>;
}
>(slice: S) {
return () => {
const dispatch = useDispatch();
return useMemo(
() => bindActionCreators(slice.actions, dispatch),
[dispatch]
) as unknown as R;
};
}

function createSelectorHooks<
S extends Slice,
I extends ReturnType<S["getInitialState"]>,
R extends { [key in Slice["reducerPath"]]: I },
H extends { [K in keyof I as `use${Capitalize<string & K>}`]: () => I[K] }
>(slice: S) {
return Object.fromEntries(
Object.keys(slice.getInitialState()).map((key) => [
`use${key[0].toUpperCase()}${key.slice(1)}`,
() => useSelector((rootState: R) => rootState[slice.reducerPath][key]),
])
) as H;
}
3 replies