Skip to content

feat(redux): implement dark/light theme toggle with redux toolkit - #6

Open
vidyashreebv wants to merge 24 commits into
refactor/apply-code-review-feedbackfrom
feature/redux-theme-management
Open

vidyashreebv wants to merge 24 commits into
refactor/apply-code-review-feedbackfrom
feature/redux-theme-management

Conversation

@vidyashreebv

Copy link
Copy Markdown
Collaborator

What does this PR do?

  • Implements Redux Toolkit for global state management
  • Adds dark/light theme toggle functionality using Redux actions
  • Creates theme slice with toggleTheme and setTheme reducers
  • Adds ThemeToggle atom component with sun/moon icons
  • Applies dark mode styling to all components using Tailwind v4 dark: variants
  • Adds PropTypes validation to all components
  • Updates gradient classes from bg-gradient-to-* to bg-linear-to-* for Tailwind v4
  • Adds form label associations with htmlFor attributes
  • Uses globalThis for window and localStorage access

What steps does your reviewer have to take to test this PR manually?

  1. Pull the feature/redux-theme-management branch and run npm install
  2. Run npm run dev to start the development server
  3. Click the theme toggle button (moon/sun icon) in top-right to switch between light and dark modes
  4. Verify all components update colors correctly in both themes (background, cards, text, inputs, buttons)
  5. Test counter functionality (increment, decrement, reset, settings) works in both themes
  6. Refresh the page and verify theme preference persists
  7. Test negative values and custom step values in both themes
  8. Run npm test to verify all 35 tests pass (27 existing + 8 new theme tests)

Screenshots

Screenshot 2026-01-09 at 12 40 52 PM Screenshot 2026-01-09 at 12 41 00 PM

Pull Request standards checklist - Please check off

  • This Branch will be carrying one single responsibility - feature/bugfix/style/refactor...
  • I have followed conventional commit messages and descriptive Branch naming.
  • My PR has descriptive folder/file names that I have worked on.

Testing checklist - Please check off

  • I have performed manual testing on my local to validate all changes.

Definition of Done - Please check off

  • My code is well tested and I have confidence my code works as I expect in a variety of situations.
  • I have lint and format code is enabled when the file is saved and I have fixed all errors highlighted by lint.
  • I have deleted all non-descriptive comments and dead code from the files I've touched.
  • I have rebased my branch with the base branch I want to merge into and all the commits in this PR are my own.

@mergemitra

mergemitra Bot commented Jan 9, 2026

Copy link
Copy Markdown

Change Summary

This PR introduces Redux Toolkit state management to support a global dark/light theme mode with persistence to localStorage.
It wires the Redux store into the app, adds a ThemeToggle button, and applies the selected theme by toggling the root dark class.
UI styling is updated for Tailwind v4 (including bg-linear-to-* gradients and dark: variants), plus improved form label/input associations.
It also adds PropTypes to key components and includes a new test suite for the theme toggle behavior.

Note (description vs diff): The PR description mentions PropTypes added to all components; in this diff it’s added to Button, Display, and Counter (not ThemeToggle).

File Changes

File Summary
package.json Adds Redux/React-Redux and PropTypes dependencies for global theme state and validation.
src/store/store.js Adds Redux Toolkit store configuration registering the new theme reducer.
src/store/slices/themeSlice.js Adds theme slice with toggle/set reducers and localStorage-backed initial state.
src/main.jsx Wraps the app with Redux Provider to supply the configured store.
src/App.jsx Integrates ThemeToggle, applies root dark class via effect, and updates layout for dark styles.
src/components/atoms/ThemeToggle/ThemeToggle.jsx Adds a theme toggle button that dispatches Redux actions and swaps sun/moon icons.
src/components/atoms/ThemeToggle/ThemeToggle.test.jsx Adds unit tests validating toggle behavior, aria labels, and state changes in Redux.
src/components/organisms/Counter/Counter.jsx Updates counter styling for dark mode/Tailwind v4 gradients and adds PropTypes.
src/components/atoms/Display/Display.jsx Updates dark-mode label styling, adjusts numeric display styling, and adds PropTypes.
src/components/atoms/Button/Button.jsx Adds PropTypes for button props; no behavioral changes to click handling.
src/index.css Adds Tailwind v4 dark variant setup and color-scheme support for dark mode.
tailwind.config.js Removes Tailwind config file (likely relying on Tailwind v4 defaults/configless setup).

