DEV Community

Paradane
Paradane

Posted on

React 19 Unstable Features: Update or Ignore ESLint Rules?

The release of React 19 brings exciting new features and patterns, but it also creates immediate friction for development teams working with existing ESLint configurations. Many developers encounter warnings about deprecated or unstable APIs that were once acceptable, particularly around default props patterns that have been standard practice in React codebases for years. These warnings force teams to make difficult decisions: update existing code, ignore the warnings, or find a middle ground that balances modern best practices with practical development needs.

React's stability classification system has evolved, and what was once considered a stable feature may suddenly carry an 'unstable' label alongside expiration policies that create uncertainty. The ESLint framework struggles to keep pace with these rapid changes, often lagging behind React's release cycle or providing conflicting guidance about which rules are essential versus optional. Teams find themselves questioning whether they should adopt every warning as a hard requirement or treat some as suggestions until the ecosystem matures.

This challenge becomes particularly acute when managing large codebases where updating every component pattern would require significant refactoring effort. Understanding which React 19 ESLint rules represent genuine improvements versus transitional suggestions helps teams make informed decisions about their migration strategy. At Paradane, we've observed that the most successful React upgrades involve strategic rule adoption rather than wholesale configuration changes.

React's Stability Classification System

React’s team classifies APIs into three maturity levels: 'core', 'experimental', and 'deprecated'. Core APIs are considered stable and guaranteed not to break between minor releases. Experimental APIs are shipped behind a feature flag or with an explicit 'unstable' label in the documentation; they are subject to change or removal based on community feedback and internal testing. When you import or call an experimental API in a development build, React emits a console warning that includes the phrase 'unstable' and points you to the relevant RFC. ESLint plugins such as eslint-plugin-react pick up these warnings and surface them as lint errors, which is why you see messages about useOptimistic, useActionState, or the new useFormState hook in React 19.

The stability decision follows a predictable lifecycle. First, an idea is proposed in an RFC; if accepted, it lands in the main branch as an experimental export. After a predefined period—typically two to three minor releases—or once a sufficient adoption threshold is met, the API is promoted to core and the 'unstable' label is removed. If feedback reveals fundamental flaws, the API may be deprecated and eventually removed in a later major release. This expiration policy gives teams a clear window to experiment while providing a migration path.

Warnings appear only in development mode (process.env.NODE_ENV !== 'production') and are intensified when React’s StrictMode is enabled. In production builds the calls are stripped out, so the unstable APIs do not affect bundle size or runtime performance. Consequently, ESLint rules flagging unstable usage are helpful during development but can be safely disabled or adjusted for production‑only branches if a team deliberately opts into an experimental feature.

Understanding this classification helps you decide whether to treat an ESLint warning as a blocker or as a signal to monitor the feature’s stability timeline.

The Default Props Debate

As React moves toward the paradigms established in version 19, a significant friction point has emerged between modern linting requirements and legacy architectural patterns: the use of defaultProps. While the React team has signaled the deprecation of defaultProps for functional components in favor of ES6 default parameters, many enterprise codebases remain heavily reliant on them. This creates a dilemma for developers encountering React 19 ESLint rules that flag these patterns as outdated or potentially unstable.

Scenario-Based Analysis: To Refactor or To Ignore?

When managing React 19 ESLint rules, developers often face two distinct scenarios. The first is a greenfield project where modern React component patterns should dictate a move toward destructuring with default values:

// The modern standard
const UserProfile = ({ name = 'Guest', isAdmin = false }) => {
  return <div>{name}</div>;
};
Enter fullscreen mode Exit fullscreen mode

In this context, ignoring the ESLint warning is a mistake. Adhering to the new standard ensures long-term compatibility and cleaner TypeScript integration.

However, the second scenario involves legacy code handling. In large-scale applications, a component might be consumed by hundreds of child components or external modules. Rapidly refactoring defaultProps to ES6 defaults can introduce subtle bugs, especially if the component relies on specific lifecycle behaviors or if the props are being passed dynamically from older class-based components. In these instances, suppressing the ESLint rule for specific files is often a more pragmatic engineering decision than a rushed, error-prone refactor.

Balancing Technical Debt and Stability

The debate isn's just about syntax; it's about risk management. For teams navigating the transition to React 19, the goal shouldn't be immediate perfection, but controlled migration. If your ESLint configuration is triggering a flood of warnings due to defaultProps, consider using a tiered linting strategy. You might set the rule to warn rather than error for legacy directories, allowing you to maintain velocity while identifying the high-priority areas that require modernization during scheduled maintenance cycles.

