Stop Breaking React: Why You Should Compute Values Instead of Storing Everything in State

Aviral SharmaAviral Sharma
6 min read

How I fixed infinite re-renders and discovered a simple rule that changed how I write React components


Last week, I spent three hours debugging what seemed like a simple dashboard feature. Users could filter data by date range, and I needed to show charts based on the filtered results. Sounds easy, right?

Wrong. My component was stuck in an infinite rendering loop, the browser was freezing, and I was questioning my life choices.

But this frustrating bug taught me one of the most important React performance lessons: if you can compute it, don't store it in state.

The Bug That Started It All

Picture this: I'm building a dashboard where users can select date ranges to filter their advertising data. The component needs to:

  1. Filter data based on the selected date range

  2. Group the filtered data by category

  3. Display charts with the grouped data

My first instinct was to manage everything with useState. After all, if data changes, it should be in state, right?

const Dashboard = ({ adsData }) => {
  const [dateRange, setDateRange] = useState({
    since: '2025-01-01', 
    until: '2025-06-04'
  });
  const [visibleData, setVisibleData] = useState([]);
  const [groupedData, setGroupedData] = useState([]);

  // Filter data when date range changes
  useEffect(() => {
    const filtered = adsData.filter(item => 
      item.date >= dateRange.since && item.date <= dateRange.until
    );
    setVisibleData(filtered);
  }, [adsData, dateRange]);

  // Group data when visible data changes
  useEffect(() => {
    const grouped = groupByCategory(visibleData);
    setGroupedData(grouped);
  }, [visibleData]);

  return (
    <div>
      <DatePicker 
        value={dateRange} 
        onChange={setDateRange} 
      />
      <Chart data={groupedData} />
    </div>
  );
};

This code looks reasonable, but it's a ticking time bomb. Here's what happens:

  1. User changes date range

  2. dateRange state updates

  3. First useEffect runs, updates visibleData

  4. Second useEffect runs, updates groupedData

  5. Component re-renders because state changed

  6. Sometimes this triggers the effects again

  7. Infinite loop nightmare begins

The Simple Fix That Changed Everything

The solution was embarrassingly simple: stop storing computed data in state.

const Dashboard = ({ adsData }) => {
  const [dateRange, setDateRange] = useState({
    since: '2025-01-01', 
    until: '2025-06-04'
  });

  // Compute filtered data directly
  const visibleData = useMemo(() => 
    adsData.filter(item => 
      item.date >= dateRange.since && item.date <= dateRange.until
    ), [adsData, dateRange]
  );

  // Compute grouped data from filtered data
  const groupedData = useMemo(() => 
    groupByCategory(visibleData), 
    [visibleData]
  );

  return (
    <div>
      <DatePicker 
        value={dateRange} 
        onChange={setDateRange} 
      />
      <Chart data={groupedData} />
    </div>
  );
};

No more useEffect. No more infinite loops. Just clean, predictable code.

The Golden Rule: Compute Don't Store

Here's the rule that saved my sanity: If data can be calculated from existing state or props, calculate it directly instead of storing it in state.

Think of it like this: your component's state should only contain the "source of truth" - the core data that can't be derived from anything else. Everything else should be computed.

Real-World Examples to Drive This Home

Let me show you several scenarios where this principle applies:

Example 1: Shopping Cart

❌ Bad: Storing computed totals in state

const ShoppingCart = ({ items }) => {
  const [cartItems, setCartItems] = useState(items);
  const [subtotal, setSubtotal] = useState(0);
  const [tax, setTax] = useState(0);
  const [total, setTotal] = useState(0);

  useEffect(() => {
    const newSubtotal = cartItems.reduce((sum, item) => 
      sum + (item.price * item.quantity), 0
    );
    setSubtotal(newSubtotal);
    setTax(newSubtotal * 0.1);
    setTotal(newSubtotal * 1.1);
  }, [cartItems]);

  // Rest of component...
};

✅ Good: Computing totals directly

const ShoppingCart = ({ items }) => {
  const [cartItems, setCartItems] = useState(items);

  const subtotal = useMemo(() => 
    cartItems.reduce((sum, item) => 
      sum + (item.price * item.quantity), 0
    ), [cartItems]
  );

  const tax = subtotal * 0.1;
  const total = subtotal + tax;

  // Rest of component...
};

Example 2: Search and Filter

❌ Bad: Multiple state variables for derived data

