- Published on
The ultimate guide to mastering state in React and saying goodbye to inconsistencies and bugs đ !
- Authors
- Name
- CĂŠdric RIBALTA

Why Reducing State in Your React Components Can Prevent Major Bugs đ
As a developer, youâve likely reviewed a Pull Request (PR) or a Merge Request (MR) and noticed a React component with too many contradictory state variables. This common issue complicates application logic and increases the likelihood of bugs. Letâs explore why itâs crucial to reduce the number of states in your React components and how to achieve this.
Keep It Simple, Itâs a Win đŻ
The React documentation warns against having too many state variables. Each piece of state is a âmoving part.â The more âmoving partsâ you have, the more complex your application logic becomes⌠and complexity is a breeding ground for bugs.
Imagine a simple example:
const [isEmpty, setIsEmpty] = useState(true)
const [isTyping, setIsTyping] = useState(false)
const [isSubmitting, setIsSubmitting] = useState(false)
const [isSuccess, setIsSuccess] = useState(false)
const [isError, setIsError] = useState(false)
Looking at this code snippet, you quickly see that the component state could become inconsistent. Imagine that isTyping
and isSubmitting
are both true
. This paradox would be impossible to handle in the user interface, as the component couldnât be both in âtypingâ and âsubmittingâ modes simultaneously.
Understanding the Difference Between Imperative and Declarative UI đ¤
In a traditional application, each user interaction is often thought of as a direct manipulation: enabling, disabling, showing, or hiding elements. This is what we call an imperative approach. For example, imagine the user interacting with a form:
- When the input field contains text, the "Submit" button becomes enabled.
- When "Submit" is clicked, the form becomes disabled, and a spinner appears.
- If the network request succeeds, the form disappears, and a thank-you message appears.
- If the request fails, an error message appears, and the form becomes active again for another attempt.
In imperative programming, you must give specific instructions for each step of this interaction. This quickly becomes hard to manage, especially in complex applications.
React, however, adopts a declarative approach. Instead of manipulating each component directly, you declare the desired final state. React handles the necessary transitions to update the UI. Itâs like telling a taxi driver your destination instead of dictating every turn. By adopting this approach, you make your code clearer and easier to maintain.
And if your components are pure (with no side effects), you can be sure that the component will always display the right state, whatever the user's interactions.
The Risks of Contradictory States â ď¸
When duplicating or multiplying state variables for the same visual behavior, you expose yourself to several problems:
- Inconsistent state: Variables can conflict.
- Difficult maintenance: The more state variables there are, the harder the component is to read and maintain.
- Hard-to-detect bugs: Unsynced states can lead to unexpected behaviors.
Managing State Changes Based on User and Machine Inputs âď¸
In React, state changes can be triggered by two types of inputs:
- User inputs: such as typing text or clicking a button.
- Machine inputs: like a network response or the end of a timeout.
For example, letâs look at how each action changes the state in a form component:
- Text input** (human input): as the user types, the state changes from
isEmpty
toisTyping
. - Clicking on âSend â** (human input): this triggers the switch to the
isSubmitting
state. - Network response successful** (machine input): the state changes to
success
, and the thank-you message is displayed. - Network response failed** (machine input): status changes to
error
, and an error message is displayed.
These steps allow you to structure changes in a clear and maintainable way. With this method, you can anticipate all possible visual and functional "steps" of the component before even creating the underlying logic, identifying all possible states: Empty
, Typing
, Submitting
, Success
, and Error
.
How to Effectively Manage State in Your Components đĄ
To tackle these issues, itâs essential to minimize and centralize state. Here are some strategies from the React documentation:
Eliminate Derived States
If a state variable can be calculated from another, it probably isnât necessary. For instance,isEmpty
could be determined byanswer.length === 0
instead of having a separate state variable.Group Related States
Use a single state variable to represent multiple interdependent visual states. For example, instead of five boolean variables (isTyping
,isSubmitting
,isSuccess
, etc.), create astatus
variable with values like'typing'
,'submitting'
, or'success'
:
const [status, setStatus] = useState('typing') // 'typing', 'submitting', 'success'
- Avoid State Duplication
State duplications often lead to inconsistencies. If information is already available through another variable, avoid creating new state. For example,isError
could be replaced witherror !== null
.
Before/After Refactoring Example â¨
Letâs look at a complete example to illustrate state refactoring in a component:
Before:
const [answer, setAnswer] = useState('')
const [isEmpty, setIsEmpty] = useState(true)
const [isTyping, setIsTyping] = useState(false)
const [isSubmitting, setIsSubmitting] = useState(false)
const [isSuccess, setIsSuccess] = useState(false)
const [isError, setIsError] = useState(false)
After refactoring :
const [answer, setAnswer] = useState('')
const [status, setStatus] = useState('typing') // 'typing', 'submitting', 'success'
const [error, setError] = useState(null)
This refactoring reduces the state to the essentials and makes the code much more readable. You can be sure that these three variables are necessary for the componentâs functionality, as each is directly linked to the componentâs visual behaviors.
Next Step: Extracting State Logic into a Reducer đ ď¸
Once you have simplified and centralized the state in your component, the next step is to extract this state logic into a reducer. Using a reducer helps to structure and organize state changes more clearly, especially when the application grows more complex and state transitions multiply.
In React, useReducer
is similar to useState
, but it is particularly suited for managing multiple interdependent state variables. It uses a reducer function, which takes the current state and an action as input and returns the new state.
Imagine our form, where possible states are typing
, submitting
, success
, and error
. Hereâs a simple example of a reducer to manage these state transitions.
import { useReducer } from 'react'
const initialState = {
answer: '',
status: 'empty', // 'typing', 'submitting', 'success', 'error'
error: null,
}
function formReducer(state, action) {
switch (action.type) {
case 'TYPING':
return { ...state, status: 'typing', answer: action.payload }
case 'SUBMITTING':
return { ...state, status: 'submitting' }
case 'SUCCESS':
return { ...state, status: 'success', answer: '' }
case 'ERROR':
return { ...state, status: 'error', error: action.payload }
default:
return state
}
}
Then you can use this reducer in your component to reset the state and manage transitions :
const [state, dispatch] = useReducer(formReducer, initialState)
// Exemple dâutilisation dans un gestionnaire dâĂŠvĂŠnements
function handleSubmit() {
dispatch({ type: 'SUBMITTING' })
// Simulation dâune requĂŞte rĂŠseau
fakeApiCall()
.then(() => dispatch({ type: 'SUCCESS' }))
.catch((error) => dispatch({ type: 'ERROR', payload: error.message }))
}
Why Use a Reducer? đ
Using a reducer, you gain readability and centralization. State transition logic is clearly defined in a single function, making transitions predictable. This is especially useful for avoiding contradictory states, as each action is explicitly managed, simplifying debugging and enhancing maintainability.
This approach also offers better scalability. If new states or transitions are added to the application, you can integrate them directly into the reducer without risking fragmented state management across multiple functions or hooks.
By applying this technique, you bring the state management of your component to a level of clarity and robustness thatâs especially valuable for larger applications and collaborative projects. đ
In Conclusion
Reducing the number of state variables in your React components can transform your code into a cleaner, more readable, and less error-prone component. The key is to keep it simple and only retain what is essential. By following these principles, you improve not only your applicationâs reliability but also make life easier for those who will read and maintain your code. â¨