Context Value Changes in React 19

React 19 continues to encourage the use of the Context API for sharing state and utilities across the component tree, but it also refines the patterns that are considered safe and performant. One notable shift is the stronger recommendation to avoid creating new objects or functions as the context value on every render. When a provider’s value changes reference equality, all consuming components re‑render, even if the actual data hasn’t changed. In React 18 this was a common source of unnecessary work; React 19’s compiler improvements make the cost more visible through stricter ESLint warnings.

Developers are now advised to memoize context values with useMemo (or useCallback for functions) when the value depends on props or state. For static values, a simple literal is fine, but for objects that contain multiple pieces of state, splitting the context into several smaller providers can reduce the surface area of changes. For example, instead of a single AppContext that holds { user, theme, locale }, you might have three separate contexts: UserContext, ThemeContext, and LocaleContext. Each provider then only updates when its specific slice changes, limiting re‑renders to the components that actually need the new data.

Another pattern gaining traction in React 19 is the use of defaultValue as undefined and relying on a wrapper component that throws an error if the context is used outside a provider. This helps catch misuse early in development, aligning with the stricter error boundaries introduced in the concurrent rendering improvements.

From an ESLint perspective, the default react/jsx-no-constructed-context-values rule flags inline object literals in the value prop of a Context.Provider. To accommodate the recommended memoization pattern, you can adjust the rule to allow values that are identifiers or calls to useMemo/useCallback. A typical configuration in .eslintrc.js might look like:

module.exports = {
  rules: {
    'react/jsx-no-constructed-context-values': ['error', { allow: ['useMemo', 'useCallback'] }]
  }
}
Enter fullscreen mode Exit fullscreen mode

Additionally, enabling react/jsx-no-bind helps prevent passing newly created functions directly as context values, reinforcing the memoization habit. By updating your ESLint setup to reflect these guidelines, you keep the warnings meaningful: they point out genuine performance risks while allowing the modern, safe patterns that React 19 encourages.

In summary, the evolution of context usage in React 19 centers on stabilizing the provider value, splitting concerns across multiple contexts, and configuring ESLint to distinguish between harmful inline constructions and legitimate, memoized values.

Modernizing Your React Codebase

When teams adopt React 19, a pragmatic upgrade roadmap helps turn "unstable" warnings into a structured improvement plan. Start with an audit: run ESLint across the project and collect all citations related to default props, context value changes, and other experimental patterns. Tag each warning by impact—critical for rendering performance, moderate for maintainability, or low for future‑proofing.

Step‑1Create a migration backlog. Add entries such as "replace defaultProps with ES6 defaults," "refactor Context providers to memoized values," and "enable new React 19 hooks." Prioritize items that affect component stability or cause unnecessary re‑renders. A simple spreadsheet or a project‑management tool can serve as the visual update roadmap, letting developers see the order of work and estimate effort.

Step‑2Adjust ESLint configuration. Update the .eslintrc.js to keep only the rules that align with your migration timeline. For example, you might temporarily disable the react/default-props-matching rule for legacy components, then re‑enable it after the refactor. A snippet like:

// .eslintrc.js
module.exports = {
  rules: {
    'react/react-in-jsx-scope': 'off',
    'react/default-props-matching': 'error',
    // Turn off for legacy components until migration
    'no-restricted-syntax': ['error', { message: 'Use ES6 default parameters', selector: 'ArrowFunctionExpression[params.length>0][params.0.type="RestElement"]' }]
  }
};
Enter fullscreen mode Exit fullscreen mode

Step‑3Integrate component testing. Every change that touches the Component API should be covered by unit or integration tests. Tools like Jest + React Testing Library let you verify that default props work as expected, that Context providers do not trigger spurious re‑renders, and that new hooks behave according to React 19 specifications. A typical test file might include:

import { render, screen } from '@testing-library/react';
import MyComponent from './MyComponent';

describe('MyComponent stability', () => {
  it('renders with ES6 defaults', () => {
    render(<MyComponent />);
    expect(screen.getByText('Default Title')).toBeInTheDocument();
  });
});
Enter fullscreen mode Exit fullscreen mode

