Skip to main content

react-mistakes

useState and useEffect mistakes

React's useState and useEffect are fundamental hooks that are used frequently in functional components. Here are some common mistakes that developers might make when using these hooks and how to avoid them:

State Updates Aren't Immediate

Key Points:

  • useState updates are asynchronous; the updated state is not available immediately after calling the setter function.
  • Attempting to use the state immediately after setting it can lead to outdated or incorrect data.

Code Sample:

import React, { useState } from 'react';

function Counter() {
const [count, setCount] = useState(0);

const handleClick = () => {
setCount(count + 1);
// Mistake: expecting the updated value immediately
console.log(count); // Will log the old value, not count + 1
};

return <button onClick={handleClick}>Increment</button>;
}

Solution:

  • Use useEffect to react to state updates, or the functional update form to access the previous state if the new state depends on the old one.

Conditional Rendering

Key Points:

  • Incorrect use of conditions inside useEffect can lead to missed updates or infinite loops.
  • Dependencies must be correctly specified to ensure the effect runs at the right time.

Code Sample:

import React, { useState, useEffect } from 'react';

function UserGreeting({ userId }) {
const [user, setUser] = useState(null);

useEffect(() => {
if (userId) {
// Mistake: not including dependencies for useEffect
fetchUser(userId).then(setUser);
}
}); // Missing dependency array

return user ? <p>Welcome back, {user.name}!</p> : <p>Loading...</p>;
}

Solution:

  • Always include a dependency array in useEffect.
  • Ensure that all variables used inside useEffect that may change over time are included in the dependency array.

Updating Object State

Key Points:

  • State updates using objects must handle merging old and new state manually, as useState does not automatically merge object updates.
  • Mutating state directly can lead to rendering issues and bugs.

Code Sample:

import React, { useState } from 'react';

function UserProfile() {
const [profile, setProfile] = useState({ name: 'John', age: 30 });

const updateAge = () => {
// Mistake: mutating the state directly
let newProfile = profile;
newProfile.age = 31;
setProfile(newProfile);
};

return (
<div>
<p>Name: {profile.name}</p>
<p>Age: {profile.age}</p>
<button onClick={updateAge}>Update Age</button>
</div>
);
}

Solution:

  • Use the spread operator or a function like Object.assign to create a new object for the state update.

Corrected Code Sample:

import React, { useState } from 'react';

function UserProfile() {
const [profile, setProfile] = useState({ name: 'John', age: 30 });

const updateAge = () => {
// Correct: creating a new object for the state update
setProfile(prevProfile => ({ ...prevProfile, age: prevProfile.age + 1 }));
};

return (
<div>
<p>Name: {profile.name}</p>
<p>Age: {profile.age}</p>
<button onClick={updateAge}>Update Age</button>
</div>
);
}

In this corrected code, we're using a functional update form with the spread operator to ensure that we're not mutating the state directly and that the new state is a new object with updated properties.

When using React's useState and useEffect hooks, developers can sometimes fall into certain traps or make mistakes that can lead to inefficiency or bugs in their applications. Here are three common mistakes related to these hooks, with explanations and code examples for each:

1. Object state instead of multiple smaller ones

Mistake: Using a single useState hook with a large object to handle multiple related states, rather than separate useState calls for each piece of state.

Problem: This can lead to unnecessary renders or complex update logic, as changing one property on the object will require creating a new object with all properties, which can cause components to re-render even if only one piece of state has changed.

Solution: Break up state into smaller, independent pieces unless they are closely tied together and always change at the same time.

Example:

// Mistake: Single large state object
const [userDetails, setUserDetails] = useState({
firstName: '',
lastName: '',
email: ''
});

// To update just firstName
setUserDetails(prevState => ({
...prevState,
firstName: 'John'
}));

// Better approach: Separate states for each field
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const [email, setEmail] = useState('');

// To update just firstName
setFirstName('John');

2. Information can be derived from state / props

Mistake: Storing information in state that can be derived from existing state or props.

Problem: It leads to redundant state management and potential for the state to get out of sync with the derived data.

Solution: Use useMemo, or calculate the derived state directly in the component render phase if the computation is not expensive.

Example:

// Mistake: Deriving state from existing state/props
const [price, setPrice] = useState(100);
const [tax, setTax] = useState(20);
const [total, setTotal] = useState(price + tax); // This can be derived

