I am working on a client-side application using React and Firebase. This week, I paired with another team member to add a new feature allowing users to submit a list name and create a new list in the database.
Just imagine you have a form element inside a component that receives existing lists' data as a prop from its parent and renders all the user's existing lists.
Here's a simplified example:
import './Home.css';
export function Home({ data }) {
const [newList, setNewList] = useState('');
return (
<div className="Home">
<form>
<label htmlFor="new list name">Create a new list</label>
<input
type="text"
id="new list name"
value={newList}
/>
<button type="submit">Create list</button>
</form>
<ul>
{data.map((list, i) => <SingleList key={i} name={list.name} />)}
</ul>
</div>
);
}
When users submit the form, you want to show a message confirming whether a new list was or was not created. How could you check that the list was successfully created using the data you receive via prop?
My first attempt was to use useEffect.
Understanding useEffect in React
First, a bit of context on the useEffect` React Hook. As described in the React docs, _
useEffect`` is a React Hook that lets you synchronize a component with an external system_. It accepts two arguments:
setup
: A function that React will run when your component is added to the DOM and after every re-render with changed dependencies.optional
dependencies
: A list of the reactive values referenced inside the setup function. The setup code will execute if at least one of the provided dependencies has changed since the previous run. If you omit this argument, the effect will re-run after every re-render of the component. And passing an empty array will make the effect to run only after the initial render. See examples here
Here is a complete guide to the useEffect with more information about its correct usage.
Why should I NOT use useEffect
Coming back to my example, I wanted to check if the last element in my lists data matches the new list name whenever the data
value changes:
import './Home.css';
export function Home({ data }) {
const [newList, setNewList] = useState('');
useEffect(() => {
const lastAddedItem = data[data.length - 1];
if (lastAddedItem?.name === newList) {
// Confirm the list was added
} else {
// Display an error message
}
}, [data, newList]);
// ...
}
However, after more reading, I found out that using useEffect here might not be the best approach.
Effects are a good choice when you need to update your component depending on external elements. But you should not use them for handling user events or for transforming data for rendering.
Instead, apply updates to the component state based on props or state changes at the top level of your components, where the code will automatically re-run when props or state change.
This was the case for my example, I wanted to update some state depending on a prop changes. To adopt a React pattern, I should base my approach on storing information from previous renders.
Based on that, something like this would be a better solution for my example:
const [prevData, setPrevData] = useState(data);
if (prevData.length !== data.length) {
// do something and then set the prevData to the new data
setPrevData(data);
}
If you find this interesting, there are a few more scenarios well detailed in the React docs where you might not need an effect and you can achieve more readable, faster, and less error-prone code by removing them.
Thanks for reading!
Top comments (5)
This is where memoization is useful. Memoization is the concept of keeping memory of a function return value to avoid repeated calls to functions that process the same input.
react.dev/reference/react/useMemo
On the initial render, useMemo returns the result of calling the callback. During next renders, it will either return an already stored value from the last render if the dependencies havenβt changed, or call the callback again and return the result.
Be aware that proper structure of component architecture would have got you to handle that logic from the parent component instead.
For the possibly interested I ported the "Storing information from previous renders" for
@preact/signals-react
:Notable differences to React:
trend
is declared withuseComputed
as it is derived statetrend
signal cannot be used as JSX conditional so workaround via extra variableSince you already have a parent component passing "data" down to your Home component, you can also pass an "onSubmit" function as a prop that you add to the form element, and remove all the internal state from Home.
The parent component handles managing the state, including updating the list and displaying a success message, and the child becomes a "dumb" component whose sole responsibility is displaying whatever data it is given.
Totally agree here !
Great article! π₯