Step‑4Iterate and monitor. After each batch of changes, run ESLint again and check test coverage. This feedback loop ensures you stay ahead of breaking changes while maintaining reliability. By coupling the update roadmap with systematic component testing, teams can adopt React 19 features confidently, keeping the codebase both modern and stable.

Team Collaboration Strategies

When a React 19 codebase spreads across multiple squads, divergent ESLint configurations quickly become a source of friction. One team may silence the react/no-unstable-context-value rule to adopt a new experimental context pattern, while another insists on the strict default configuration. The result is inconsistent lint output, merge conflicts, and a higher risk of unintentionally shipping unstable APIs.

1. Define a shared base config

Start by publishing a single ESLint configuration package (e.g., @your-org/eslint-config-react19) that every repository extends. This shared config should include:

{
  "extends": ["eslint:recommended", "plugin:react/recommended"],
  "plugins": ["react"],
  "rules": {
    "react/no-unstable-context-value": "error",
    "react/require-default-props": "warn",
    "react-hooks/rules-of-hooks": "error"
  }
}
Enter fullscreen mode Exit fullscreen mode

By pinning the version of this package in your monorepo (or via a private npm registry), you guarantee that all teams lint against the same baseline. When React 19 stabilizes a previously unstable feature, you simply bump the package version and run a single npm install across projects.

2. Layer team‑specific overrides

Some squads need temporary relaxations—for example, a feature‑flagged rollout that still uses an experimental API. Allow each team to add an overrides section in their local .eslintrc.json that references the shared config:

{
  "extends": ["@your-org/eslint-config-react19"],
  "overrides": [
    {
      "files": ["src/experimental/**"],
      "rules": {"react/no-unstable-context-value": "off"}
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

Document the purpose of every override in the code comment and set a deadline for its removal. This practice keeps exceptions visible and time‑boxed.

3. Communicate changes through a living style guide

Maintain a lightweight markdown file (e.g., ESLINT_GUIDELINES.md) in the root of the shared config repository. Include:

  • A changelog of rule adjustments tied to React 19 releases.
  • Rationale for each rule (why react/no-unstable-context-value is critical for performance).
  • A checklist for reviewers to confirm that any new override has an owner and an expiration date.

Encourage teams to reference this guide during pull‑request reviews. A brief checklist item such as “✅ ESLint overrides documented” adds accountability without slowing down the workflow.

4. Foster regular syncs

Schedule a quarterly “Lint Alignment” meeting where one engineer presents any upcoming React 19 feature migrations and the impact on lint rules. Use the meeting to:

  • Review pending overrides.
  • Decide whether a rule should be promoted to the shared config.
  • Capture feedback from QA on false‑positive lint warnings.

These syncs turn what could be a silent source of debt into a visible, collaborative effort.

5. Leverage CI to enforce consistency

Add a lint‑only job in your CI pipeline that runs against the shared config and fails on any error‑level violations. For overrides, require a PR label like eslint‑override and a signed‑off comment from the feature owner. This gate keeps the repository clean and surfaces accidental rule drift early.

By combining a centrally managed base configuration, transparent overrides, clear documentation, and regular team communication, you turn ESLint from a friction point into a unifying guardrail for safe React 19 adoption.


This section reflects best practices developed at Paradane for scaling React 19 development across distributed engineering teams.

Next Steps for React 19 Adoption

Implementing React 19 features while managing stability concerns begins with a clear audit of your current codebase. Start by running the official React 19 release notes and the ESLint rule set that ships with the new React preset (https://reactjs.org/blog/2024/04/10/react-19). Identify which unstable APIs your project imports—common examples include useTransition, useId, and the new useOptimistic hook. For each, check the stability status in the documentation; if a feature is marked “experimental” or “unstable,” consider whether the benefits outweigh the risk of future breaking changes.

Next, create a tiered ESLint configuration. Use a base config that enforces the stable rules, then add overrides for files or directories where you deliberately use unstable patterns. Document each override with a comment that references the specific React 19 feature and the justification (e.g., “use useTransition in this component because it reduces perceived latency for large lists”). This approach lets you keep the linting strict for production‑ready code while allowing experimentation elsewhere.

Paradane can assist your team in reviewing these configurations and in writing migration scripts that replace deprecated patterns with modern equivalents, ensuring a smoother upgrade path. Finally, schedule regular reviews—quarterly or after major releases—to reassess which unstable features are still relevant and to retire any that have become stable or obsolete.

Top comments (0)