You Might Not Need an Effect
Overview¶
Most Effects are unnecessary. If your logic only adjusts one thing based on another thing you already have, it belongs in the render — not in an Effect. This page walks through the common cases where an Effect looks right and isn't, with the render counts each one costs.
You will learn
- How to recognize an Effect that is doing a render's job
- What to write instead, case by case
- Why ReactPy cannot use React's "set state during render" escape hatch
- Which Effects genuinely are necessary
How to remove unnecessary Effects¶
Work through each Effect in your code and ask: does this code need to run after a render, or could it run while rendering?
An Effect is genuinely necessary when the component must reach something outside ReactPy — subscribing to a queue, opening a socket, starting a task. Everything below is the opposite case: the "external system" is a value your component already has.
If you're not sure what Effects are for in the first place, start with Synchronizing with Effects.
Updating state based on props or state¶
A component shows a name built from two fields. The tempting version stores the combination and keeps it in step with an Effect:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | |
1 | |
Remove the state variable and the Effect, and calculate it during render:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | |
1 | |
The difference is measurable, and the first row of each is the part users actually see:
| First paint | Renders for two keystrokes | |
|---|---|---|
| Effect + stored state | "(nothing yet)" | 6 |
| Derived during render | "Ada Lovelace" | 3 |
Verified in a Layout. The Effect version renders once with an empty name, sets state from the Effect, renders again — every keystroke costs two renders instead of one, and the very first paint is wrong. Nothing downstream can rescue that first frame, because it is a real render that a real client receives.
When something can be calculated from existing props or state, don't put it in state. Calculate it during render. Fewer state variables means less code, no way for two of them to drift apart, and half the renders. If this feels unfamiliar, Thinking in React covers what belongs in state.
How expensive is too expensive?
If you're not sure whether a calculation is costly enough to worry about, write it without caching first, then measure it: time the component body, or count how often it runs. In practice the work that is genuinely slow is usually obvious — parsing, sorting or filtering thousands of rows, regex over a large string, cryptographic hashes. String concatenation, len(), and a loop over a page's worth of items are not worth a cache, and a cache costs more than it saves when its dependency list is wrong.
Caching expensive calculations¶
When a calculation genuinely is expensive, the answer is still not state plus an Effect — it is use_memo, which caches a value without adding a render:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | |
1 | |
The filter records each run so the result is countable. Verified: after two keystrokes the component has rendered three times and the filter has run once; pressing Change filter adds a render and a second run. The value is recomputed exactly when the inputs it reads change, and never on a render that didn't change them.
Two properties worth knowing, both verified:
use_memoinfers its dependencies the same wayuse_effectdoes — from the names the function reads. Passdependencies=[]to pin a value forever, ordependencies=Noneto recompute on every render. See Use Memo.- It caches the value, not the render. The component body still runs on every render; ReactPy has no component-level
memo, so a child re-runs whenever its parent does.use_memosaves the computation, never the render — see Render and Commit.
Mutating a value in place and "setting" it back defeats use_memo too, for the same reason it defeats use_state: the new value compares equal to the old one. Build a new object instead (Updating Arrays in State).
Resetting all state when a prop changes¶
A profile page holds a comment draft. When the user navigates to a different profile, the draft should not follow them:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | |
1 | |
Verified: type a draft, press Next profile, and the draft is empty on the new profile — in a single extra render.
Passing key=user_id tells ReactPy that a different user_id means a different component instance. It tears down profile() and everything below it and builds them again, so all of their state starts fresh. One line, and it covers every piece of state in the whole subtree — no Effect, and no remembering to add one to each nested component.
The Effect version (set_comment("") in an Effect depending on user_id) is worse in three ways: the subtree renders once with the previous user's draft still in it, every nested component needs its own copy, and a piece of state added later silently fails to reset. See Preserving and Resetting State for the full mechanism.
key is spelled differently for elements and components
Here it is a keyword argument to the component call: profile(user_id, key=user_id). On an element it goes in the attribute dict instead: html.li({"key": 1}, ...). Put it in the wrong place and you get TypeError: Vdom.__call__() got an unexpected keyword argument 'key', or a literal key="..." attribute in the page.
Adjusting some state when a prop changes¶
Sometimes you don't want to throw state away — just invalidate part of it when an input changes. A list keeps a selection, and a new items prop should clear it. React's advice is to call the setter during render, and that escape hatch does not work in ReactPy. Measured, the render-phase-setter version takes four renders and the client receives this sequence:
<root> items: [a,b,c] selected: None
/children/0/children/1 items: [a,b,c] selected: a
<root> items: [a,b,c,i4] selected: a <-- stale, and delivered
/children/0/children/1 items: [a,b,c,i4] selected: None
React throws the discarded render away; ReactPy hands the model to the socket as soon as it exists, so the stale frame ships. Use a derived read instead. Store the value alongside what it belonged to, and let the read decide whether it is still valid:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | |
1 | |
Verified: Select costs one render and shows the selection; Add item costs one render and the selection is already gone in that same patch — three renders total, no stale frame.
The general shape is a guard on the read rather than a call to the setter:
is_valid = chosen["items"] == items
selection = chosen["value"] if is_valid else None
Do not set state from an Effect that depends on it
The reflexive alternative — an Effect calling set_selection(None) with dependencies=[items] — is not merely slow. When the value it sets is one it also reads, it is a loop, and ReactPy has no detector for one. Measured: an Effect that set a counter it depended on rendered 86 times before the probe gave up. If a component visibly spins, read what the Effect awaits, what it depends on, and what it sets.
Sharing logic between event handlers¶
Two handlers should do the same thing on input. There is no reason for this to involve an Effect:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | |
1 | |
Verified: type in the shipping field and the billing readout changes with it, in one render, because both setters were called from one handler.
Event handlers are ordinary functions. Call one from the other, extract the shared part into a plain function, or pass the other setter as a parameter. Whatever React's docs tell you here transfers unchanged, because none of it is rendering work.
Sending a POST request¶
Submitting a form is an event, not a consequence of rendering. Do the work in the handler:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | |
1 | |
Verified: fill the field, submit, and the transport recorded exactly one send — ['ada@example.com'].
The version to avoid is a flag plus an Effect: set_pending(True), and an Effect that watches pending. It costs a state variable, an extra render per submission, and — because Effects re-run on dependency changes and remounts — a way to send twice. event.preventDefault() stops the page navigating, and must appear literally in the handler's body, since ReactPy detects it by bytecode inspection (Creating VDOM Event Handlers).
Notifying parent components about state changes¶
A child should tell its parent something happened. Pass a callback and call it from the child's event handler — this works exactly as it does in React:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | |
1 | |
Verified: type ada, submit, and the parent's paragraph reads searching for 'ada'....
What you must not do is call the parent's function from an Effect that watches a value. Handlers run once per user action; Effects run once per dependency change, which includes renders the user never caused. There is also no use_imperative_handle for the reverse direction — pass data down as props and callbacks up as arguments (Sharing State Between Components).
Chains of computations¶
When several values feed each other, keep the whole chain in render. Each intermediate is a local variable, not a state variable:
@component
def report(rows, currency):
priced = [(*row, row[1] * RATES[currency]) for row in rows]
total = sum(item[-1] for item in priced)
heading = f"{currency.upper()}: {total:,.2f}"
return html.div(html.h1(heading))
An Effect at any link in that chain means every upstream change costs an extra full render — and, as above, each of those renders is visible to the client. If one link is genuinely expensive, wrap that one in use_memo rather than putting the whole chain in state.
Initializing the application¶
Code that should run once per process does not belong in a component at all — a component's Effect runs once per connection, and a server serves many connections:
# module level, or the ASGI app's startup: runs once
configure_logging()
POOL = create_pool()
Keep a dependencies=[] Effect for work that is genuinely per-connection, such as a subscription or a session-scoped task. Never guard a process-level side effect with my_component.started = True: the function object is module-scope, so the first visitor flips the flag for everyone (VDOM Mutations).
Subscribing to an external store¶
Here is the case where the Effect is correct. The value does not live in ReactPy at all, so there is nothing to derive it from — ReactPy has to be told when it changes:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | |
1 | |
Verified: pressing Tick once shows 1 ticks; pressing Stop watching unmounts the view and leaves 0 listeners registered on the store.
Three things make this one legitimate where the others were not:
- The source of truth is outside ReactPy. Deriving would mean reading
clock.get()during render, which has no way to know when to run again. - The subscription needs a lifetime, and the clean-up is what returns the store to zero listeners.
- The state exists to trigger a render — that is its whole purpose.
This is also where use_async_effect earns its place, when the external system is an async stream rather than a callback. See Synchronizing with Effects for cancellation and the four-way dependency table.
Fetching data, without the extra Effects¶
Fetching genuinely needs an Effect — the network is external. Everything you would otherwise stack on top of it does not. Store the response, and shape it in render:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | |
1 | |
Verified: the Effect stores the raw response; the labels, the per-row formatting, and the total are computed during render. One request per report name (['orders', 'users']), and no rows or total state variables that could fall out of step with the response.
The example carries one more lesson in its state shape. It stores the response together with the name that produced it and derives the read:
raw = loaded["data"] if loaded["name"] == name else {}
Storing the response alone looks fine and is wrong: switch from orders to users and the component re-renders immediately, before the new response arrives, so the client receives users: 55 total above the orders rows — 55 being orders' total. Measured, and the derived read above is what fixes it, without remounting the component.
The anti-pattern is a second Effect that watches the response and sets three more state variables from it. Each of those is a value you can calculate, and each adds a render in which the page is half-updated.
Because ReactPy already runs on the server, there is usually a better option than fetching in the child at all: fetch in a parent during render, or at startup, and pass the result down as a prop. Then the child has no Effect and no loading state, and the response is shared rather than re-requested. Keep the Effect for data that depends on something only the connection knows, like use_location.
How to tell whether an Effect is necessary¶
| The Effect... | Verdict |
|---|---|
| reads state or props and sets other state | unnecessary — compute it during render |
| caches a calculation | unnecessary — use_memo |
| resets state when a prop changes | unnecessary — pass a key |
| invalidates state when a prop changes | unnecessary — derive the read from what it belonged to |
| runs logic shared by two handlers | unnecessary — call a function |
| reacts to a user action | unnecessary — put it in the event handler |
| calls a parent's callback when a value changes | unnecessary — call it from the handler |
| subscribes to a queue, socket, or store | necessary, with a clean-up |
starts async work that outlives the render | necessary, with use_async_effect |
| starts per-connection work on mount | necessary, with dependencies=[] |
If you cannot name what the Effect is synchronizing with, it is probably doing a render's job.
Two failure modes that cost hours
- An Effect that sets state it depends on loops silently. ReactPy has no loop detector. Measured: 86 renders and counting before the probe stopped.
- Hooks do not work inside an effect body.
use_context,use_state, and friends all raiseRuntimeError: No life cycle hook is active. Are you rendering in a layout?— effects run in their own task, with no hook on the stack. Read what you need during render and let the closure carry it in. The traceback appears only in the server log; the page just goes quiet.
Recap¶
- An Effect runs after a render. If the work could happen during the render, it should.
- Values calculated from props or state are not state. Compute them; do not store and re-sync them. Measured: the Effect version costs twice the renders and ships a wrong first frame.
use_memocaches expensive calculations without adding a render — but it caches values, not component renders.- Reset state with a
key, not with an Effect. - ReactPy cannot use React's render-phase setter trick: the stale patch reaches the client. Derive the value instead.
- Sharing logic between handlers, notifying a parent, and submitting a form are all handler work.
- Subscriptions and async streams are the real use for Effects — with a clean-up, always.
Related¶
- Synchronizing with Effects — how Effects work, and their four-way dependency table
- Choosing the State Structure — avoid redundant state in the first place
- Preserving and Resetting State — resetting with
key - Use Memo and Use Callback
- Separating Events from Effects — the remaining case where reading fresh state matters