@mergemitra

mergemitra Bot commented Jan 9, 2026

Copy link
Copy Markdown

PR Scorecard

Score

Communication Quality Code Correctness & Design Quality Test Quality & Coverage Code Readability & Maintainability
Scoring Methodology

Communication Score

The overall communication score is a weighted average:

Dimension Weight Evaluates
Description Quality 60% Title format (conventional commits) + Description clarity (what changed & why)
PR Size & Scope 25% Appropriate sizing, scope cohesion, and justification for size
Commit Messages 15% Conventional commits format, atomic & descriptive changes

Formula: (Description x 0.6) + (PR Size x 0.25) + (Commits x 0.15)

Scoring Framework

The scorecard evaluates code using 3 key reviewer questions:

Reviewer Question Category Subcategories
Is this the right solution, implemented the right way? Code Correctness & Design Quality Correctness, Robustness, Best Practices, Architecture
Would this catch bugs if the code broke tomorrow? Test Quality & Coverage Coverage, Test Design
Can someone new understand and safely modify this in 6 months? Code Readability & Maintainability Clarity, Structure, Consistency

Each category score is the average of its subcategories.

Score Bands: 90-100 Excellent | 75-89 Good | 60-74 Adequate | 40-59 Needs Work | 0-39 Critical

PR Communication Notes

Description Quality

  • ✅ Title follows conventional commits format and clearly describes the feature (feat(redux): ...).
  • ✅ Description is thorough: explains what/why, includes manual testing steps and screenshots.
  • ❌ Description claims 'PropTypes validation to all components' but new ThemeToggle has no PropTypes; either add PropTypes or update the description to be accurate.

PR Size & Scope

  • ✅ Change set is well-scoped to theme + redux + supporting tests/docs and stays within the ideal size (<500 lines, <20 files).
  • ✅ Adds focused tests for the new ThemeToggle which justify the added lines.
  • 💡 Suggestion: If you want further clarity split unrelated config changes into their own PR (e.g. tailwind config removal): tailwind.config.js (removed) and any global css tweaks in src/index.css.

Commit Messages

  • ✅ All commits follow conventional commit format ((scope): description) and are descriptive.
  • ✅ Commits are small and focused (install deps, add slice, add tests, apply styling), making history easy to review.

Notes

Code Correctness & Design Quality

  • ❌ Stale state updates in click handlers (setCount(count + step) / setCount(count - step)) can drop rapid clicks due to captured count; use functional updates at src/components/organisms/Counter/Counter.jsx
  • ❌ Invalid numeric-variant styling likely does nothing (className="... tabular" and style={{ fontVariantNumeric: 'tabular' }}); should be tabular-nums / fontVariantNumeric: 'tabular-nums' at src/components/atoms/Display/Display.jsx
  • ❌ Tailwind class min-w-35 is likely not generated (previously min-w-[140px]), which can break layout at src/App.jsx
  • color-scheme: dark is set only from OS preference, not from the .dark class toggle, so native form controls may not match the selected theme at src/index.css
  • ❌ Theme persistence reads/writes can throw (blocked storage, quota, privacy mode) and will currently crash since localStorage.getItem/setItem aren’t wrapped/guarded at src/store/slices/themeSlice.js
  • ❌ Persisted theme and setTheme payload are not validated (any string can be stored/used), which can put the app into an unsupported theme state at src/store/slices/themeSlice.js
  • ❌ Direct document.documentElement access in useEffect will throw in non-DOM environments (SSR/tests without DOM); add an environment guard at src/App.jsx
  • ❌ Redux reducers have side effects (writing to localStorage) which breaks reducer purity and complicates testing/debugging; move persistence to middleware/listener/effect at src/store/slices/themeSlice.js
  • ❌ Reusable Button doesn’t set type="button", so when used inside a <form> it will submit by default at src/components/atoms/Button/Button.jsx
  • 💭 [nitpick] Consider renaming local variable variants to something like variantClassMap to reduce ambiguity at src/components/atoms/Button/Button.jsx
  • ❌ Theme concerns are split across reducer (persistence) and App (DOM class toggling), making the feature harder to evolve; consider a single theme “integration” layer (e.g., listener middleware that persists + toggles DOM class) at src/store/slices/themeSlice.js and src/App.jsx
  • 💭 [nitpick] If theme is the only global state, a lighter approach (context + persistence hook) may reduce complexity unless Redux is intentionally being introduced project-wide

