if-me.org is an open source mental health platform that allows users to share experiences with trusted people. The project uses Ruby on Rails on the backend and React on the frontend, integrated through the react-on-rails gem.
The project carries significant accumulated technical debt. React was gradually inserted into the project — which originally was pure Rails with ERB — without a clear architectural strategy. The result is an inconsistent integration between the two layers, where Rails frequently assumes responsibilities that should belong to React.
Architectural:
- Rails builds complete form schemas, including field types, labels, validations and UI flags — responsibilities that belong to React's View layer
- React components act as passive renderers, receiving excessively processed data from Rails
- The Moment detail page uses 6 separate
react_componentcalls interspersed with ERB, fragmenting the layout between two rendering engines. The same pattern exists instrategies/show.html.erbwith 4 calls - Pre-rendered HTML is passed as props to React components, creating XSS vectors and making testing and styling impossible
Technical:
babel-plugin-flow-react-proptypeshas known incompatibilities with imported Flow types and modern syntax, causing crashes in development- Outdated stack: React 17, Jest 26, ESLint 7, Storybook 6.5, chart.js 2 — with growing incompatibility risks
- Deprecated webpack plugins:
optimize-css-assets-webpack-plugin,extract-css-chunks-webpack-plugin,url-loader,file-loader - Babel plugins with deprecated names:
@babel/plugin-proposal-class-properties,@babel/plugin-proposal-private-methods,@babel/plugin-proposal-private-property-in-object— moved to@babel/plugin-transform-*in Babel 7.22+ - The axios package was the target of a supply chain attack in March 2026 (versions 1.14.1 and 0.30.4 compromised); although the project's current version (
^1.15.0) was not directly affected, the incident highlights the risk of this dependency stylelint ^13.7.2— very outdated, current version is 16.x- Font Awesome across 3 different packages with outdated versions:
@fortawesome/react-fontawesome ^0.1.4,@fortawesome/free-solid-svg-icons ^5.10.2, and stillfont-awesome ^4.7.0(legacy CSS version) history ^4.9.0— legacy; React Router v6+ requires history v5node-polyfill-webpack-pluginpresent in webpack — may be unnecessary depending on actual usageexact_by_default=falsein.flowconfig— allows inexact objects by default, reducing type safety
Performance:
- N+1 queries in
ViewersHelperandCommentsFormHelper— one query per viewer per form @babel/polyfillandes5-shimunnecessary with current configurations
Components that already follow the ideal architecture — CrisisPrevention, MomentTemplates, CarePlanContacts, Notifications — serve as reference for the correct Rails/React integration pattern.
These principles guide all contribution decisions:
- Rails is a data provider, not a UI builder — Rails should provide structured and clean data; React is responsible for the entire presentation layer
- Small and explicit contracts — each integration point between Rails and React must be minimal, clear and well-defined
- Autonomous components — each React component must be able to function and be tested independently of Rails
- Stability before modernization — the development environment must be stable before any architectural refactoring
- Small and focused PRs — each PR must have a clear and unique scope, facilitating review and rollback
- Verify before submitting — no PR should be opened without local verification that nothing broke
Stabilize the development environment and modernize critical dependencies, ensuring the project works consistently across development, testing and production.
Clearly delineate responsibility layers — Rails as data API, React as the complete presentation layer. Strengthen existing contracts and eliminate unnecessary coupling.
Complete migration to TypeScript, which will be natural and efficient after contracts are clean and well-defined. With autonomous components and small contracts, typing is precise and migration is incremental. The project maintainer has confirmed interest in this migration.
Rails (Model + Controller)
↓ clean and minimal data
react-on-rails (initialization bridge)
↓ initial props
React (complete View)
→ manages state
→ defines UI
→ controls forms
→ makes requests via native fetch (after B3)
Summary: Remove the plugin that automatically generates PropTypes from Flow types, eliminating development crashes caused by known incompatibilities.
Context: The babel-plugin-flow-react-proptypes plugin is configured only in development (.babelrc). It has two critical incompatibilities: it does not support the ...$Exact<> Flow syntax and cannot resolve types imported from other modules, generating ReferenceError: bpfrpt_proptype_* is not defined at runtime. Both issues are known limitations of the plugin — issue #157 in the official repository. The project already uses Flow for static compile-time validation, making runtime-generated PropTypes redundant.
Note: The project maintainer is investigating a fix for Flow that may resolve some of these instabilities. Wait for her response before executing this task.
What to do:
- Remove
flow-react-proptypesfrom thedevelopment.pluginssection inclient/.babelrc - Restart the development container
- Navigate through all app pages checking the browser console for errors
- Verify all tests pass with
docker compose run app yarn test - Revert previously applied workarounds: remove the
'no babel-plugin-flow-react-proptypes'flag fromInputTag.jsxand restore the original syntax inForm/utils.js
Impact: Stable development environment, without crashes caused by the plugin. Consistent behavior between development, testing and production.
Complexity: Low
Dependencies: None
Completion criteria:
- Plugin removed from
.babelrc - No errors related to
bpfrpt_proptype_*in browser console - All forms rendering correctly
- Tests passing
Summary: Update the testing framework to a version compatible with Node 20, eliminating known instabilities.
Context: The project uses Jest 26 with Node 20. Jest 26 was not designed for Node 20 and exhibits unstable behavior. Additionally, the plugin is disabled in tests, creating divergence between the development environment and CI — bugs that appear in the browser may not be caught by tests.
What to do:
- Update
jestandbabel-jestfrom^26.xto^29.xinclient/package.json - Update
@testing-library/reactto^14.x - Update
react-test-rendererto the same React version (required if D1 is executed) - Add
jest-environment-jsdomas an explicit dependency — it became a separate package in Jest 28+ - Check breaking changes between Jest 26 and 29 in the official documentation
- Run
yarn installandyarn testverifying all tests pass - Fix any breaks caused by the update
Impact: Reliable test suite compatible with Node 20. Solid foundation for adding new tests.
Complexity: Low/Medium
Dependencies: None
Completion criteria:
- Jest 29 installed
-
jest-environment-jsdomadded as explicit dependency - All existing tests passing
- No incompatibility warnings with Node 20
Summary: Configure minimum test coverage and mandatory thresholds, establishing a quality baseline.
Context: The project has moderate unit tests but no minimum coverage configuration. After updating Jest (A2), it is the ideal time to establish thresholds that prevent coverage regressions.
What to do:
- Configure
collectCoverageFrominjest.config.jswith appropriate globs (e.g.:app/**/*.{js,jsx}, excluding stories and mocks) - Define conservative initial minimum thresholds (e.g.: 50% statements/branches/functions/lines)
- Add
yarn test:coveragescript topackage.json - Integrate coverage verification in CI (GitHub Actions)
Impact: Quality baseline established, coverage regressions automatically detected in CI.
Complexity: Low
Dependencies: A2
Completion criteria:
-
collectCoverageFromconfigured - Thresholds defined and passing
-
yarn test:coveragescript working - CI verifying coverage
Summary: Replace deprecated Webpack 5 plugins and loaders with modern alternatives, and update Babel plugins with old names.
Context: The project uses Webpack 5 but still uses Webpack 4 era plugins that have been deprecated. Additionally, .babelrc uses @babel/plugin-proposal-* that were renamed to @babel/plugin-transform-* in Babel 7.22+, generating unnecessary warnings.
What to do:
- Replace
optimize-css-assets-webpack-pluginwithcss-minimizer-webpack-plugin - Replace
extract-css-chunks-webpack-pluginwithmini-css-extract-plugin - Replace
url-loaderandfile-loaderwith native Webpack 5 Asset Modules - Update
client/webpack.config.jswith new configurations - Rename in
.babelrc:@babel/plugin-proposal-class-properties→@babel/plugin-transform-class-properties@babel/plugin-proposal-private-methods→@babel/plugin-transform-private-methods@babel/plugin-proposal-private-property-in-object→@babel/plugin-transform-private-property-in-object
- Verify build and visual functionality of the app
Impact: More modern build, without deprecation warnings, lower risk of future incompatibilities.
Complexity: Medium
Dependencies: None
Completion criteria:
- Deprecated webpack plugins removed
- Babel plugins renamed
- Build working without deprecation warnings
- App functioning visually identical to before
Summary: Update linters to current versions, adopting new configuration formats.
Context: ESLint 9 completely changed the configuration format — from .eslintrc to eslint.config.js. Stylelint 13.7 is also very outdated (current is 16.x). Both should be updated together as complementary linting tools.
What to do:
- Update
eslintto^9.xinclient/package.json - Migrate
.eslintrctoeslint.config.jsfollowing the official migration guide - Update related ESLint plugins (airbnb config, etc.)
- Update
stylelintto^16.x - Migrate
.stylelintrcto the new configuration format - Verify and fix any new warnings/errors in both
Impact: Updated linting, better detection of code and style problems.
Complexity: Medium
Dependencies: None
Completion criteria:
- ESLint 9 installed and configured
- Stylelint 16 installed and configured
- No unexpected linting errors
Summary: Replace the axios library with native Node.js/browser fetch, eliminating a critical external dependency.
Context: The axios package was the target of a supply chain attack in March 2026 (versions 1.14.1 and 0.30.4 compromised with RAT attributed to a North Korean state actor). Although the project's current version (^1.15.0) was not directly affected, the incident highlights the risk of external dependencies for critical operations like HTTP requests. Node 20 and modern browsers already have native fetch with full support.
What to do:
- Map all axios usages in the project
- Create a custom fetch wrapper with the same interfaces used by axios in the project
- Gradually replace each usage — starting with the simplest components
- Remove axios from
package.jsonafter all replacements
Impact: Elimination of critical external dependency, bundle size reduction, supply chain risk elimination.
Complexity: Medium
Dependencies: None — but recommended after A1 and A2
Completion criteria:
- No references to axios in code
- All requests working with native fetch
- Tests passing
Summary: Update Storybook to the current version, making it usable for component development.
Context: Storybook 6.5 is EOL. The current version is 8.x with a completely new configuration API and native Webpack 5 builder. After architectural stabilization, Storybook will be essential for developing React components in isolation, without backend dependency.
What to do:
- Follow the official Storybook 6 → 8 migration guide
- Migrate configuration files
- Adopt the native Webpack 5 builder
- Verify all existing stories work
- Update broken stories
Impact: Storybook usable for component development and documentation.
Complexity: Medium
Dependencies: Recommended after B1 (webpack plugins updated)
Completion criteria:
- Storybook 8 running
- Existing stories working
- Webpack 5 builder configured
Summary: Remove unnecessary polyfills that increase bundle size without real benefit.
Context: The project already uses @babel/preset-env with useBuiltIns: 'usage' and core-js@3, which automatically manage polyfills based on browserslist. @babel/polyfill is deprecated and es5-shim is unnecessary for modern targets. Additionally, node-polyfill-webpack-plugin is present in webpack.config.js and may be unnecessary depending on actual Node API usage in client code.
What to do:
- Remove
@babel/polyfillandes5-shim/es5-shamfrompackage.jsonand webpack entry point - Verify
core-js@3is covering necessary polyfills - Investigate which Node.js APIs
node-polyfill-webpack-pluginis polyfilling - If there is no real usage, remove the plugin as well
- Test the app in target browsers
Impact: Smaller bundle, less unnecessary code.
Complexity: Low
Dependencies: None
Completion criteria:
- Polyfills removed
-
node-polyfill-webpack-pluginevaluated and removed if unnecessary - App working in target browsers
- Bundle size reduced
Summary: Consolidate and update Font Awesome packages to the current version.
Context: The project uses Font Awesome across 3 different packages with mixed versions: @fortawesome/fontawesome-svg-core ^1.2.22, @fortawesome/free-solid-svg-icons ^5.10.2, @fortawesome/react-fontawesome ^0.1.4, and still font-awesome ^4.7.0 (legacy CSS version). FA6 is the current version. Some icon names changed between v5 and v6.
What to do:
- Map all Font Awesome icon usages in the project
- Update all
@fortawesome/*packages to v6 - Remove
font-awesome ^4.7.0(legacy CSS version) - Fix icon names that changed between v5 and v6
- Visually verify all pages
Impact: Consolidated and updated icon dependency, removal of legacy CSS package.
Complexity: Low/Medium
Dependencies: None
Completion criteria:
- All
@fortawesome/*packages at v6 -
font-awesome ^4.7.0removed - All icons displaying correctly
Summary: Update the history library to the version compatible with React Router v6.
Context: history ^4.9.0 is legacy. React Router v6+ requires history v5. If there are plans to adopt React Router v6, this update will be necessary.
What to do:
- Check all
historyusages in the project - Evaluate whether there are plans for React Router v6 migration
- Update to
history ^5.xadjusting the API where necessary
Impact: Compatibility with React Router v6, updated dependency.
Complexity: Low/Medium
Dependencies: Evaluate together with D1 (React 18)
Completion criteria:
-
historyupdated to v5 - No navigation breaks
- Tests passing
Summary: Enable exact types by default in Flow, increasing type safety while the project still uses Flow.
Context: The .flowconfig has exact_by_default=false, which allows objects to accept extra undeclared properties. This reduces the effectiveness of type checking and can cause inconsistencies with generated PropTypes. Enabling exact_by_default=true will make types safer and more consistent.
What to do:
- Change
exact_by_default=falsetoexact_by_default=truein.flowconfig - Run
yarn flowto identify all introduced errors - Fix errors incrementally — start with the simplest components
- Verify tests continue passing
Impact: Safer and more consistent Flow types, reduction of silent bugs.
Complexity: Medium
Dependencies: Recommended after A1
Completion criteria:
-
exact_by_default=truein.flowconfig - No pending Flow errors
- Tests passing
Summary: Create JSON endpoints that return clean and structured data, preparing the foundation for architectural migration.
Context: Currently Rails serializes data directly in form helpers, mixing data with UI logic. For React to take responsibility for forms and lists, Rails needs to provide data via clean JSON endpoints. This is Phase 1 of the migration path defined by the audit.
What to do:
- Check which JSON endpoints already exist (
.jsonformat in routes) - Create or complete endpoints for: moments, strategies, medications, groups, meetings, categories, moods, users (lightweight), moment_templates, comments
- Ensure each endpoint returns only data — no UI flags, no pre-rendered HTML
- Document the contract of each endpoint
Impact: Clean and accessible data foundation for React. Prerequisite for all subsequent architectural tasks.
Complexity: Medium
Dependencies: None technical, but recommended after Category A
Completion criteria:
- JSON endpoints working for each domain
- Data returned without UI logic
- Documentation of each endpoint contract
Summary: Transfer form construction responsibility from Rails to React, eliminating form helpers.
Context: This is the highest impact and highest ROI task in the project. Currently 7 Rails helpers build complete form schemas: MomentsFormHelper, StrategiesFormHelper, MedicationsFormHelper, GroupsFormHelper, MeetingsFormHelper, CommentsFormHelper, and the base FormHelper. React receives these schemas and only renders. This violates MVC and creates N+1 queries in the database. CrisisPrevention and MomentTemplates are examples of the correct pattern that should be replicated here.
What to do:
- Create React schema components per domain:
MomentForm,StrategyForm,MedicationForm, etc. - Each component defines its own fields, types, labels, validations and UI flags
- Update Rails helpers to return only
{ action, record, associations }via JSON - Migrate one form at a time — start with the simplest (suggestion:
CategoryForm) - Gradually eliminate Rails form helpers
Impact: Eliminates N+1 queries, enables form development in Storybook, makes forms testable independently of Rails, removes critical coupling.
Complexity: High
Dependencies: C1
Completion criteria:
- Each form defined as an autonomous React component
- Rails passes only data — no field types, labels or validations
- Rails form helpers eliminated or drastically simplified
- Tests covering each FormSchema
Summary: Move Story presentation logic from Rails to React, eliminating pre-rendered HTML in props.
Context: The present_moment_or_strategy helper mixes data with presentation — generates HTML links with link_to, resolves viewer names with N+1 queries, calculates visibility labels. The storyBy.author field contains HTML generated by Rails — XSS vector and impossible to test. This pattern affects all index pages and the home page.
What to do:
- Simplify
present_moment_or_strategyto return only raw data - Move link generation to React
- Move viewer name resolution to React
- Move date formatting to React
- Move draft and visibility labels to React
- Eliminate pre-rendered HTML in props
Impact: Eliminates XSS vector, eliminates N+1 queries, makes Story testable, enables Storybook development.
Complexity: High
Dependencies: C1
Completion criteria:
- No pre-rendered HTML in Story props
- Rails returns only raw data for Story
- N+1 queries eliminated
- Tests covering presentation logic in React
Summary: Complete the Comments widget migration by transferring form construction and viewer resolution to React.
Context: Comments already has autonomous React behavior — submit and delete via axios. But the form is still built by Rails (CommentsFormHelper) and viewer names are resolved with database queries. It is a half-finished migration that needs to be completed.
What to do:
- Create comment form schema directly in the React component
- Replace viewer resolution in Rails with a
GET /viewersendpoint that returns[{id, name}] - Update the widget to fetch viewers via API
- Simplify
CommentsFormHelperuntil it can be eliminated
Impact: Fully autonomous Comments, N+1 queries eliminated, testable form.
Complexity: Medium
Dependencies: C1
Completion criteria:
- Form schema defined in React
- Viewers fetched via API
-
CommentsFormHelpereliminated - Tests covering the complete widget
Summary: Convert the Rails partial _dashboard_nav_mobile.html.erb to a React component, eliminating injected HTML in the Header.
Context: The Header receives a mobileOnly prop containing pre-rendered HTML from the _dashboard_nav_mobile partial (confirmed in header_helper.rb line 51). This breaks the React component model and prevents proper testing and styling. Affects all pages of the application.
What to do:
- Create
DashboardNavMobileReact component - Pass only necessary data (links, current user) as props
- Update Header to use the new component
- Remove ERB partial
Impact: Header completely in React, without injected HTML, testable and styleable.
Complexity: Medium
Dependencies: None technical
Completion criteria:
- ERB partial removed
- React component working in all screen sizes
- Header without
mobileOnlyprop
Summary: Replace multiple separate react_component calls on detail pages with a single React page component.
Context: The pages moments/show.html.erb and strategies/show.html.erb use multiple react_component calls interspersed with ERB (6 calls in moments/show, 4 in strategies/show). This divides the layout between two rendering engines, making it impossible to test and maintain the page as a unit.
What to do:
- Create
MomentShowandStrategyShowcomponents - Each component receives the complete record and renders all sub-components internally
- Replace multiple
react_componentcalls with a single call per page - Remove interspersed ERB
Impact: Detail pages completely in React, testable as a unit, layout controlled by React.
Complexity: High
Dependencies: C2, C3
Completion criteria:
- A single
react_componentcall per detail page - No ERB interspersed with React
- Layout working identically to current in
moments/showandstrategies/show
Summary: Update React to the current version, enabling Concurrent Features and maintaining ecosystem compatibility.
Context: React 17 is out of mainstream support. React 18 introduced Concurrent Mode and changed ReactDOM.render to createRoot. react-on-rails is fixed at version 12.0.1 (without ^), suggesting sensitivity to updates — verifying React 18 compatibility will be critical before proceeding.
What to do:
- Verify compatibility of
react-on-railsversion12.0.1with React 18 - Update
reactandreact-domto^18.x - Migrate
ReactDOM.rendertocreateRootwhere necessary - Update
react-test-rendererto the same React version - Update
@testing-library/reactto compatible version - Test the entire application
Impact: Current React, modern ecosystem compatibility, access to Concurrent Features.
Complexity: High
Dependencies: A1, A2
Completion criteria:
- React 18 installed
- Compatibility with
react-on-rails 12.0.1verified - No legacy
ReactDOM.renderusage - Tests passing
- App functioning completely
Summary: Update the charting library to the current version, which has a completely different API.
Context: chart.js 2.x is EOL. Version 4.x has a breaking API — all components using charts will need to be rewritten. It is the most labor-intensive migration in the dependency category.
What to do:
- Map all chart.js usages in the project
- Study API differences between v2 and v4
- Rewrite chart components using the new API
- Update
react-chartkickor replace with direct Chart.js usage
Impact: Current and maintained charting library.
Complexity: High
Dependencies: D1 recommended
Completion criteria:
- chart.js 4 installed
- All charts working correctly
- No usage of old API
Summary: Migrate the frontend from Flow to TypeScript, unifying the type system and eliminating all identified inconsistencies.
Context: This is the definitive solution for the project's typing problems. The maintainer has confirmed interest in this migration. With clean contracts and autonomous components (result of categories A, B and C), the migration will be natural and incremental. TypeScript eliminates the need for Flow + PropTypes + babel-plugin, unifying everything in a single consistent typing layer across all environments.
What to do:
- Configure TypeScript + Webpack + Babel for coexistence with Flow during migration
- Migrate file by file — starting with the simplest and most autonomous components
- Create TypeScript types for each Rails/React contract defined in previous tasks
- Gradually eliminate Flow as migration progresses
- Remove Flow and related configurations after complete migration (the
babel-plugin-flow-react-proptypeswill already have been removed in A1)
Impact: Unified, safe and consistent type system. Eliminates all current complexity of Flow + PropTypes. Dramatically improves development experience.
Complexity: Very High
Dependencies: All previous categories recommended
Completion criteria:
- No
.jsor.jsxfiles with Flow annotations - All files in
.tsor.tsx -
tsconfig.jsonconfigured - Flow and related configurations removed
- Tests passing with ts-jest
PHASE 1 — Stabilization (start here)
A1 → A2 → A3 → B5
PHASE 2 — Dependency Modernization
B1 → B2 → B3 → B4 → B7 → B8
PHASE 3 — Architectural Foundation
C0 → C1
PHASE 4 — Contracts (execute per domain)
C2 → C3 → C4 → C5 → C6
PHASE 5 — Modern Stack
D1 → D2
PHASE 6 — Long Term
E1
Before opening any PR:
- Code tested locally
- No new errors in browser console
- No new errors in container terminal
- Existing tests passing
- PR with single and clear scope
- Complete PR description with context and motivation
- Issue linked when applicable
- Screenshots when there are visual changes
Document created: April 2026
Last updated: April 2026