A state mixin can silently shadow an inherited base var
Nobody has claimed this yet.
Assessment
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Newbie friendliness
- 74/100
- Issue type
- Bug
- Clarity
- Clearly specified
- Activity status
- Active
- Tech stack
- python
- Domain
- full-stack
Research direction
Start in reflex/state.py at BaseState._check_overridden_inherited_vars and reproduce the mixin example from the issue. Trace how the inherited name and class attribute resolve through the MRO, then add a regression test covering class-level access and the shadowing error. Done means mixin redeclarations are rejected or remain reactive without silently replacing the inherited Var.
Written by the indexing model from the issue text.
Description
Summary
_check_overridden_inherited_vars (added in #7077) rejects a base var that shadows one inherited from a parent state, but the check is bypassed when the redeclaration is laundered through a mixin=True state. The shadow still happens — it just isn't reported.
The consequence is compile-time: class-level access to the name stops returning a Var and returns the mixin's raw default instead, so any frontend reference to it is baked into the page as a frozen literal with no state subscription.
Reproduction
import reflex as rx
class Parent(rx.State):
ordinary_var: str = "parent"
# Direct redeclaration -> correctly rejected
class DirectShadow(Parent):
ordinary_var: str = "shadowed-directly"
# BaseVarShadowsInheritedVarError: The var `ordinary_var` in ... shadows a var
# inherited from ...; use a different name instead
# The same redeclaration through a mixin -> accepted
class OrdinaryMixin(rx.State, mixin=True):
ordinary_var: str = "shadowed-via-mixin"
class MixinShadow(OrdinaryMixin, Parent):
pass
MixinShadow is created without complaint. Observed state:
Parent.ordinary_var -> StringCastedVar(...) # a Var
MixinShadow.ordinary_var -> 'shadowed-via-mixin' # a plain str
'ordinary_var' in MixinShadow.base_vars -> False
'ordinary_var' in MixinShadow.inherited_vars -> True
So the var machinery still (correctly) treats the parent as the owner, while the class attribute has been overwritten by the mixin's raw default.
Rendering it compiles a constant:
rx.text(MixinShadow.ordinary_var).render()["children"]
# [{'contents': '"shadowed-via-mixin"'}] -- a literal, not a state subscription
Instance-level behaviour is unaffected once the state is wired into a tree — reads and writes delegate to the parent and the delta is correct:
c.ordinary_var -> 'parent' (mixin default silently discarded)
c.ordinary_var = "written"
p.ordinary_var -> 'written'
p.get_delta() -> {'reflex___state____state.__main______parent': {'ordinary_var_rx_state_': 'written'}}
Two things are wrong, then: the mixin's declared default is silently dropped, and class-level access is no longer reactive.
Root cause
BaseState._check_overridden_inherited_vars (reflex/state.py) skips any field not present in cls.__dict__:
if (
name.startswith("_")
or not own_field.is_var
or name not in cls.inherited_vars
or name not in cls.__dict__
):
continue
With a mixin, the raw default lives in OrdinaryMixin.__dict__, not in MixinShadow.__dict__ — so the guard skips the name. The default still resolves through the MRO, which is what clobbers class-level access. The cls.__dict__ test is there to let a bare re-annotation (which leaves no class attribute, and so stays reactive) through as inert; it needs to look along the MRO for a non-Var class attribute instead of only at the leaf class.
Related gap
A direct BaseState subclass starts its own state root, so get_parent_state() returns None and the check returns early. Nothing inherited exists to shadow at that point, so this is arguably correct, but it means the guard offers no protection for the framework's own root-level vars in that position.
Why this came up
Found while working on #7068 (splitting router into per-field base vars). The framework's rx_router_* vars are protected by exactly this guard, so the mixin route bypasses their protection too:
class RouterMixin(rx.State, mixin=True):
rx_router_url: str = "hijacked"
class Consumer(RouterMixin, rx.State):
pass
Consumer.rx_router_url # -> 'hijacked', not a Var
Consumer.router.url still renders correctly (the switchboard reads the per-field vars directly) and instance-level router access is unaffected, so this is not a router-specific defect — the router is just one more caller that inherits the general gap. Filing separately from #7068 for that reason.
Related: #7074, #7077.
- Dominant language
- Python
- Stars
- 28.9k
- Forks
- 1.8k
- Avg merge
- 1d 20h
- Merged PRs (30d)
- 173
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from reflex-dev/reflex
-
Difficulty 1/5 1-3 hours Newbie friendliness 78/100
reflex-dev/reflex#7149 · 1 comment ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
reflex-dev/reflex#7148 · 1 comment ·
-
Difficulty 1/5 Under an hour Newbie friendliness 85/100
reflex-dev/reflex#7103 · 1 comment ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
reflex-dev/reflex#6823 · 1 comment ·
-
support redis v8 Openfeature request
Difficulty 1/5 Under an hour Newbie friendliness 88/100
reflex-dev/reflex#6764 · 3 comments · 1 reaction ·
All issues in reflex-dev/reflex
Similar issues
-
Difficulty 2/5 1-3 hours Newbie friendliness 88/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 82/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
-
enhancement
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 74/100