// Better approach: Compute derived data
const total = useMemo(() => price + tax, [price, tax]);
// Or just compute it directly without useMemo if the calculation is cheap
const total = price + tax;

3. Primitives vs non-primitives

Mistake: Not understanding the difference between primitive (e.g., string, number) and non-primitive (e.g., object, array) state updates.

Problem: React uses shallow comparison to determine whether to re-render. For primitives, it's straightforward, but for objects and arrays, a new reference is needed to trigger a re-render.

Solution: When updating non-primitive state, make sure to provide a new reference, typically by spreading into a new object or array.

Example:

// Mistake: Updating an array or object state without creating a new reference
const [items, setItems] = useState([{ id: 1, name: 'Item 1' }]);

// Incorrect update - this won't trigger a re-render
items.push({ id: 2, name: 'Item 2' });
setItems(items);

// Correct update - creating a new array reference
setItems(prevItems => [...prevItems, { id: 2, name: 'Item 2' }]);

Understanding these nuances and avoiding these common mistakes can greatly improve the performance and predictability of your React applications.

When working with React's useState and useEffect hooks, developers often encounter pitfalls related to the types of values they are managing, especially when dealing with primitives versus non-primitives, initializing state with objects, and using TypeScript. Here are some key points and code samples addressing these common mistakes.

Primitives vs Non-Primitives

Key Points:

  • Primitives (strings, numbers, booleans) are compared by value, whereas non-primitives (objects, arrays, functions) are compared by reference.
  • This can lead to issues in useEffect where a non-primitive dependency might change in reference but not in content, causing unnecessary re-renders or missed effect executions.

Code Sample:

import React, { useState, useEffect } from 'react';

function MyComponent({ options }) {
const [settings, setSettings] = useState({ ...options });

useEffect(() => {
// Mistake: 'options' is an object, and if its reference doesn't change
// even if content does, this effect will not re-run
setSettings({ ...options });
}, [options]); // Object 'options' as a dependency

// ...
}

Solution:

  • Ensure that objects are not recreated on every render if their content does not change.
  • Use a custom hook or a library like lodash's isEqual for deep comparison if necessary, or memoize objects with useMemo.

Initializing State with Object

Key Points:

  • Initializing state with objects can lead to issues if the initial object is mutated outside the component.
  • Always use a fresh instance or a constant that doesn't change.

Code Sample:

const defaultSettings = { theme: 'dark' };

function App() {
// Mistake: if 'defaultSettings' is mutated elsewhere, it will affect the state
const [settings, setSettings] = useState(defaultSettings);

// ...
}

Solution:

  • Use a function to initialize state to ensure a fresh instance is used.

Corrected Code Sample:

const defaultSettings = { theme: 'dark' };

function App() {
// Correct: using a function guarantees a fresh instance for each component instance
const [settings, setSettings] = useState(() => ({ ...defaultSettings }));

// ...
}

TypeScript Mistakes

Key Points:

  • When using TypeScript with useState, it's important to correctly type the state variable and updater function to avoid type errors.
  • TypeScript cannot infer the state type from an initializer function, so you need to explicitly define it.

Code Sample:

import React, { useState } from 'react';

interface User {
name: string;
age: number;
}

function UserProfile() {
// Mistake: TypeScript cannot infer type from initializer function
const [user, setUser] = useState(() => fetchUser()); // 'fetchUser' returns a 'User'

// ...
}

Solution:

  • Explicitly set the type of the state when initializing with a function or when the initial state could be undefined.

Corrected Code Sample:

import React, { useState } from 'react';

interface User {
name: string;
age: number;
}

function UserProfile() {
// Correct: explicitly type the state
const [user, setUser] = useState<User>(() => fetchUser());

// ...
}

In this corrected example, we provide a generic type parameter User to useState to ensure TypeScript knows the type of the user state. This way, TypeScript can correctly infer the type of the state and the updater function.

React's useState and useEffect hooks are powerful tools, but without proper usage, they can lead to issues in your application. Here are some common mistakes and how to address them:

1. Not using custom hooks

Mistake: Repeating logic across multiple components instead of abstracting it into custom hooks.

Problem: Leads to code duplication and makes it harder to maintain and update shared logic.

Solution: Create custom hooks to encapsulate shared stateful logic.

Example:

