Skip to content

refactor: eliminate Redux round-trip anti-pattern with createSelector#4

Open
ashkanrdn wants to merge 1 commit intorefactor/type-consolidationfrom
refactor/redux-cleanup
Open

refactor: eliminate Redux round-trip anti-pattern with createSelector#4
ashkanrdn wants to merge 1 commit intorefactor/type-consolidationfrom
refactor/redux-cleanup

Conversation

@ashkanrdn
Copy link
Owner

@ashkanrdn ashkanrdn commented Feb 21, 2026

Summary

  • mapSlice: replace colorScaleValues + barChartData with enhancedGeojson: EnhancedFeature[] — single worker output stored in Redux
  • Add lib/features/map/selectors.ts with selectBarChartData, selectRankedCounties, selectColorScaleValues (all memoized via createSelector)
  • filterSlice: remove rankedCounties — now derived by selector
  • MapStory: worker onmessage dispatches one setEnhancedGeojson; removes 3 useMemo + useEffect round-trip pairs (~60 lines); colorScale built from selectColorScaleValues
  • BarChartWidget, CountyRank: read from typed selectors instead of raw state slices

Test plan

  • npm run build passes with no TypeScript errors
  • Load /map — map renders counties with colors
  • Metric buttons switch displayed data
  • Per capita toggle works
  • County click highlights in right sidebar ranking
  • BarChart updates when metric changes

🤖 Generated with Claude Code

Summary by Sourcery

Refactor map data flow so worker outputs a single enhanced geojson payload into Redux, with derived visualization data computed via memoized selectors.

New Features:

  • Add memoized selectors for enhanced geojson-derived color scale values, ranked counties, and bar chart data.

Enhancements:

  • Simplify MapStory by dispatching worker results directly to Redux and deriving color scales locally from selector-provided values.
  • Streamline BarChartWidget and CountyRank to consume typed selector outputs instead of raw Redux slices.
  • Remove redundant ranked county and bar chart state from Redux in favor of selector-derived data.

- mapSlice: replace colorScaleValues + barChartData with enhancedGeojson (single source)
- Add lib/features/map/selectors.ts with selectBarChartData, selectRankedCounties,
  selectColorScaleValues (all memoized via createSelector)
- filterSlice: remove rankedCounties — derived by selector instead
- MapStory: dispatch setEnhancedGeojson once from worker; remove 3 useMemo+useEffect
  round-trips; colorScale built from selectColorScaleValues
- BarChartWidget, CountyRank: read from selectors instead of raw state slices

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@vercel
Copy link

vercel bot commented Feb 21, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
casi-2 Error Error Feb 21, 2026 7:03pm

@sourcery-ai
Copy link

sourcery-ai bot commented Feb 21, 2026

Reviewer's Guide

Refactors map-related Redux state to store only worker-produced enhancedGeojson and derives color scale values, ranked counties, and bar chart data via memoized selectors, simplifying MapStory and updating dependent widgets to consume typed selectors instead of raw slices.

Sequence diagram for worker-to-selectors map data flow

sequenceDiagram
    actor User
    participant MapStory
    participant Worker
    participant ReduxStore
    participant Selectors
    participant BarChartWidget
    participant CountyRank

    User->>MapStory: Interacts with map / filters
    MapStory->>Worker: postMessage(geojsonData, filteredData, metric, perCapita)
    Worker-->>MapStory: message(enhancedGeojson)
    MapStory->>ReduxStore: dispatch(setEnhancedGeojson(enhancedGeojson))
    BarChartWidget->>ReduxStore: useSelector(selectBarChartData)
    CountyRank->>ReduxStore: useSelector(selectRankedCounties)
    MapStory->>ReduxStore: useSelector(selectColorScaleValues)

    ReduxStore->>Selectors: selectEnhancedGeojson, selectSelectedMetric, selectIsPerCapita
    Selectors-->>ReduxStore: derived colorScaleValues, rankedCounties, barChartData

    ReduxStore-->>MapStory: colorScaleValues
    ReduxStore-->>BarChartWidget: barChartData, colorScaleValues
    ReduxStore-->>CountyRank: rankedCounties

    MapStory->>MapStory: build d3 colorScale from colorScaleValues
    BarChartWidget->>BarChartWidget: build d3 colorScale from colorScaleValues
    CountyRank->>CountyRank: render ranking list
Loading

Class diagram for updated map and filter Redux slices with selectors