const ProductList = ({ products }) => {
  const [searchTerm, setSearchTerm] = useState('');
  const [selectedCategory, setSelectedCategory] = useState('all');
  const [filteredProducts, setFilteredProducts] = useState(products);
  const [searchResults, setSearchResults] = useState(products);

  useEffect(() => {
    let filtered = products;
    if (selectedCategory !== 'all') {
      filtered = filtered.filter(p => p.category === selectedCategory);
    }
    setFilteredProducts(filtered);
  }, [products, selectedCategory]);

  useEffect(() => {
    const searched = filteredProducts.filter(p => 
      p.name.toLowerCase().includes(searchTerm.toLowerCase())
    );
    setSearchResults(searched);
  }, [filteredProducts, searchTerm]);
};

✅ Good: One computation chain

const ProductList = ({ products }) => {
  const [searchTerm, setSearchTerm] = useState('');
  const [selectedCategory, setSelectedCategory] = useState('all');

  const displayedProducts = useMemo(() => {
    return products
      .filter(p => selectedCategory === 'all' || p.category === selectedCategory)
      .filter(p => p.name.toLowerCase().includes(searchTerm.toLowerCase()));
  }, [products, selectedCategory, searchTerm]);
};

Example 3: Form Validation

❌ Bad: Storing validation results in state

const SignupForm = () => {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [confirmPassword, setConfirmPassword] = useState('');
  const [isEmailValid, setIsEmailValid] = useState(false);
  const [isPasswordValid, setIsPasswordValid] = useState(false);
  const [doPasswordsMatch, setDoPasswordsMatch] = useState(false);
  const [isFormValid, setIsFormValid] = useState(false);

  useEffect(() => {
    setIsEmailValid(email.includes('@') && email.includes('.'));
  }, [email]);

  useEffect(() => {
    setIsPasswordValid(password.length >= 8);
  }, [password]);

  useEffect(() => {
    setDoPasswordsMatch(password === confirmPassword);
  }, [password, confirmPassword]);

  useEffect(() => {
    setIsFormValid(isEmailValid && isPasswordValid && doPasswordsMatch);
  }, [isEmailValid, isPasswordValid, doPasswordsMatch]);
};

✅ Good: Computing validation on the fly

const SignupForm = () => {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [confirmPassword, setConfirmPassword] = useState('');

  const isEmailValid = email.includes('@') && email.includes('.');
  const isPasswordValid = password.length >= 8;
  const doPasswordsMatch = password === confirmPassword;
  const isFormValid = isEmailValid && isPasswordValid && doPasswordsMatch;
};

When to Use What: A Simple Decision Tree

Here's how I decide what approach to use:

Use useState when:

  • Data comes from user input (form fields, toggles, selections)

  • Data is fetched from an API

  • Data represents the "source of truth" that can't be derived

Use useMemo when:

  • Calculation is expensive (sorting large arrays, complex math)

  • Data is derived from state/props and used in multiple places

  • You want to prevent unnecessary re-calculations

Use regular variables when:

  • Calculation is simple and cheap

  • Result is only used once

  • No performance concerns

The Performance Win

This approach doesn't just prevent bugs - it actually improves performance:

  1. Fewer re-renders: No cascading state updates

  2. Predictable updates: Clear dependency tracking with useMemo

  3. Less memory: No extra state variables storing derived data

  4. Easier debugging: Simpler component lifecycle

My New Mental Model

I now think of React components like this:

  • State = The essential data that defines what the component shows

  • Computed values = Everything else that can be calculated from state

  • Effects = Side effects like API calls, not data transformations

This mental shift has made my components more reliable and easier to understand.

The Bottom Line

Next time you reach for useState to store some data, ask yourself: "Can I calculate this from existing state or props?"

If yes, compute it instead of storing it. Your future self (and your users) will thank you.

The infinite re-render bug that frustrated me for hours taught me one of the most valuable React lessons: the best state is the state you don't have to manage.


What's your go-to pattern for handling derived data in React? Have you run into similar infinite loop situations? I'd love to hear about your experiences in the comments!

1
Subscribe to my newsletter

Read articles from Aviral Sharma directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Aviral Sharma
Aviral Sharma

Driven 3rd year BTech (IT) student passionate about front-end development. Eager to leverage skills in React, Redux, JavaScript, Next.js, and Tailwind CSS to contribute to a dynamic team.