// Mistake: Duplicate state logic in multiple components
function ComponentA() {
const [isOnline, setIsOnline] = useState(null);

useEffect(() => {
const handleStatusChange = (status) => setIsOnline(status);
ChatAPI.subscribeToFriendStatus(handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(handleStatusChange);
};
}, []);

// ...
}

// Custom hook to encapsulate the shared logic
function useFriendStatus() {
const [isOnline, setIsOnline] = useState(null);

useEffect(() => {
const handleStatusChange = (status) => setIsOnline(status);
ChatAPI.subscribeToFriendStatus(handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(handleStatusChange);
};
}, []);

return isOnline;
}

// Using custom hook in components
function ComponentA() {
const isOnline = useFriendStatus();
// ...
}

2. Server & Client components

Mistake: Not differentiating logic that should run on the server-side during SSR (Server-Side Rendering) and client-side logic.

Problem: It can lead to memory leaks, and unexpected behavior, especially when hooks are used to handle side-effects or subscriptions.

Solution: Use platform-specific checks or lifecycle methods to ensure that the client-only code runs in the browser.

Example:

// Mistake: Using useEffect for server-side logic
useEffect(() => {
// This code should not run on the server
const subscription = dataSource.subscribe();
return () => {
subscription.unsubscribe();
};
}, []);

// Better approach: Check if we are in a browser environment
useEffect(() => {
if (typeof window !== "undefined") {
// This code should run only in the browser
const subscription = dataSource.subscribe();
return () => {
subscription.unsubscribe();
};
}
}, []);

3. Stale closure

Mistake: Capturing old state or props values inside useEffect due to closures.

Problem: useEffect captures the values from the render it was defined in, which can lead to "stale" values if the state or props change.

Solution: Use functional updates or the useRef hook to always have access to the latest state/props.

Example:

// Mistake: Stale closure capturing the initial count value of 0
function Counter() {
const [count, setCount] = useState(0);

useEffect(() => {
const intervalId = setInterval(() => {
console.log(count); // This will always log 0
}, 1000);

return () => clearInterval(intervalId);
}, []); // Empty dependency array means this effect runs once on mount

// ...
}

// Solution: Use functional updates
function Counter() {
const [count, setCount] = useState(0);

useEffect(() => {
const intervalId = setInterval(() => {
setCount(currentCount => {
console.log(currentCount);
return currentCount;
});
}, 1000);

return () => clearInterval(intervalId);
}, []);

// ...
}

4. Fetching in useEffect

Mistake: Performing fetch calls without handling cleanup, leading to setting state on unmounted components.

Problem: If the component unmounts before the fetch completes, you'll try to update the state on an unmounted component, which can cause memory leaks and errors.

Solution: Use a cleanup function or abort the fetch with an AbortController.

Example:

// Mistake: Not handling cleanup
useEffect(() => {
fetch('api/data')
.then(response => response.json())
.then(data => setData(data));

// No cleanup function here
}, []);

// Better approach: Abort fetch on cleanup
useEffect(() => {
const abortController = new AbortController();
const { signal } = abortController;

fetch('api/data', { signal })
.then(response => response.json())
.then(data => setData(data));

return () => abortController.abort(); // Cleanup function to abort the fetch
}, []);

Paying attention to these points can help you write more reliable and maintainable React components.

Initializing State with Object

Key Points:

  • Initializing state with an object can be tricky because updates to state must be handled carefully to avoid mutating the state directly.
  • React state updates may be asynchronous, so always use the functional update form when the new state depends on the old state.

Code Sample:

import React, { useState } from 'react';

function MyComponent() {
const [state, setState] = useState({ count: 0 });

const incrementCount = () => {
// Mistake: This doesn't merge the old state with the new state
setState({ count: state.count + 1 });

// Correct way: Use functional update form to ensure correct previous state is used
// setState(prevState => ({ ...prevState, count: prevState.count + 1 }));
};

return (
<div>
<p>Count: {state.count}</p>
<button onClick={incrementCount}>Increment</button>
</div>
);
}

TypeScript Mistakes

Key Points:

  • When using TypeScript, the state must be typed correctly to ensure you're working with the expected values.
  • TypeScript won't automatically infer the types when the state is set to null or undefined initially. You need to explicitly define the type.

Code Sample:

import React, { useState } from 'react';

interface StateType {
count: number;
text: string;
}