classDiagram
    class MapState {
        string selectedCounty
        EnhancedFeature[] enhancedGeojson
    }

    class FilterState {
        CsvRow[] filteredData
        number[2] yearRange
        string selectedMetric
        DataSourceType selectedDataSource
        boolean isPerCapita
        string[] selectedCounties
    }

    class MapSlice {
        +MapState initialState
        +setSelectedCounty(payload string) void
        +setEnhancedGeojson(payload EnhancedFeature[]) void
    }

    class FilterSlice {
        +FilterState initialState
        +setCsvData(payload CsvRow[]) void
        +toggleFilter(payload unknown) void
        +setYear(payload number) void
        +setSelectedDataSource(payload DataSourceType) void
        +setIsPerCapita(payload boolean) void
        +setSelectedCounties(payload string[]) void
        +setFilteredData(payload CsvRow[]) void
    }

    class RootState {
        MapState map
        FilterState filters
    }

    class MapSelectors {
        +selectEnhancedGeojson(state RootState) EnhancedFeature[]
        +selectSelectedCounty(state RootState) string
        +selectColorScaleValues(state RootState) number[]
        +selectRankedCounties(state RootState) RankedCounty[]
        +selectBarChartData(state RootState) BarChartDatum[]
    }

    class RankedCounty {
        string name
        number value
        number rank
    }

    class BarChartDatum {
        string county
        number value
    }

    class EnhancedFeatureProperties {
        string name
        number rawValue
        number perCapitaValue
        number metricValue
    }

    class EnhancedFeature {
        EnhancedFeatureProperties properties
        unknown geometry
        unknown id
    }

    MapSlice --> MapState
    FilterSlice --> FilterState
    RootState o-- MapState
    RootState o-- FilterState

    MapSelectors ..> RootState
    MapSelectors ..> EnhancedFeature
    MapSelectors ..> RankedCounty
    MapSelectors ..> BarChartDatum

    EnhancedFeature o-- EnhancedFeatureProperties
Loading

File-Level Changes

Change Details Files
Consolidate map Redux state to store only enhancedGeojson and selectedCounty.
  • Update MapState to replace colorScaleValues and barChartData with enhancedGeojson array of EnhancedFeature
  • Adjust initialState to initialize enhancedGeojson as an empty array
  • Replace setColorScaleValues and setBarChartData reducers with a single setEnhancedGeojson reducer
  • Update exported mapSlice actions to expose setSelectedCounty and setEnhancedGeojson only
lib/features/map/mapSlice.ts
Move derived data (color scale values, ranked counties, bar chart data) from component state into memoized selectors.
  • Introduce selectEnhancedGeojson and selectSelectedCounty primitive selectors
  • Add selectColorScaleValues selector deriving numeric metric values from enhancedGeojson
  • Add selectRankedCounties selector that sorts counties by metric and assigns ranks
  • Add selectBarChartData selector that derives per-capita/raw values and sorts descending, depending on isPerCapita
lib/features/map/selectors.ts
Refactor MapStory to dispatch worker results into Redux and consume selector-derived values instead of local/mirrored Redux state.
  • Use useSelector to read enhancedGeojson and colorScaleValues via new selectors
  • Update worker onmessage handler to dispatch setEnhancedGeojson instead of setting local component state
  • Remove local enhancedGeojson and colorValues state and associated useEffect hooks for syncing to Redux
  • Remove local rankedCounties and barChartData memoization plus dispatch effects, relying on selectors instead
  • Rebuild colorScale with useMemo using selector-derived colorScaleValues as the domain source
app/components/widgets/MapStory.tsx
Simplify filterSlice by removing rankedCounties from filter-related Redux state.
  • Remove rankedCounties field from FilterState and initialState
  • Delete setRankedCounties reducer and its export from filterSlice
  • Remove rankedCounties reset when switching data sources
lib/features/filters/filterSlice.ts
Update widgets to consume derived data via typed selectors instead of raw Redux fields.
  • In BarChartWidget, replace direct access to state.map.barChartData and state.map.colorScaleValues with selectBarChartData and selectColorScaleValues selectors, and adjust local variable naming
  • In CountyRank, replace useSelector of state.filters.rankedCounties with selectRankedCounties selector
app/components/widgets/BarChartWidget.tsx
app/components/widgets/CountyRank.tsx

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Hey - I've found 1 issue, and left some high level feedback:

  • In selectRankedCounties, value: f.properties[metric] as number is assumed to be numeric without filtering, which can lead to NaN rankings; consider mirroring the numeric guard used in selectColorScaleValues (or reusing a shared helper) before sorting.
  • selectSelectedCounty in selectors.ts is currently unused; if it's meant for future use consider wiring it into components or remove it to keep the selector surface minimal.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `selectRankedCounties`, `value: f.properties[metric] as number` is assumed to be numeric without filtering, which can lead to `NaN` rankings; consider mirroring the numeric guard used in `selectColorScaleValues` (or reusing a shared helper) before sorting.
- `selectSelectedCounty` in `selectors.ts` is currently unused; if it's meant for future use consider wiring it into components or remove it to keep the selector surface minimal.

## Individual Comments

### Comment 1
<location> `lib/features/map/selectors.ts:26` </location>
<code_context>
+        geojson
+            .map((f) => ({
+                name: f.properties.name,
+                value: f.properties[metric] as number,
+                rank: 0,
+            }))
</code_context>

<issue_to_address>
**issue:** Guard `value` against non-numeric or missing data before ranking counties.

If `f.properties[metric]` is missing or non-numeric, `value` becomes `NaN`, so `b.value - a.value` will also be `NaN` and the sort order undefined. Please either reuse the numeric filtering from `selectColorScaleValues` (e.g., filter to finite numbers before sorting) or coerce invalid values to a deterministic default (such as `0`) before ranking.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

geojson
.map((f) => ({
name: f.properties.name,
value: f.properties[metric] as number,
Copy link

Choose a reason for hiding this comment

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

issue: Guard value against non-numeric or missing data before ranking counties.

If f.properties[metric] is missing or non-numeric, value becomes NaN, so b.value - a.value will also be NaN and the sort order undefined. Please either reuse the numeric filtering from selectColorScaleValues (e.g., filter to finite numbers before sorting) or coerce invalid values to a deterministic default (such as 0) before ranking.

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.

1 participant