Test Quality & Coverage

  • ❌ No tests cover theme persistence behavior (reading initial theme from storage and writing on toggle), so regressions won’t be caught at src/store/slices/themeSlice.js
  • ❌ No tests verify the .dark class is applied/removed on the document root when theme changes (core visible behavior) at src/App.jsx
  • ❌ Tests assert internal Redux state (store.getState()) rather than user-visible behavior (e.g., aria-label/icon/class changes), making them more brittle to refactors at src/components/atoms/ThemeToggle/ThemeToggle.test.jsx
  • 💭 [nitpick] Minor formatting/import spacing issues reduce readability (import { describe, it, expect} ...) at src/components/atoms/ThemeToggle/ThemeToggle.test.jsx

Code Readability & Maintainability

  • 💭 [nitpick] State name key is non-descriptive; something like counterInstanceKey communicates intent better at src/App.jsx
  • 💭 [nitpick] createMockStore/renderWithStore are likely reusable across future Redux component tests; consider moving to a shared test util to avoid duplication at src/components/atoms/ThemeToggle/ThemeToggle.test.jsx
  • ❌ ThemeToggle component is missing PropTypes while other components were updated to include them, leading to inconsistent runtime validation at src/components/atoms/ThemeToggle/ThemeToggle.jsx
  • 💭 [nitpick] Mixed indentation/formatting styles across files (e.g., 2-space vs 4-space blocks) makes diffs noisier and the codebase harder to scan consistently

Comment on lines +13 to +14
className="text-7xl font-bold bg-linear-to-r from-green-600 to-emerald-600 bg-clip-text text-transparent tabular transition-all duration-300"
style={{ fontVariantNumeric: 'tabular' }}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: 🟡 Moderate

Correctness: Invalid tabular number styling

tabular and fontVariantNumeric: 'tabular' are unlikely to do what you intend. The CSS keyword is tabular-nums, and Tailwind’s utility is tabular-nums.

Why this matters: Without proper tabular numerals, the counter can visually “jump” width as digits change.

Suggested fix:

<div
  className="text-7xl font-bold bg-linear-to-r from-green-600 to-emerald-600 bg-clip-text text-transparent tabular-nums transition-all duration-300"
  aria-live="polite"
  aria-label={displayAriaLabel}
>
  {value}
</div>

(And remove the inline style entirely.)

Comment on lines +9 to +22
const createMockStore = (initialTheme = 'light') => {
return configureStore({
reducer: {
theme: themeReducer,
},
preloadedState: {
theme: { mode: initialTheme },
},
})
}

