Skip to content

Lambdaphile/use-rtk-slice

Repository files navigation

use-rtk-slice

A React hook for working with Redux Toolkit slices, with zero setup and boilerplate βš›οΈ πŸ› οΈ

npm Downloads per month Codecov coverage TypeScript Ready

npm i use-rtk-slice

Using Redux Toolkit slices with plain useSelector and useDispatch hooks often requires:

  1. Manually defining typed versions of useSelector and useDispatch in TypeScript projects: Define useAppSelector and useAppDispatch.
  2. Repeatedly writing const dispatch = useDispatch() just to dispatch an action.
  3. 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.

Contents

Usage

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>
  )
}

Testing (Mocking Slices)

To mock slices, use the mockSlice utility from use-rtk-slice/test/vitest or use-rtk-slice/test/jest:

Mock State

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)
  })
})

Mock Selectors

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()
  })
})

Mock and Spy on Actions

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.

About

React hook for working with Redux Toolkit slices, with zero setup and boilerplate βš›οΈ πŸ› οΈ

Topics

Resources

License

Stars

Watchers

Forks