Repository: reflex-dev/reflex
Pull request: fix: do not allow instantiation of State mixins
Closes #4343

Problem statement:
Testing `rx.ComponentState` by direct initialization broken in `v0.6.2+` (SetUndefinedStateVarError)
**Describe the bug**
Before v0.6.2, it was possible to directly initialize `ComponentState` for testing purposes (same as directly initializing `State` instances), but since `v0.6.2` I get an error:
```
reflex.utils.exceptions.SetUndefinedStateVarError: The state variable 'some_var' has not been defined in 'DummyComponent'. All state variables must be declared before they can be set.
```

I've not been able to really narrow down where the issue comes from, but below is a very minimal example that demonstrates the issue.

**To Reproduce**
Steps to reproduce the behavior:
```python
import reflex as rx


class DummyComponent(rx.ComponentState):
    some_var: int = 0

    def do_something(self):
        self.some_var += 1


def test_direct_component_init():
    state_inst = DummyComponent()

    assert state_inst.some_var == 0

    state_inst.do_something()

    assert state_inst.some_var == 1
```

Running pytest on this with `reflex==0.6.1` passes... With `reflex==0.6.2` (or higher) it fails.

**Expected behavior**
Should be able to directly initialize ComponentState instances for testing purposes.

**Specifics (please complete the following information):**
 - Python Version: 3.12
 - Reflex Version: 0.6.2+
 - OS: WSL2
 
 
Is there a different recommended way to test basic behaviour of ComponetState subclasses? (and is this still the right approach to take with `rx.State` subclasses if not for `rx.ComponentState` subclasses?). 

Thanks!

Hints from issue discussion:
<p><a href="https://linear.app/reflex-dev/issue/ENG-4086/testing-rxcomponentstate-by-direct-initialization-broken-in-v062">ENG-4086 Testing `rx.ComponentState` by direct initialization broken in `v0.6.2+` (SetUndefinedStateVarError)</a></p>
@TimChild i suggest adjusting the test to smth like this:

```python
from typing import override

import reflex as rx


class DummyComponent(rx.ComponentState):
    some_var: int = 0

    def do_something(self):
        self.some_var += 1

    @override
    @classmethod
    def get_component(cls):
        return rx.box()


def test_direct_component_init():
    state_cls = DummyComponent.create().State
    assert state_cls is not None
    state_inst = state_cls()
    assert isinstance(state_inst, DummyComponent)

    assert state_inst.some_var == 0

    state_inst.do_something()

    assert state_inst.some_var == 1
```

@reflex-dev maybe we should raise an exception if someone tries to directly instantiate a ComponentState?
@benedikt-bartscher , oh yes, that make sense now that I see it, thanks! 

It would definitely be nice to get a helpful error message there, or for it to be possible to directly instantiate if in a testing environment, but your solution does make sense.

Commit message: improve error message for ComponentState mixins

Patch to review:
diff --git a/reflex/state.py b/reflex/state.py
index 66b1e3cabfa..1cbac53ce2c 100644
--- a/reflex/state.py
+++ b/reflex/state.py
@@ -86,6 +86,7 @@
     ImmutableStateError,
     InvalidStateManagerMode,
     LockExpiredError,
+    ReflexRuntimeError,
     SetUndefinedStateVarError,
     StateSchemaMismatchError,
 )
@@ -386,6 +387,10 @@ def __init__(
                 "State classes should not be instantiated directly in a Reflex app. "
                 "See https://reflex.dev/docs/state/ for further information."
             )
+        if type(self)._mixin:
+            raise ReflexRuntimeError(
+                f"{type(self).__name__} is a state mixin and cannot be instantiated directly."
+            )
         kwargs["parent_state"] = parent_state
         super().__init__()
         for name, value in kwargs.items():
@@ -2361,6 +2366,23 @@ def get_component(cls, **props):
     # The number of components created from this class.
     _per_component_state_instance_count: ClassVar[int] = 0
 
+    def __init__(self, *args, **kwargs):
+        """Do not allow direct initialization of the ComponentState.
+
+        Args:
+            *args: The args to pass to the State init method.
+            **kwargs: The kwargs to pass to the State init method.
+
+        Raises:
+            ReflexRuntimeError: If the ComponentState is initialized directly.
+        """
+        if type(self)._mixin:
+            raise ReflexRuntimeError(
+                f"{ComponentState.__name__} {type(self).__name__} is not meant to be initialized directly. "
+                + "Use the `create` method to create a new instance and access the statte via the `State` attribute."
+            )
+        super().__init__(*args, **kwargs)
+
     @classmethod
     def __init_subclass__(cls, mixin: bool = True, **kwargs):
         """Overwrite mixin default to True.
diff --git a/tests/units/components/test_component_state.py b/tests/units/components/test_component_state.py
index 574997ba53f..1b62e35c81d 100644
--- a/tests/units/components/test_component_state.py
+++ b/tests/units/components/test_component_state.py
@@ -1,7 +1,10 @@
 """Ensure that Components returned by ComponentState.create have independent State classes."""
 
+import pytest
+
 import reflex as rx
 from reflex.components.base.bare import Bare
+from reflex.utils.exceptions import ReflexRuntimeError
 
 
 def test_component_state():
@@ -40,3 +43,21 @@ def get_component(cls, *children, **props):
     assert len(cs2.children) == 1
     assert cs2.children[0].render() == Bare.create("b").render()
     assert cs2.id == "b"
+
+
+def test_init_component_state() -> None:
+    """Ensure that ComponentState subclasses cannot be instantiated directly."""
+
+    class CS(rx.ComponentState):
+        @classmethod
+        def get_component(cls, *children, **props):
+            return rx.el.div()
+
+    with pytest.raises(ReflexRuntimeError):
+        CS()
+
+    class SubCS(CS):
+        pass
+
+    with pytest.raises(ReflexRuntimeError):
+        SubCS()
diff --git a/tests/units/test_state.py b/tests/units/test_state.py
index 2ce0b7bd52d..e427c17d9e0 100644
--- a/tests/units/test_state.py
+++ b/tests/units/test_state.py
@@ -43,7 +43,7 @@
 )
 from reflex.testing import chdir
 from reflex.utils import format, prerequisites, types
-from reflex.utils.exceptions import SetUndefinedStateVarError
+from reflex.utils.exceptions import ReflexRuntimeError, SetUndefinedStateVarError
 from reflex.utils.format import json_dumps
 from reflex.vars.base import Var, computed_var
 from tests.units.states.mutation import MutableSQLAModel, MutableTestState
@@ -3411,3 +3411,19 @@ class TypedState(rx.State):
         field: rx.Field[str] = rx.field("")
 
     _ = TypedState(field="str")
+
+
+def test_init_mixin() -> None:
+    """Ensure that State mixins can not be instantiated directly."""
+
+    class Mixin(BaseState, mixin=True):
+        pass
+
+    with pytest.raises(ReflexRuntimeError):
+        Mixin()
+
+    class SubMixin(Mixin, mixin=True):
+        pass
+
+    with pytest.raises(ReflexRuntimeError):
+        SubMixin()


Code review comments:
diff_hunk: @@ -2361,6 +2366,23 @@ def get_component(cls, **props):
     # The number of components created from this class.
     _per_component_state_instance_count: ClassVar[int] = 0
 
+    def __init__(self, *args, **kwargs):
+        """Do not allow direct initialization of the ComponentState.
+
+        Args:
+            *args: The args to pass to the State init method.
+            **kwargs: The kwargs to pass to the State init method.
+
+        Raises:
+            ReflexRuntimeError: If the ComponentState is initialized directly.
+        """
+        if type(self)._mixin:
+            raise ReflexRuntimeError(
+                f"{ComponentState.__name__} {type(self).__name__} is not meant to be initialized directly. "
+                + "Use the `create` method to create a new instance and access the statte via the `State` attribute."
path: reflex/state.py
text: @user1:
```suggestion
                + "Use the `create` method to create a new instance and access the state via the `State` attribute."
```