function MyComponent() {
// Mistake: TypeScript can't infer the correct type
// const [state, setState] = useState({ count: 0, text: 'hello' });

// Correct: Explicitly define the type of the state
const [state, setState] = useState<StateType>({ count: 0, text: 'hello' });

// ...
}

Not Using Custom Hooks

Key Points:

  • Repeated logic across multiple components for handling state can lead to code duplication.
  • Custom hooks are a great way to encapsulate and reuse stateful logic across components.

Code Sample:

// Mistake: Duplicating state logic in multiple components
function MyComponent() {
const [isToggled, setIsToggled] = useState(false);

const toggle = () => {
setIsToggled(prev => !prev);
};

// ...
}

function AnotherComponent() {
const [isToggled, setIsToggled] = useState(false);

const toggle = () => {
setIsToggled(prev => !prev);
};

// ...
}

Solution:

  • Create a custom hook to encapsulate the toggling logic.

Corrected Code Sample:

import React, { useState } from 'react';

// Custom hook
function useToggle(initialValue = false) {
const [value, setValue] = useState(initialValue);
const toggle = () => {
setValue(v => !v);
};
return [value, toggle];
}

function MyComponent() {
const [isToggled, toggle] = useToggle();

// ...
}

function AnotherComponent() {
const [isToggled, toggle] = useToggle();

// ...
}

In the corrected examples, useToggle is a custom hook that provides the toggling functionality, which can be reused in multiple components, preventing code duplication and making the components cleaner.

Let's delve deeper into each of these topics:

Server & Client components

Mistake: Failing to handle code that should only run on the client side, especially when using server-side rendering (SSR) with frameworks like Next.js.

Key Points:

  • Server-side code runs during the build process or on the server, not in the user's browser.
  • Client-side code runs in the user's browser and can interact with browser APIs.

Solution:

  • Use environment checks to ensure that client-specific code runs only in the browser.
  • Utilize the useEffect hook for client-side operations, as it only runs after the component mounts in the DOM.

Example:

// React component with SSR
function MyComponent() {
useEffect(() => {
// This code will only run in the client-side environment
if (typeof window !== 'undefined') {
// Initialize client-side libraries, listen to window resize, etc.
}
}, []);

// ...
}

Stale closure

Mistake: Not accounting for stale closures when state or props are used within useEffect or other asynchronous callbacks.

Key Points:

  • A closure captures the state and props from the render it was created in, leading to "stale" values if they change later.
  • This can lead to bugs where the effect uses an old state or prop value.

Solution:

  • Use functional updates when setting state to ensure the latest state is always used.
  • Use the useRef hook to keep a mutable reference to the latest value which doesn't get trapped in a closure.

Example:

function Timer() {
const [count, setCount] = useState(0);
const countRef = useRef(count);

// Synchronize the ref with the current count
useEffect(() => {
countRef.current = count;
}, [count]);

useEffect(() => {
const intervalId = setInterval(() => {
// Use the current value of the ref instead of the stale closure over count
setCount(countRef.current + 1);
}, 1000);

return () => clearInterval(intervalId);
}, []);

// ...
}

Fetching in useEffect

Mistake: Initiating fetch requests in useEffect without proper cleanup, which can lead to trying to update the state of an unmounted component.

Key Points:

  • If the component unmounts before the fetch request completes, attempts to set state will trigger React warnings/errors.
  • This can cause memory leaks if the effect creates subscriptions or performs other cleanup-sensitive operations.

Solution:

  • Use an AbortController to cancel fetch requests if the component unmounts before the request is complete.
  • Ensure that any subscriptions or listeners are properly cleaned up in the effect's cleanup function.

Example:

function DataFetcher() {
const [data, setData] = useState(null);

useEffect(() => {
const abortController = new AbortController();
const signal = abortController.signal;

async function fetchData() {
try {
const response = await fetch('/api/data', { signal });
const result = await response.json();
setData(result);
} catch (error) {
if (error.name !== 'AbortError') {
console.error('Fetch error:', error);
}
}
}

fetchData();

// Cleanup function to abort the fetch
return () => {
abortController.abort();
};
}, []);

// ...
}

Understanding these concepts and correctly implementing them can significantly improve the performance and reliability of your React application, especially in complex scenarios involving asynchronous operations, SSR, and dynamic state management.