const renderWithStore = (component, store) => {
return render(<Provider store={store}>{component}</Provider>)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: 💭 Minor [nitpick]

Structure: Reusable Redux test helpers should be centralized

createMockStore / renderWithStore are likely to be reused as more Redux-connected components are added.

Why this matters: Keeping these helpers in one place prevents duplication and keeps tests focused on behavior.

Suggested fix:

// src/test/testUtils.js
import { render } from '@testing-library/react'
import { Provider } from 'react-redux'

export const renderWithStore = (ui, store) => {
  return render(<Provider store={store}>{ui}</Provider>)
}

Then import it in tests instead of redefining per file.

Comment thread src/App.jsx Outdated
const themeMode = useSelector(selectThemeMode)
const [initialValue, setInitialValue] = useState(10)
const [step, setStep] = useState(5)
const [key, setKey] = useState(0)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: 💭 Minor [nitpick]

Clarity: Non-descriptive state name key

key doesn’t communicate that it’s used to force-remount the Counter.

Why this matters: Readers have to infer intent from usage; clearer naming makes maintenance easier.

Suggested fix:

const [counterInstanceKey, setCounterInstanceKey] = useState(0)

// ...
setCounterInstanceKey((prevKey) => prevKey + 1)

// ...
<Counter key={counterInstanceKey} initialValue={initialValue} step={step} />

Comment on lines +25 to +30
setTheme: (state, action) => {
state.mode = action.payload
if (globalThis.window !== undefined) {
globalThis.localStorage.setItem('theme', state.mode)
}
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: 🟠 Major

Robustness: Persisted theme / setTheme payload is not validated

setTheme writes action.payload directly to state, and getInitialTheme trusts whatever is in storage. Any unexpected value (typo, manual edit, stale data) can put the app into an unsupported state.

Why this matters: Unsupported values can lead to UI desyncs (no .dark class, wrong icon/label, etc.) and make bugs harder to diagnose.

Suggested fix:

const THEME_MODES = {
    light: 'light',
    dark: 'dark',
}

const isThemeMode = (value) => value === THEME_MODES.light || value === THEME_MODES.dark

// in getInitialTheme()
const savedTheme = safeStorageGet('theme')
return isThemeMode(savedTheme) ? savedTheme : THEME_MODES.light

// in setTheme reducer
setTheme: (state, action) => {
    state.mode = isThemeMode(action.payload) ? action.payload : THEME_MODES.light
},

@@ -0,0 +1,100 @@
import { describe, it, expect} from 'vitest'

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: 💭 Minor [nitpick]

Test Design: Minor import formatting

There’s a small spacing issue in the Vitest import (expect}) that makes the file slightly harder to scan.

Suggested fix:

import { describe, it, expect } from 'vitest'

Comment on lines +4 to +53
export const ThemeToggle = () => {
const dispatch = useDispatch()
const themeMode = useSelector(selectThemeMode)

const handleClickToggleTheme = () => {
dispatch(toggleTheme())
}

const isDarkMode = themeMode === 'dark'

return (
<button
onClick={handleClickToggleTheme}
aria-label={`Switch to ${isDarkMode ? 'light' : 'dark'} mode`}
className="p-3 rounded-lg bg-white dark:bg-gray-800 shadow-md hover:shadow-lg transition-all duration-200 border border-gray-200 dark:border-gray-700"
>
{isDarkMode ? (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={2}
stroke="currentColor"
className="w-6 h-6 text-yellow-500"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M12 3v2.25m6.364.386l-1.591 1.591M21 12h-2.25m-.386 6.364l-1.591-1.591M12 18.75V21m-4.773-4.227l-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0z"
/>
</svg>
) : (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={2}
stroke="currentColor"
className="w-6 h-6 text-gray-700"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M21.752 15.002A9.718 9.718 0 0118 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 003 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 009.002-5.998z"
/>
</svg>
)}
</button>
)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: 🟡 Moderate

Consistency: ThemeToggle missing PropTypes

Other components were updated with PropTypes, but ThemeToggle doesn’t define any, creating inconsistent runtime validation.

Why this matters: Inconsistency makes it easier for components to silently drift in how they’re used and validated.

Suggested fix (even if there are no required props, allow an override like className):

import PropTypes from 'prop-types'

export const ThemeToggle = ({ className = '' }) => {
  // ...
  return (
    <button
      type="button"
      onClick={handleClickToggleTheme}
      aria-label={`Switch to ${isDarkMode ? 'light' : 'dark'} mode`}
      className={`p-3 rounded-lg bg-white dark:bg-gray-800 shadow-md hover:shadow-lg transition-all duration-200 border border-gray-200 dark:border-gray-700 ${className}`}
    >
      {/* ... */}
    </button>
  )
}

ThemeToggle.propTypes = {
  className: PropTypes.string,
}

Comment on lines +3 to +8
const getInitialTheme = () => {
if (globalThis.window !== undefined) {
const savedTheme = globalThis.localStorage.getItem('theme')
return savedTheme || 'light'
}
return 'light'

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: 🟠 Major

Robustness: localStorage access can throw and crash the app

localStorage.getItem / setItem can throw in some environments (privacy mode, blocked storage, quota exceeded). Right now an exception will bubble up and potentially break the app.

Why this matters: Theme toggle and initial load can hard-fail for a subset of users/browsers.

Suggested fix:

const safeStorageGet = (key) => {
    try {
        return globalThis.localStorage?.getItem(key) ?? null
    } catch {
        return null
    }
}

const safeStorageSet = (key, value) => {
    try {
        globalThis.localStorage?.setItem(key, value)
    } catch {
        // ignore persistence failures
    }
}

Use safeStorageGet('theme') inside getInitialTheme() and safeStorageSet('theme', mode) wherever you persist.

Comment thread src/index.css Outdated
Comment on lines +5 to +11
@supports (selector(&:where(.dark, .dark *))) {
@media (prefers-color-scheme: dark) {
:root {
color-scheme: dark;
}
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: 🟡 Moderate

Correctness: color-scheme doesn’t follow the selected theme

color-scheme: dark is currently tied to OS preference, not to your .dark class toggle. That means native controls (inputs, scrollbars) may stay “dark” even when the app is in light mode (or vice versa).

Why this matters: Users will see inconsistent theming for native UI elements.

Suggested fix:

:root {
    color-scheme: light;
}

:root.dark {
    color-scheme: dark;
}

(Then you can drop the @media (prefers-color-scheme: dark) override unless you’re explicitly supporting “system” mode.)

Comment thread src/App.jsx Outdated
Comment on lines +14 to +21
useEffect(() => {
const root = document.documentElement
if (themeMode === 'dark') {
root.classList.add('dark')
} else {
root.classList.remove('dark')
}
}, [themeMode])

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: 🟠 Major

Robustness: Un-guarded document access breaks non-DOM environments

document.documentElement will throw in SSR or test environments that don’t provide a DOM.

Why this matters: It makes rendering App in SSR or certain test setups fail immediately.

Suggested fix:

useEffect(() => {
  if (typeof document === 'undefined') return

  const root = document.documentElement
  root.classList.toggle('dark', themeMode === 'dark')
}, [themeMode])

Comment thread src/App.jsx Outdated
<div className="flex gap-3 flex-wrap">
<div className="flex-1 min-w-[140px]">
<label className="block text-xs font-medium text-gray-700 mb-1">
<div className="flex-1 min-w-35">

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: 🟡 Moderate

Correctness: Non-generated Tailwind class may break layout

min-w-35 is likely not a generated Tailwind utility (unless you’ve explicitly extended the scale), so the min width constraint may silently stop applying.

Why this matters: The settings inputs can collapse and wrap unexpectedly, especially on narrow screens.

Suggested fix:

<div className="flex-1 min-w-[140px]">

(Alternatively, use a known scale value like min-w-36 if you want a tokenized size.)

Comment on lines +46 to +56
it('toggles theme from light to dark when clicked', async () => {
const store = createMockStore('light')
renderWithStore(<ThemeToggle />, store)
const user = userEvent.setup()

const button = screen.getByRole('button')
await user.click(button)

const state = store.getState()
expect(state.theme.mode).toBe('dark')
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: 🟡 Moderate

Test Design: Tests assert internal Redux state instead of user-visible behavior

These tests read store.getState() after clicks. That couples tests to implementation details and makes refactors (middleware, different storage, derived selectors) painful.

Why this matters: UI tests should primarily verify what the user can observe (aria-label, icon swap, root class), not internal store shape.

Suggested fix:

it('toggles aria-label when clicked', async () => {
  const store = createMockStore('light')
  renderWithStore(<ThemeToggle />, store)
  const user = userEvent.setup()

  const button = screen.getByRole('button', { name: 'Switch to dark mode' })
  await user.click(button)

  expect(screen.getByRole('button', { name: 'Switch to light mode' })).toBeInTheDocument()
})

Comment on lines +3 to +36
const getInitialTheme = () => {
if (globalThis.window !== undefined) {
const savedTheme = globalThis.localStorage.getItem('theme')
return savedTheme || 'light'
}
return 'light'
}

const initialState = {
mode: getInitialTheme(),
}

const themeSlice = createSlice({
name: 'theme',
initialState,
reducers: {
toggleTheme: (state) => {
state.mode = state.mode === 'light' ? 'dark' : 'light'
if (globalThis.window !== undefined) {
globalThis.localStorage.setItem('theme', state.mode)
}
},
setTheme: (state, action) => {
state.mode = action.payload
if (globalThis.window !== undefined) {
globalThis.localStorage.setItem('theme', state.mode)
}
},
},
})

export const { toggleTheme, setTheme } = themeSlice.actions
export const selectThemeMode = (state) => state.theme.mode
export default themeSlice.reducer

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: 🟠 Major

Coverage: Missing tests for theme persistence (read/write)

There are no tests verifying (1) initial theme is read from storage and validated, and (2) theme changes are persisted.

Why this matters: Persistence bugs are easy to introduce and hard to notice until a refresh; tests prevent regressions.

Suggested fix (example test cases):

import { describe, it, expect, vi, beforeEach } from 'vitest'
import { configureStore } from '@reduxjs/toolkit'
import themeReducer, { toggleTheme } from './themeSlice'

describe('theme persistence', () => {
  beforeEach(() => {
    vi.restoreAllMocks()
  })

  it('uses stored theme when valid', () => {
    vi.spyOn(globalThis.localStorage, 'getItem').mockReturnValue('dark')
    const store = configureStore({ reducer: { theme: themeReducer } })
    expect(store.getState().theme.mode).toBe('dark')
  })

  it('persists theme on toggle', () => {
    const setItemSpy = vi.spyOn(globalThis.localStorage, 'setItem')
    const store = configureStore({ reducer: { theme: themeReducer } })
    store.dispatch(toggleTheme())
    expect(setItemSpy).toHaveBeenCalledWith('theme', store.getState().theme.mode)
  })
})

(If you move persistence into listener middleware, assert setItem calls via the configured store middleware instead.)

Comment on lines +15 to +31
const themeSlice = createSlice({
name: 'theme',
initialState,
reducers: {
toggleTheme: (state) => {
state.mode = state.mode === 'light' ? 'dark' : 'light'
if (globalThis.window !== undefined) {
globalThis.localStorage.setItem('theme', state.mode)
}
},
setTheme: (state, action) => {
state.mode = action.payload
if (globalThis.window !== undefined) {
globalThis.localStorage.setItem('theme', state.mode)
}
},
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: 🟠 Major

Best Practices: Reducers have side effects (writing to localStorage)

Reducers should remain pure; persisting to storage inside reducers makes behavior harder to test/debug and can break tooling assumptions.

Why this matters: Side effects in reducers complicate replay/debugging, can introduce hidden failures, and make unit tests brittle.

Recommended approach: keep reducers pure and move persistence to a listener middleware.

Suggested fix:

// themeSlice.js (reducers become pure)
reducers: {
    toggleTheme: (state) => {
        state.mode = state.mode === 'light' ? 'dark' : 'light'
    },
    setTheme: (state, action) => {
        state.mode = isThemeMode(action.payload) ? action.payload : 'light'
    },
},
// store.js
import { configureStore, createListenerMiddleware } from '@reduxjs/toolkit'
import themeReducer, { toggleTheme, setTheme, selectThemeMode } from './slices/themeSlice'

const themeListener = createListenerMiddleware()

themeListener.startListening({
    matcher: (action) => toggleTheme.match(action) || setTheme.match(action),
    effect: async (action, listenerApi) => {
        const mode = selectThemeMode(listenerApi.getState())
        try {
            globalThis.localStorage?.setItem('theme', mode)
        } catch {
            // ignore persistence failures
        }
    },
})

export const store = configureStore({
    reducer: { theme: themeReducer },
    middleware: (getDefaultMiddleware) =>
        getDefaultMiddleware().prepend(themeListener.middleware),
})

Comment thread src/App.jsx Outdated
Comment on lines +13 to +21
// Apply theme to document on mount and when theme changes
useEffect(() => {
const root = document.documentElement
if (themeMode === 'dark') {
root.classList.add('dark')
} else {
root.classList.remove('dark')
}
}, [themeMode])

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: 🟠 Major

Architecture: Theme integration split across reducer + App DOM side effects

Theme persistence and DOM class toggling are spread across multiple layers (slice + App effect), which makes the feature harder to extend (e.g., adding “system” mode, syncing color-scheme, analytics, etc.).

Why this matters: Centralizing the “integration” side effects reduces duplication, avoids subtle ordering bugs, and keeps UI components focused on rendering.

Recommended approach: keep themeSlice pure, and use a single listener middleware to (1) persist to storage and (2) toggle document.documentElement.classList.

Suggested fix:

// store.js listener effect (extend persistence listener)
effect: async (action, listenerApi) => {
  const mode = selectThemeMode(listenerApi.getState())

  if (typeof document !== 'undefined') {
    document.documentElement.classList.toggle('dark', mode === 'dark')
  }

  try {
    globalThis.localStorage?.setItem('theme', mode)
  } catch {
    // ignore
  }
}

Then App can drop the theme-related useEffect entirely.

Comment thread src/App.jsx Outdated
Comment on lines +13 to +21
// Apply theme to document on mount and when theme changes
useEffect(() => {
const root = document.documentElement
if (themeMode === 'dark') {
root.classList.add('dark')
} else {
root.classList.remove('dark')
}
}, [themeMode])

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: 🟠 Major

Coverage: No test verifies .dark class toggling on the root element

The core visible behavior is the .dark class being added/removed on document.documentElement, but there’s no test asserting this behavior.

Why this matters: A regression here would make the UI appear “stuck” in one theme with no failing tests.

Suggested fix (integration-style):

import { render } from '@testing-library/react'
import { Provider } from 'react-redux'
import { configureStore } from '@reduxjs/toolkit'
import themeReducer, { setTheme } from './store/slices/themeSlice'
import App from './App'

it('adds and removes the dark class when theme changes', async () => {
  const store = configureStore({ reducer: { theme: themeReducer } })

  render(
    <Provider store={store}>
      <App />
    </Provider>
  )

  store.dispatch(setTheme('dark'))
  expect(document.documentElement.classList.contains('dark')).toBe(true)

  store.dispatch(setTheme('light'))
  expect(document.documentElement.classList.contains('dark')).toBe(false)
})

(If you move DOM toggling to listener middleware, keep the same assertion—just ensure the store in the test includes that middleware.)

Comment thread src/store/store.js
Comment on lines +4 to +8
export const store = configureStore({
reducer: {
theme: themeReducer,
},
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: 💭 Minor [nitpick]

Architecture: Consider whether Redux is intentional project-wide

If theme is the only global state, a Context + persistence hook can be lighter. If Redux is being introduced as a foundation for more global features, then this is totally fine—just worth confirming the direction.

Why this matters: It affects long-term complexity and how future state is structured.

@mergemitra

mergemitra Bot commented Jan 9, 2026

Copy link
Copy Markdown

PR Analysis

Focus Areas for Architect Review

  1. Theme “source of truth” & side-effect boundaries: Decide whether DOM class toggling + persistence should live in a single integration layer (middleware/listener/custom hook) to keep UI components and state reducers clean and make future additions (e.g., “system” mode) straightforward.
  2. State management direction: Confirm Redux Toolkit is the intended foundation for upcoming global concerns (beyond theme) vs. using a lighter pattern for this app’s scope.
  3. Runtime environment expectations: Align on whether SSR/non-DOM rendering is in-scope; that choice affects where/how theme initialization and document mutations should occur.

Recommendation

Action: REQUEST CHANGES

Quick Wins:

  • Update counter click handlers to use functional setCount(prev => prev ± step) to avoid dropped rapid updates.
  • Add type="button" (and prop support) to the shared Button to prevent accidental form submissions when reused.
  • Replace min-w-35 with a guaranteed Tailwind utility (e.g., min-w-[140px]) to avoid silent layout regressions.
Author Insights

PR Type: feat

Missing Skills

  • Correctness: State updates may break on rapid repeated clicks.
  • Robustness: Storage and DOM access need safer guards.
  • Best Practices: Reducers should avoid side effects like persistence.
  • Test Coverage: Missing tests for persistence and root class.

Strengths

  • Description Quality: PR description is clear and easy to test.
  • Commit Messages: Commits are consistent and easy to follow.
  • Code Clarity: UI changes are readable and well organized.
  • Code Structure: Components and store setup are well separated.

Sarath-Kumar-G and others added 6 commits January 11, 2026 10:25
chore(setup): initialize counter application with react vite and tailwind
…ings management

- Complete refactor of context architecture and tooling migration
- Split context into counterContext.js (context + hook) and CounterProvider.jsx (provider component)
- Moved Settings component inside CounterProvider for proper encapsulation
- Implemented applySettings() action to replace remount pattern
- Added useEffect to sync count when initialValue/step change
- Created Settings molecule component for settings UI
- Counter component uses only state from context, implements handlers locally
- App.jsx simplified to just wrap components with CounterProvider
- Added parseFiniteNumber validation for empty/non-finite numeric inputs
- Removed draft values update settings reactivity - changes only apply on "Apply" click
- Removed ESLint, Prettier, and all related packages, and added biome and cspell
- Harden CounterProvider against invalid inputs and memoize context value
- Sync draft inputs in Settings to prevent UI drift from source of truth
- Replace hard-coded defaults in App with named constants
- Update tests to use accessible queries instead of raw text assertions
- Refactor CounterContext tests to focus on provider behavior and add integration test
- Make package.json check script non-mutating with separate check:fix command
- Extract shared getCount helper in App and Counter test files to avoid duplication
- Add proper assertions to CounterContext integration test for workflow validation
- Sanitize CounterProvider props against invalid inputs and guard applySettings against null/non-objects
feat(context): implement context api state management for counter app
@mergemitra

mergemitra Bot commented Jan 29, 2026

Copy link
Copy Markdown

Tip

Need another review?

Tag me and say rereview for re-analysis after you have fixed all the issues.

@cw-pr-agent rereview

- Add themeSlice with toggleTheme/setTheme and localStorage persistence
- Create accessible ThemeToggle button (type, SVG titles) and integrate with Redux
- Apply/remove dark class on document.documentElement from App.jsx
- Fix imports and provider wiring in main.jsx and tests; wrap tests with Provider/CounterProvider
- Tailwind/PostCSS fixes: set dark-mode handling, remove incompatible @config, update index.css
- Fix UI styling issues (gradient class, CSS import) and accessibility lint problems
- Add/remove temporary debug logs while diagnosing theme behavior
- Clear Vite cache and force rebuild to regenerate Tailwind dark variants
- All tests passing (50/50)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants