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.
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 amixin=Truestate. The shadow still happens — it just isn't reported.The consequence is compile-time: class-level access to the name stops returning a
Varand 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
MixinShadowis created without complaint. Observed state: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:
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:
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 incls.__dict__:With a mixin, the raw default lives in
OrdinaryMixin.__dict__, not inMixinShadow.__dict__— so the guard skips the name. The default still resolves through the MRO, which is what clobbers class-level access. Thecls.__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-Varclass attribute instead of only at the leaf class.Related gap
A direct
BaseStatesubclass starts its own state root, soget_parent_state()returnsNoneand 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
routerinto per-field base vars). The framework'srx_router_*vars are protected by exactly this guard, so the mixin route bypasses their protection too:Consumer.router.urlstill 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.