In ReactJs & React-Native we often find a bad use of components’ state. This is an issue that takes special relevance as applications scale up and get more complex (e.g. through the use of nested components) and it can end up in either a compromised performance (damaging the user’s experience), a harsh dev experience when it comes to maintain our code or even a buggy application plagued with unexpected behaviours.
When this happens it is also common for trivial problems to take more time to be fixed than they should or for particular solutions to trigger bugs at another corner of our app.
Today we will use a simple -but real- error scenario to show what a bad implementation of React Components' state can get us and how to make it work as expected. In this case we are using React Hooks for state management.
Particular situation
In one of our components, formed by a basic form and an attached logic that sends its data to an API, we detected an inconsistency between the inputted data and the one that our service was finally receiving.
It turned out to be an issue allocated within the logic attached to the component’s state changes. To be more specific, a state variable was being used to save the form values and its content was being sent inside a request immediately after its update (or the attempt to update its value, as we will see).
This is how React Components’ lifecycle was not being followed: when we update state variables React needs some time to re-render the whole component and its children (unless we tell it not to do so) with every change. That’s why we cannot make use of our state variables until this process is complete.
💡 Going a little further, bear in mind that when we instruct a state change in a component, React enqueues this update (along with any other that we might have requested), applies it in the Virtual DOM and finally (through a process called Reconciliation) transmits it to the DOM so we can see the update in our app. Definitely far more complex than just assigning a new value to a variable.
For more information about components state and re-renders, feel free to consult the docs!
Let me show our code at this point:
import { useState } from "react";
import TestApi from "services"
const MyComponent = (): ReactElement => {
const [statefulData, setStatefulData] = useState<String>("");
const handleClick = async (newData: String) => {
// The problem here is that React needs some time to re-render our
// component everytime statefulData is updated (in this case through a
// hook). For this reason, statefulData is not updated by the time
// we call TestApi.postData (its value will be `''`), so this
// handler needs a fix.
setStatefulData(newData);
await TestApi.postData(statefulData);
}
return (
<button onClick={() => handleClick("New data")}>
Click me!
</button>
)
}
Possible ways out
I believe that there are two paths to have a scenario like this one solved. The correct one depends on what you need to do with your state value once it gets an update.
1. Removing the state variable
If we are not after re-renders in our component every time the form receives any changes then there is no need for a state variable and we can directly consume our API using the form’s inner data.
import TestApi from "services"
const MyImprovedComponent = (): ReactElement => {
const handleClick = async (newData: String) => {
// If there is no need for a re-render (and therefore for a state
// variable), a possible solution is to avoid the use of the hook
// and simply use the value that we receive from params.
await TestApi.postData(newData);
}
return (
<button onClick={() => handleClick("New data")}>
Click me!
</button>
)
}
2. Attaching logic to state changes
If we do want to re-render our component everytime our variable is updated (let’s say we want to show its content on screen) then we need a state variable and, what’s more, we need to add a useEffect hook that gets triggered when our variable is updated to handle this event.
import { useState } from "react";
import TestApi from "services"
const MyImprovedStatefulComponent = (): ReactElement => {
const [statefulData, setStatefulData] = useState<String>("");
// If we need a state variable we have to attach our logic to its updates
// through the useEffect hook This way we will be consuming TestApi only
// when statefulData has been updated.
useEffect(() => {
// An extra validation here since this callback is not only
// triggered when statefulData but also when mounting the component.
// For more information check the docs!
if (!!statefulData) {
await TestApi.postData(statefulData);
}
}, [statefulData])
const handleClick = async (newData: String) => {
setStatefulData(newData);
}
return (
<button onClick={() => handleClick("New data")}>
Click me!
<div>{statefulData}</div>
</button>
)
}
Concluding remarks
The problem solved here was a consequence of bad components’ state management. As React lifecycle stages were not respected, our component experienced a lag between the data stored in its state and the expected result of an enqueued state update attached to the user’s interaction.
In conclusion, the use of good practices and the components’ adjustment to the official React documentation is crucial. Correct state management, components’ atomization after SRP (Single Responsibility Principle, centralizing logic after DRY (Don't Repeat Yourself) decoupling from external components and the achievement of a highly cohesive internal logic are practices that minimize problems' solving time, lower error rate and allow for stability and scalability in our applications.
Picture taken from the official React docs.
Top comments (0)