A React hook for working with Redux Toolkit slices, with zero setup and boilerplate βοΈ π οΈ
npm i use-rtk-slice
Using Redux Toolkit slices with plain useSelector
and useDispatch
hooks often requires:
- Manually defining typed versions of
useSelector
anduseDispatch
in TypeScript projects: DefineuseAppSelector
anduseAppDispatch
. - Repeatedly writing
const dispatch = useDispatch()
just to dispatch an action. - Slice selectors are not bound - using them requires passing the relevant slice state, e.g.,
selector({ stateName: state })
.
The useSlice
hook from use-rtk-slice
handles all of this: it binds actions and selectors internally, and returns fully typed, ready-to-use slice state, actions, and selectors.
Define a RTK slice:
todosSlice.ts
import { createSlice, type PayloadAction } from '@reduxjs/toolkit'
interface Todo {
id: number
name: string
done: boolean
}
const initialState: Todo[] = [
{
id: Date.now(),
name: 'Be Awesome π¦',
done: false
}
]
const todosSlice = createSlice({
name: 'todos',
initialState: initialState,
reducers: {
addTodo(state, action: PayloadAction<Todo['name']>) {
state.push({ id: Date.now(), name: action.payload, done: false })
},
toggleTodo(state, action: PayloadAction<Todo['id']>) {
return state.map((todo) =>
todo.id === action.payload ? { ...todo, done: !todo.done } : todo
)
},
removeTodo(state, action: PayloadAction<Todo['id']>) {
return state.filter((todo) => todo.id !== action.payload)
}
},
selectors: {
selectCompleted: (state) => state.filter((todo) => todo.done)
}
})
Destructure the state
, and the bound actions
and selectors
from the slice as needed, using the useSlice
hook:
TodoList.tsx
import useSlice from 'use-rtk-slice'
import { todosSlice } from './todosSlice'
function TodoList() {
const [state, actions, selectors] = useSlice(todosSlice)
return (
<div>
<ul>
{state.map(({ id, name, done }) => (
<li key={id}>
<label>
<input
type="checkbox"
checked={done}
onChange={() => actions.toggleTodo(id)}
/>
{done ? <s>{name}</s> : name}
</label>
<button onClick={() => actions.removeTodo(id)}>ποΈ</button>
</li>
))}
</ul>
<p>Completed todo count: {selectors.selectCompleted().length}</p>
</div>
)
}
To mock slices, use the mockSlice
utility from use-rtk-slice/test/vitest
or use-rtk-slice/test/jest
:
import { mockSlice } from 'use-rtk-slice/test/vitest'
// or
import { mockSlice } from 'use-rtk-slice/test/jest'
import { App } from './App'
import { todoSlice } from './todoSlice'
describe('TodoList', () => {
beforeEach(() => {
mockSlice.beforeEach()
})
it('should render todos', () => {
mockSlice(todosSlice, {
state: [
{ id: 0, name: 'Be Awesome π¦', done: false },
{ id: 1, name: 'Spread Good Vibes π', done: false }
]
})
render(<App />)
const todos = screen.getAllByRole('listitem')
expect(todos).toHaveLength(2)
})
})
describe('TodoList', () => {
beforeEach(() => {
mockSlice.beforeEach()
})
it('should render completed todo count', () => {
mockSlice(todosSlice, {
selectCompleted: () => [{ id: 0, name: 'Be Awesome π¦', done: true }]
})
render(<App />)
expect(screen.getByText('Completed todo count: 1')).toBeInTheDocument()
})
})
describe('TodoList', () => {
beforeEach(() => {
mockSlice.beforeEach()
})
it('should toggle todos', () => {
const { toggleTodo } = mockSlice(todosSlice, {
state: [{ id: 0, name: 'Be Awesome π¦', done: false }]
})
render(<App />)
const todoToggle = screen.getByRole('checkbox')
fireEvent.click(todoToggle)
expect(toggleTodo).toHaveBeenCalled()
})
})
Note: Calling beforeEach(() => { mockSlice.beforeEach() })
is required to ensure test cases run in isolation.