# Stop Using useEffect for Derived State in React

If your component stores `price`, `quantity`, and `total` separately, it stores a mathematical relationship as three independently updateable values.

Every change to the inputs must then keep the result synchronized.

For simple calculations, React already provides the answer: calculate during render. When those calculations become a shared network of business rules, you can move the relationships into a reactive model.

This article demonstrates both approaches, using [`.me`](https://github.com/neurons-me/.me), the open-source declarative kernel I’m building.

## 1\. The Anti-Pattern: Syncing Derived State with `useEffect`

Consider this component:

```jsx
import { useEffect, useState } from "react";

function OrderTotal({ price, quantity }) {
  const [total, setTotal] = useState(0);

  useEffect(() => {
    setTotal(price * quantity);
  }, [price, quantity]);

  return <output>{total}</output>;
}
```

The dependency array is correct. But when an input changes, the component renders with the previous total. The Effect updates state afterward, causing another render.

Now omit `quantity`:

```jsx
useEffect(() => {
  setTotal(price * quantity);
}, [price]); // Missing quantity
```

Changing quantity alone no longer updates the total. The exhaustive-dependencies lint rule helps catch this mistake, but the redundant state was unnecessary in the first place.

The first example introduces an extra update cycle. The second adds a missing-dependency bug. Neither requires an asynchronous race condition to produce an incorrect intermediate result.

## 2\. The Idiomatic React Fix: Calculate During Render

Delete the redundant state and Effect:

```jsx
function OrderTotal({ price, quantity }) {
  const total = price * quantity;

  return <output>{total}</output>;
}
```

For this example, stop here. You do not need another library.

React’s documentation recommends calculating values from existing props or state during rendering. For expensive calculations, memoization can help avoid repeating work when inputs have not changed. [React: You Might Not Need an Effect](https://react.dev/learn/you-might-not-need-an-effect)

### When the Calculation Becomes a Shared Model

Suppose checkout, invoice previews, and reporting screens all use these relationships:

```text
price × quantity → subtotal
subtotal − discount → taxable amount
taxable amount × tax rate → tax
taxable amount + tax → total
```

You can express them with shared functions, selectors, or an existing state manager. React does not prevent that.

A dependency graph offers another approach: declare the relationships in a model that maintains derived values independently of component rendering.

## 3\. Modeling Relationships with `.me`

`.me` lets you declare facts at paths and attach rules to those paths.

Install it:

```bash
npm install this.me
```

Declare two inputs and their relationship:

```javascript
import Me from "this.me";

const me = Me();

me.order.price(100);
me.order.quantity(5);

me.order["="]("total", "price * quantity");

console.log(me("order.total")); // 500
```

The formula belongs to `order`, so `price` and `quantity` resolve in that context. The kernel records those dependencies.

Update an input:

```javascript
me.order.quantity(7);

console.log(me("order.total")); // 700
```

There is no separate `setTotal` call.

### From One Formula to a Dependency Chain

The same mechanism expresses the invoice model:

```javascript
const invoice = Me();

invoice.order.price(100);
invoice.order.quantity(5);
invoice.order.discount(50);
invoice.order.taxRate(0.1);

invoice.order["="]("subtotal", "price * quantity");
invoice.order["="]("taxable", "subtotal - discount");
invoice.order["="]("tax", "taxable * taxRate");
invoice.order["="]("total", "taxable + tax");

console.log(invoice("order.total")); // 495

invoice.order.quantity(7);

console.log(invoice("order.total")); // 715
```

The dependency chain belongs to the model. Components consume its results.

These are illustrative arithmetic rules; production billing also needs explicit currency and rounding rules. Formula strings use `.me`’s expression language, not arbitrary JavaScript.

See the [TypeScript README](https://github.com/neurons-me/.me/blob/main/Typescript/README.md) for supported syntax and operators.

## 4\. Connecting the Model to React

Updating an external model does not automatically rerender React. The model must notify its consumers.

Here is a small adapter for the original `price × quantity` example:

```javascript
// orderStore.js
import Me from "this.me";

const me = Me();
const listeners = new Set();

me.order.price(100);
me.order.quantity(5);
me.order["="]("total", "price * quantity");

export const orderStore = {
  subscribe(listener) {
    listeners.add(listener);

    return () => listeners.delete(listener);
  },

  getTotal() {
    return me("order.total");
  },

  setQuantity(quantity) {
    me.order.quantity(quantity);

    for (const listener of listeners) {
      listener();
    }
  },

  explainTotal() {
    return me.explain("order.total");
  },
};
```

`subscribe` belongs to this adapter. It is not a `.me` method.

React connects through `useSyncExternalStore`:

```jsx
import { useSyncExternalStore } from "react";
import { orderStore } from "./orderStore.js";

export default function OrderSummary() {
  const total = useSyncExternalStore(
    orderStore.subscribe,
    orderStore.getTotal
  );

  return (
    <section>
      <output>Total: {total}</output>

      <button onClick={() => orderStore.setQuantity(7)}>
        Set quantity to 7
      </button>
    </section>
  );
}
```

The responsibilities are explicit:

*   `.me` maintains the calculation.
    
*   The adapter notifies subscribers after a mutation.
    
*   React reads the snapshot and updates the UI when its value changes.
    

The snapshot here is a number, so unchanged reads produce a stable value. [React: `useSyncExternalStore`](https://react.dev/reference/react/useSyncExternalStore)

This is a client-side example. All writes must go through the adapter’s notifying methods; bypassing them would leave React uninformed. Server rendering requires an appropriate store lifecycle and server snapshot.

## 5\. Why Is the Total 700?

Now inspect the calculation:

```javascript
orderStore.setQuantity(7);

console.log(orderStore.explainTotal());
```

This excerpt comes from an actual run against my local `this.me` 4.2.1 build. Timestamps and additional input metadata are omitted:

```json
{
  "path": "order.total",
  "value": 700,
  "expr": "price * quantity",
  "derivation": {
    "expression": "price * quantity",
    "inputs": [
      {
        "label": "price",
        "path": "order.price",
        "value": 100
      },
      {
        "label": "quantity",
        "path": "order.quantity",
        "value": 7
      }
    ]
  },
  "meta": {
    "dependsOn": [
      "order.price",
      "order.quantity"
    ],
    "k": 1,
    "recomputed": ["order.total"],
    "changed": ["order.total"],
    "sourcePath": "order.quantity"
  }
}
```

The trace connects the result to the expression and resolved inputs. It also identifies the triggering path and the derived values reevaluated during propagation.

You do not have to add logging statements around this formula to inspect those relationships.

Logs remain useful for requests, failures, and external events. `explain()` provides a different view: the computation represented by the model. It does not, by itself, reconstruct every historical state or establish that an input was factually correct.

### What `O(k)` Propagation Means

In this example, `k = 1`: one derived value was reevaluated.

Dependency-driven propagation follows the affected relationships instead of deliberately evaluating every derivation in the graph. If only a small part of a large model depends on an input, that distinction matters.

`O(k)` does not mean constant time. More affected computations mean more work, and expression costs, dependency traversal, and tracing contribute to elapsed time.

Kernel propagation and React rendering are separate workloads, too. An efficient propagation wave does not automatically guarantee fewer component renders; that depends on subscriptions and component structure.

The [architecture notes](https://neurons-me.github.io/.me/docs/Architecture.html) and [benchmark sources](https://github.com/neurons-me/.me/tree/main/Typescript/tests/Benchmarks) provide more detail.

## Try One Relationship

For a multiplication inside a component, calculate during render.

When relationships become a shared business model, consider giving them a home outside individual components—where their dependencies and results can be inspected.

```bash
npm install this.me
```

```javascript
import Me from "this.me";

const me = Me();

me.order.price(100);
me.order.quantity(5);
me.order["="]("total", "price * quantity");

me.order.quantity(7);

console.log(me("order.total"));
console.log(me.explain("order.total"));
```

Declare a relationship. Change an input. Inspect what happened.

[Source code](https://github.com/neurons-me/.me) · [Usage guide](https://github.com/neurons-me/.me/blob/main/Typescript/README.md) · [npm package](https://www.npmjs.com/package/this.me)
