ソースを参照

Reassign state Var when fields on a Base instance change (#1748)

Masen Furer 1 年間 前
コミット
1430075bdc
8 ファイル変更494 行追加471 行削除
  1. 85 11
      poetry.lock
  2. 1 0
      pyproject.toml
  3. 169 99
      reflex/state.py
  4. 0 272
      reflex/vars.py
  5. 0 36
      reflex/vars.pyi
  6. 12 0
      tests/conftest.py
  7. 227 19
      tests/test_state.py
  8. 0 34
      tests/test_var.py

+ 85 - 11
poetry.lock

@@ -1489,7 +1489,6 @@ files = [
     {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"},
     {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"},
     {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"},
     {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"},
     {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"},
     {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"},
-    {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"},
     {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"},
     {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"},
     {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"},
     {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"},
     {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"},
     {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"},
@@ -1497,15 +1496,8 @@ files = [
     {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"},
     {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"},
     {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"},
     {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"},
     {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"},
     {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"},
-    {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"},
     {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"},
     {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"},
     {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"},
     {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"},
-    {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"},
-    {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"},
-    {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"},
-    {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"},
-    {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"},
-    {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"},
     {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"},
     {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"},
     {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"},
     {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"},
     {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"},
     {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"},
@@ -1522,7 +1514,6 @@ files = [
     {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"},
     {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"},
     {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"},
     {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"},
     {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"},
     {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"},
-    {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"},
     {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"},
     {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"},
     {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"},
     {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"},
     {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"},
     {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"},
@@ -1530,7 +1521,6 @@ files = [
     {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"},
     {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"},
     {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"},
     {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"},
     {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"},
     {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"},
-    {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"},
     {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"},
     {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"},
     {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"},
     {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"},
     {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"},
     {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"},
@@ -2189,6 +2179,90 @@ files = [
     {file = "websockets-10.4.tar.gz", hash = "sha256:eef610b23933c54d5d921c92578ae5f89813438fded840c2e9809d378dc765d3"},
     {file = "websockets-10.4.tar.gz", hash = "sha256:eef610b23933c54d5d921c92578ae5f89813438fded840c2e9809d378dc765d3"},
 ]
 ]
 
 
+[[package]]
+name = "wrapt"
+version = "1.15.0"
+description = "Module for decorators, wrappers and monkey patching."
+optional = false
+python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
+files = [
+    {file = "wrapt-1.15.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:ca1cccf838cd28d5a0883b342474c630ac48cac5df0ee6eacc9c7290f76b11c1"},
+    {file = "wrapt-1.15.0-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:e826aadda3cae59295b95343db8f3d965fb31059da7de01ee8d1c40a60398b29"},
+    {file = "wrapt-1.15.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:5fc8e02f5984a55d2c653f5fea93531e9836abbd84342c1d1e17abc4a15084c2"},
+    {file = "wrapt-1.15.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:96e25c8603a155559231c19c0349245eeb4ac0096fe3c1d0be5c47e075bd4f46"},
+    {file = "wrapt-1.15.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:40737a081d7497efea35ab9304b829b857f21558acfc7b3272f908d33b0d9d4c"},
+    {file = "wrapt-1.15.0-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:f87ec75864c37c4c6cb908d282e1969e79763e0d9becdfe9fe5473b7bb1e5f09"},
+    {file = "wrapt-1.15.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:1286eb30261894e4c70d124d44b7fd07825340869945c79d05bda53a40caa079"},
+    {file = "wrapt-1.15.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:493d389a2b63c88ad56cdc35d0fa5752daac56ca755805b1b0c530f785767d5e"},
+    {file = "wrapt-1.15.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:58d7a75d731e8c63614222bcb21dd992b4ab01a399f1f09dd82af17bbfc2368a"},
+    {file = "wrapt-1.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:21f6d9a0d5b3a207cdf7acf8e58d7d13d463e639f0c7e01d82cdb671e6cb7923"},
+    {file = "wrapt-1.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ce42618f67741d4697684e501ef02f29e758a123aa2d669e2d964ff734ee00ee"},
+    {file = "wrapt-1.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41d07d029dd4157ae27beab04d22b8e261eddfc6ecd64ff7000b10dc8b3a5727"},
+    {file = "wrapt-1.15.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:54accd4b8bc202966bafafd16e69da9d5640ff92389d33d28555c5fd4f25ccb7"},
+    {file = "wrapt-1.15.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fbfbca668dd15b744418265a9607baa970c347eefd0db6a518aaf0cfbd153c0"},
+    {file = "wrapt-1.15.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:76e9c727a874b4856d11a32fb0b389afc61ce8aaf281ada613713ddeadd1cfec"},
+    {file = "wrapt-1.15.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e20076a211cd6f9b44a6be58f7eeafa7ab5720eb796975d0c03f05b47d89eb90"},
+    {file = "wrapt-1.15.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a74d56552ddbde46c246b5b89199cb3fd182f9c346c784e1a93e4dc3f5ec9975"},
+    {file = "wrapt-1.15.0-cp310-cp310-win32.whl", hash = "sha256:26458da5653aa5b3d8dc8b24192f574a58984c749401f98fff994d41d3f08da1"},
+    {file = "wrapt-1.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:75760a47c06b5974aa5e01949bf7e66d2af4d08cb8c1d6516af5e39595397f5e"},
+    {file = "wrapt-1.15.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ba1711cda2d30634a7e452fc79eabcadaffedf241ff206db2ee93dd2c89a60e7"},
+    {file = "wrapt-1.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:56374914b132c702aa9aa9959c550004b8847148f95e1b824772d453ac204a72"},
+    {file = "wrapt-1.15.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a89ce3fd220ff144bd9d54da333ec0de0399b52c9ac3d2ce34b569cf1a5748fb"},
+    {file = "wrapt-1.15.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3bbe623731d03b186b3d6b0d6f51865bf598587c38d6f7b0be2e27414f7f214e"},
+    {file = "wrapt-1.15.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3abbe948c3cbde2689370a262a8d04e32ec2dd4f27103669a45c6929bcdbfe7c"},
+    {file = "wrapt-1.15.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b67b819628e3b748fd3c2192c15fb951f549d0f47c0449af0764d7647302fda3"},
+    {file = "wrapt-1.15.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:7eebcdbe3677e58dd4c0e03b4f2cfa346ed4049687d839adad68cc38bb559c92"},
+    {file = "wrapt-1.15.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:74934ebd71950e3db69960a7da29204f89624dde411afbfb3b4858c1409b1e98"},
+    {file = "wrapt-1.15.0-cp311-cp311-win32.whl", hash = "sha256:bd84395aab8e4d36263cd1b9308cd504f6cf713b7d6d3ce25ea55670baec5416"},
+    {file = "wrapt-1.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:a487f72a25904e2b4bbc0817ce7a8de94363bd7e79890510174da9d901c38705"},
+    {file = "wrapt-1.15.0-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:4ff0d20f2e670800d3ed2b220d40984162089a6e2c9646fdb09b85e6f9a8fc29"},
+    {file = "wrapt-1.15.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:9ed6aa0726b9b60911f4aed8ec5b8dd7bf3491476015819f56473ffaef8959bd"},
+    {file = "wrapt-1.15.0-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:896689fddba4f23ef7c718279e42f8834041a21342d95e56922e1c10c0cc7afb"},
+    {file = "wrapt-1.15.0-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:75669d77bb2c071333417617a235324a1618dba66f82a750362eccbe5b61d248"},
+    {file = "wrapt-1.15.0-cp35-cp35m-win32.whl", hash = "sha256:fbec11614dba0424ca72f4e8ba3c420dba07b4a7c206c8c8e4e73f2e98f4c559"},
+    {file = "wrapt-1.15.0-cp35-cp35m-win_amd64.whl", hash = "sha256:fd69666217b62fa5d7c6aa88e507493a34dec4fa20c5bd925e4bc12fce586639"},
+    {file = "wrapt-1.15.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:b0724f05c396b0a4c36a3226c31648385deb6a65d8992644c12a4963c70326ba"},
+    {file = "wrapt-1.15.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bbeccb1aa40ab88cd29e6c7d8585582c99548f55f9b2581dfc5ba68c59a85752"},
+    {file = "wrapt-1.15.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:38adf7198f8f154502883242f9fe7333ab05a5b02de7d83aa2d88ea621f13364"},
+    {file = "wrapt-1.15.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:578383d740457fa790fdf85e6d346fda1416a40549fe8db08e5e9bd281c6a475"},
+    {file = "wrapt-1.15.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:a4cbb9ff5795cd66f0066bdf5947f170f5d63a9274f99bdbca02fd973adcf2a8"},
+    {file = "wrapt-1.15.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:af5bd9ccb188f6a5fdda9f1f09d9f4c86cc8a539bd48a0bfdc97723970348418"},
+    {file = "wrapt-1.15.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:b56d5519e470d3f2fe4aa7585f0632b060d532d0696c5bdfb5e8319e1d0f69a2"},
+    {file = "wrapt-1.15.0-cp36-cp36m-win32.whl", hash = "sha256:77d4c1b881076c3ba173484dfa53d3582c1c8ff1f914c6461ab70c8428b796c1"},
+    {file = "wrapt-1.15.0-cp36-cp36m-win_amd64.whl", hash = "sha256:077ff0d1f9d9e4ce6476c1a924a3332452c1406e59d90a2cf24aeb29eeac9420"},
+    {file = "wrapt-1.15.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5c5aa28df055697d7c37d2099a7bc09f559d5053c3349b1ad0c39000e611d317"},
+    {file = "wrapt-1.15.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3a8564f283394634a7a7054b7983e47dbf39c07712d7b177b37e03f2467a024e"},
+    {file = "wrapt-1.15.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:780c82a41dc493b62fc5884fb1d3a3b81106642c5c5c78d6a0d4cbe96d62ba7e"},
+    {file = "wrapt-1.15.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e169e957c33576f47e21864cf3fc9ff47c223a4ebca8960079b8bd36cb014fd0"},
+    {file = "wrapt-1.15.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:b02f21c1e2074943312d03d243ac4388319f2456576b2c6023041c4d57cd7019"},
+    {file = "wrapt-1.15.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:f2e69b3ed24544b0d3dbe2c5c0ba5153ce50dcebb576fdc4696d52aa22db6034"},
+    {file = "wrapt-1.15.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d787272ed958a05b2c86311d3a4135d3c2aeea4fc655705f074130aa57d71653"},
+    {file = "wrapt-1.15.0-cp37-cp37m-win32.whl", hash = "sha256:02fce1852f755f44f95af51f69d22e45080102e9d00258053b79367d07af39c0"},
+    {file = "wrapt-1.15.0-cp37-cp37m-win_amd64.whl", hash = "sha256:abd52a09d03adf9c763d706df707c343293d5d106aea53483e0ec8d9e310ad5e"},
+    {file = "wrapt-1.15.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cdb4f085756c96a3af04e6eca7f08b1345e94b53af8921b25c72f096e704e145"},
+    {file = "wrapt-1.15.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:230ae493696a371f1dbffaad3dafbb742a4d27a0afd2b1aecebe52b740167e7f"},
+    {file = "wrapt-1.15.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63424c681923b9f3bfbc5e3205aafe790904053d42ddcc08542181a30a7a51bd"},
+    {file = "wrapt-1.15.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d6bcbfc99f55655c3d93feb7ef3800bd5bbe963a755687cbf1f490a71fb7794b"},
+    {file = "wrapt-1.15.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c99f4309f5145b93eca6e35ac1a988f0dc0a7ccf9ccdcd78d3c0adf57224e62f"},
+    {file = "wrapt-1.15.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b130fe77361d6771ecf5a219d8e0817d61b236b7d8b37cc045172e574ed219e6"},
+    {file = "wrapt-1.15.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:96177eb5645b1c6985f5c11d03fc2dbda9ad24ec0f3a46dcce91445747e15094"},
+    {file = "wrapt-1.15.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d5fe3e099cf07d0fb5a1e23d399e5d4d1ca3e6dfcbe5c8570ccff3e9208274f7"},
+    {file = "wrapt-1.15.0-cp38-cp38-win32.whl", hash = "sha256:abd8f36c99512755b8456047b7be10372fca271bf1467a1caa88db991e7c421b"},
+    {file = "wrapt-1.15.0-cp38-cp38-win_amd64.whl", hash = "sha256:b06fa97478a5f478fb05e1980980a7cdf2712015493b44d0c87606c1513ed5b1"},
+    {file = "wrapt-1.15.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2e51de54d4fb8fb50d6ee8327f9828306a959ae394d3e01a1ba8b2f937747d86"},
+    {file = "wrapt-1.15.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0970ddb69bba00670e58955f8019bec4a42d1785db3faa043c33d81de2bf843c"},
+    {file = "wrapt-1.15.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76407ab327158c510f44ded207e2f76b657303e17cb7a572ffe2f5a8a48aa04d"},
+    {file = "wrapt-1.15.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cd525e0e52a5ff16653a3fc9e3dd827981917d34996600bbc34c05d048ca35cc"},
+    {file = "wrapt-1.15.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d37ac69edc5614b90516807de32d08cb8e7b12260a285ee330955604ed9dd29"},
+    {file = "wrapt-1.15.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:078e2a1a86544e644a68422f881c48b84fef6d18f8c7a957ffd3f2e0a74a0d4a"},
+    {file = "wrapt-1.15.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:2cf56d0e237280baed46f0b5316661da892565ff58309d4d2ed7dba763d984b8"},
+    {file = "wrapt-1.15.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:7dc0713bf81287a00516ef43137273b23ee414fe41a3c14be10dd95ed98a2df9"},
+    {file = "wrapt-1.15.0-cp39-cp39-win32.whl", hash = "sha256:46ed616d5fb42f98630ed70c3529541408166c22cdfd4540b88d5f21006b0eff"},
+    {file = "wrapt-1.15.0-cp39-cp39-win_amd64.whl", hash = "sha256:eef4d64c650f33347c1f9266fa5ae001440b232ad9b98f1f43dfe7a79435c0a6"},
+    {file = "wrapt-1.15.0-py3-none-any.whl", hash = "sha256:64b1df0f83706b4ef4cfb4fb0e4c2669100fd7ecacfb59e091fad300d4e04640"},
+    {file = "wrapt-1.15.0.tar.gz", hash = "sha256:d06730c6aed78cee4126234cf2d071e01b44b915e725a6cb439a879ec9754a3a"},
+]
+
 [[package]]
 [[package]]
 name = "wsproto"
 name = "wsproto"
 version = "1.2.0"
 version = "1.2.0"
@@ -2221,4 +2295,4 @@ testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more
 [metadata]
 [metadata]
 lock-version = "2.0"
 lock-version = "2.0"
 python-versions = "^3.7"
 python-versions = "^3.7"
-content-hash = "0dd6230851cc4f43e192e45431d1c1dcb451b7946ae7cd169e220e7f7a072aa2"
+content-hash = "091bbeb36378731e9016db10ac0fcd19dda01947515fcfdc29303b2b3a2b37d6"

+ 1 - 0
pyproject.toml

@@ -47,6 +47,7 @@ alembic = "^1.11.1"
 platformdirs = "^3.10.0"
 platformdirs = "^3.10.0"
 distro = {version = "^1.8.0", platform = "linux"}
 distro = {version = "^1.8.0", platform = "linux"}
 python-engineio = "!=4.6.0"
 python-engineio = "!=4.6.0"
+wrapt = "^1.15.0"
 
 
 [tool.poetry.group.dev.dependencies]
 [tool.poetry.group.dev.dependencies]
 pytest = "^7.1.2"
 pytest = "^7.1.2"

+ 169 - 99
reflex/state.py

@@ -22,18 +22,18 @@ from typing import (
     Sequence,
     Sequence,
     Set,
     Set,
     Type,
     Type,
-    Union,
 )
 )
 
 
 import cloudpickle
 import cloudpickle
 import pydantic
 import pydantic
+import wrapt
 from redis import Redis
 from redis import Redis
 
 
 from reflex import constants
 from reflex import constants
 from reflex.base import Base
 from reflex.base import Base
 from reflex.event import Event, EventHandler, EventSpec, fix_events, window_alert
 from reflex.event import Event, EventHandler, EventSpec, fix_events, window_alert
 from reflex.utils import format, prerequisites, types
 from reflex.utils import format, prerequisites, types
-from reflex.vars import BaseVar, ComputedVar, ReflexDict, ReflexList, ReflexSet, Var
+from reflex.vars import BaseVar, ComputedVar, Var
 
 
 Delta = Dict[str, Any]
 Delta = Dict[str, Any]
 
 
@@ -129,32 +129,6 @@ class State(Base, ABC, extra=pydantic.Extra.allow):
         # Create a fresh copy of the backend variables for this instance
         # Create a fresh copy of the backend variables for this instance
         self._backend_vars = copy.deepcopy(self.backend_vars)
         self._backend_vars = copy.deepcopy(self.backend_vars)
 
 
-        # Initialize the mutable fields.
-        self._init_mutable_fields()
-
-    def _init_mutable_fields(self):
-        """Initialize mutable fields.
-
-        Allow mutation to dict, list, and set to be detected by the app.
-        """
-        for field in self.base_vars.values():
-            value = getattr(self, field.name)
-
-            if types._issubclass(field.type_, Union[List, Dict, Set]):
-                value_in_rx_data = _convert_mutable_datatypes(
-                    value, self._reassign_field, field.name
-                )
-                setattr(self, field.name, value_in_rx_data)
-
-        for field_name, value in self._backend_vars.items():
-            if isinstance(value, (list, dict, set)):
-                value_in_rx_data = _convert_mutable_datatypes(
-                    value, self._reassign_field, field_name
-                )
-                self._backend_vars[field_name] = value_in_rx_data
-
-        self._clean()
-
     def _init_event_handlers(self, state: State | None = None):
     def _init_event_handlers(self, state: State | None = None):
         """Initialize event handlers.
         """Initialize event handlers.
 
 
@@ -178,20 +152,6 @@ class State(Base, ABC, extra=pydantic.Extra.allow):
         if state.parent_state is not None:
         if state.parent_state is not None:
             self._init_event_handlers(state.parent_state)
             self._init_event_handlers(state.parent_state)
 
 
-    def _reassign_field(self, field_name: str):
-        """Reassign the given field.
-
-        Primarily for mutation in fields of mutable data types.
-
-        Args:
-            field_name: The name of the field we want to reassign
-        """
-        setattr(
-            self,
-            field_name,
-            getattr(self, field_name),
-        )
-
     def __repr__(self) -> str:
     def __repr__(self) -> str:
         """Get the string representation of the state.
         """Get the string representation of the state.
 
 
@@ -636,9 +596,20 @@ class State(Base, ABC, extra=pydantic.Extra.allow):
         }
         }
         if name in inherited_vars:
         if name in inherited_vars:
             return getattr(super().__getattribute__("parent_state"), name)
             return getattr(super().__getattribute__("parent_state"), name)
-        elif name in super().__getattribute__("_backend_vars"):
-            return super().__getattribute__("_backend_vars").__getitem__(name)
-        return super().__getattribute__(name)
+
+        backend_vars = super().__getattribute__("_backend_vars")
+        if name in backend_vars:
+            value = backend_vars[name]
+        else:
+            value = super().__getattribute__(name)
+
+        if isinstance(value, MutableProxy.__mutable_types__) and (
+            name in super().__getattribute__("base_vars") or name in backend_vars
+        ):
+            # track changes in mutable containers (list, dict, set, etc)
+            return MutableProxy(wrapped=value, state=self, field_name=name)
+
+        return value
 
 
     def __setattr__(self, name: str, value: Any):
     def __setattr__(self, name: str, value: Any):
         """Set the attribute.
         """Set the attribute.
@@ -649,18 +620,16 @@ class State(Base, ABC, extra=pydantic.Extra.allow):
             name: The name of the attribute.
             name: The name of the attribute.
             value: The value of the attribute.
             value: The value of the attribute.
         """
         """
+        if isinstance(value, MutableProxy):
+            # unwrap proxy objects when assigning back to the state
+            value = value.__wrapped__
+
         # Set the var on the parent state.
         # Set the var on the parent state.
         inherited_vars = {**self.inherited_vars, **self.inherited_backend_vars}
         inherited_vars = {**self.inherited_vars, **self.inherited_backend_vars}
         if name in inherited_vars:
         if name in inherited_vars:
             setattr(self.parent_state, name, value)
             setattr(self.parent_state, name, value)
             return
             return
 
 
-        # Make sure lists and dicts are converted to ReflexList, ReflexDict and ReflexSet.
-        if name in (*self.base_vars, *self.backend_vars) and types._isinstance(
-            value, Union[List, Dict, Set]
-        ):
-            value = _convert_mutable_datatypes(value, self._reassign_field, name)
-
         if types.is_backend_variable(name) and name != "_backend_vars":
         if types.is_backend_variable(name) and name != "_backend_vars":
             self._backend_vars.__setitem__(name, value)
             self._backend_vars.__setitem__(name, value)
             self.dirty_vars.add(name)
             self.dirty_vars.add(name)
@@ -1087,54 +1056,6 @@ class StateManager(Base):
         self.redis.set(token, cloudpickle.dumps(state), ex=self.token_expiration)
         self.redis.set(token, cloudpickle.dumps(state), ex=self.token_expiration)
 
 
 
 
-def _convert_mutable_datatypes(
-    field_value: Any, reassign_field: Callable, field_name: str
-) -> Any:
-    """Recursively convert mutable data to the Rx data types.
-
-    Note: right now only list, dict and set would be handled recursively.
-
-    Args:
-        field_value: The target field_value.
-        reassign_field:
-            The function to reassign the field in the parent state.
-        field_name: the name of the field in the parent state
-
-    Returns:
-        The converted field_value
-    """
-    if isinstance(field_value, list):
-        field_value = [
-            _convert_mutable_datatypes(value, reassign_field, field_name)
-            for value in field_value
-        ]
-
-        field_value = ReflexList(
-            field_value, reassign_field=reassign_field, field_name=field_name
-        )
-
-    if isinstance(field_value, dict):
-        field_value = {
-            key: _convert_mutable_datatypes(value, reassign_field, field_name)
-            for key, value in field_value.items()
-        }
-        field_value = ReflexDict(
-            field_value, reassign_field=reassign_field, field_name=field_name
-        )
-
-    if isinstance(field_value, set):
-        field_value = [
-            _convert_mutable_datatypes(value, reassign_field, field_name)
-            for value in field_value
-        ]
-
-        field_value = ReflexSet(
-            field_value, reassign_field=reassign_field, field_name=field_name
-        )
-
-    return field_value
-
-
 class ClientStorageBase:
 class ClientStorageBase:
     """Base class for client-side storage."""
     """Base class for client-side storage."""
 
 
@@ -1234,3 +1155,152 @@ class LocalStorage(ClientStorageBase, str):
             inst = super().__new__(cls, object)
             inst = super().__new__(cls, object)
         inst.name = name
         inst.name = name
         return inst
         return inst
+
+
+class MutableProxy(wrapt.ObjectProxy):
+    """A proxy for a mutable object that tracks changes."""
+
+    # Methods on wrapped objects which should mark the state as dirty.
+    __mark_dirty_attrs__ = set(
+        [
+            "add",
+            "append",
+            "clear",
+            "difference_update",
+            "discard",
+            "extend",
+            "insert",
+            "intersection_update",
+            "pop",
+            "popitem",
+            "remove",
+            "reverse",
+            "setdefault",
+            "sort",
+            "symmetric_difference_update",
+            "update",
+        ]
+    )
+
+    __mutable_types__ = (list, dict, set, Base)
+
+    def __init__(self, wrapped: Any, state: State, field_name: str):
+        """Create a proxy for a mutable object that tracks changes.
+
+        Args:
+            wrapped: The object to proxy.
+            state: The state to mark dirty when the object is changed.
+            field_name: The name of the field on the state associated with the
+                wrapped object.
+        """
+        super().__init__(wrapped)
+        self._self_state = state
+        self._self_field_name = field_name
+
+    def _mark_dirty(self, wrapped=None, instance=None, args=tuple(), kwargs=None):
+        """Mark the state as dirty, then call a wrapped function.
+
+        Intended for use with `FunctionWrapper` from the `wrapt` library.
+
+        Args:
+            wrapped: The wrapped function.
+            instance: The instance of the wrapped function.
+            args: The args for the wrapped function.
+            kwargs: The kwargs for the wrapped function.
+        """
+        self._self_state.dirty_vars.add(self._self_field_name)
+        self._self_state._mark_dirty()
+        if wrapped is not None:
+            wrapped(*args, **(kwargs or {}))
+
+    def __getattribute__(self, __name: str) -> Any:
+        """Get the attribute on the proxied object and return a proxy if mutable.
+
+        Args:
+            __name: The name of the attribute.
+
+        Returns:
+            The attribute value.
+        """
+        value = super().__getattribute__(__name)
+
+        if callable(value) and __name in super().__getattribute__(
+            "__mark_dirty_attrs__"
+        ):
+            # Wrap special callables, like "append", which should mark state dirty.
+            return wrapt.FunctionWrapper(
+                value,
+                super().__getattribute__("_mark_dirty"),
+            )
+
+        if isinstance(
+            value, super().__getattribute__("__mutable_types__")
+        ) and __name not in ("__wrapped__", "_self_state"):
+            # Recursively wrap mutable attribute values retrieved through this proxy.
+            return MutableProxy(
+                wrapped=value,
+                state=self._self_state,
+                field_name=self._self_field_name,
+            )
+
+        return value
+
+    def __getitem__(self, key) -> Any:
+        """Get the item on the proxied object and return a proxy if mutable.
+
+        Args:
+            key: The key of the item.
+
+        Returns:
+            The item value.
+        """
+        value = super().__getitem__(key)
+        if isinstance(value, self.__mutable_types__):
+            # Recursively wrap mutable items retrieved through this proxy.
+            return MutableProxy(
+                wrapped=value,
+                state=self._self_state,
+                field_name=self._self_field_name,
+            )
+        return value
+
+    def __delattr__(self, name):
+        """Delete the attribute on the proxied object and mark state dirty.
+
+        Args:
+            name: The name of the attribute.
+        """
+        self._mark_dirty(super().__delattr__, args=(name,))
+
+    def __delitem__(self, key):
+        """Delete the item on the proxied object and mark state dirty.
+
+        Args:
+            key: The key of the item.
+        """
+        self._mark_dirty(super().__delitem__, args=(key,))
+
+    def __setitem__(self, key, value):
+        """Set the item on the proxied object and mark state dirty.
+
+        Args:
+            key: The key of the item.
+            value: The value of the item.
+        """
+        self._mark_dirty(super().__setitem__, args=(key, value))
+
+    def __setattr__(self, name, value):
+        """Set the attribute on the proxied object and mark state dirty.
+
+        If the attribute starts with "_self_", then the state is NOT marked
+        dirty as these are internal proxy attributes.
+
+        Args:
+            name: The name of the attribute.
+            value: The value of the attribute.
+        """
+        if name.startswith("_self_"):
+            # Special case attributes of the proxy itself, not applied to the wrapped object.
+            super().__setattr__(name, value)
+            return
+        self._mark_dirty(super().__setattr__, args=(name, value))

+ 0 - 272
reflex/vars.py

@@ -15,7 +15,6 @@ from typing import (
     Dict,
     Dict,
     List,
     List,
     Optional,
     Optional,
-    Set,
     Tuple,
     Tuple,
     Type,
     Type,
     Union,
     Union,
@@ -1321,277 +1320,6 @@ def cached_var(fget: Callable[[Any], Any]) -> ComputedVar:
     return cvar
     return cvar
 
 
 
 
-class ReflexList(list):
-    """A custom list that reflex can detect its mutation."""
-
-    def __init__(
-        self,
-        original_list: List,
-        reassign_field: Callable = lambda _field_name: None,
-        field_name: str = "",
-    ):
-        """Initialize ReflexList.
-
-        Args:
-            original_list (List): The original list
-            reassign_field (Callable):
-                The method in the parent state to reassign the field.
-                Default to be a no-op function
-            field_name (str): the name of field in the parent state
-        """
-        self._reassign_field = lambda: reassign_field(field_name)
-
-        super().__init__(original_list)
-
-    def append(self, *args, **kwargs):
-        """Append.
-
-        Args:
-            args: The args passed.
-            kwargs: The kwargs passed.
-        """
-        super().append(*args, **kwargs)
-        self._reassign_field()
-
-    def insert(self, *args, **kwargs):
-        """Insert.
-
-        Args:
-            args: The args passed.
-            kwargs: The kwargs passed.
-        """
-        super().insert(*args, **kwargs)
-        self._reassign_field()
-
-    def __setitem__(self, *args, **kwargs):
-        """Set item.
-
-        Args:
-            args: The args passed.
-            kwargs: The kwargs passed.
-        """
-        super().__setitem__(*args, **kwargs)
-        self._reassign_field()
-
-    def __delitem__(self, *args, **kwargs):
-        """Delete item.
-
-        Args:
-            args: The args passed.
-            kwargs: The kwargs passed.
-        """
-        super().__delitem__(*args, **kwargs)
-        self._reassign_field()
-
-    def clear(self, *args, **kwargs):
-        """Remove all item from the list.
-
-        Args:
-            args: The args passed.
-            kwargs: The kwargs passed.
-        """
-        super().clear(*args, **kwargs)
-        self._reassign_field()
-
-    def extend(self, *args, **kwargs):
-        """Add all item of a list to the end of the list.
-
-        Args:
-            args: The args passed.
-            kwargs: The kwargs passed.
-        """
-        super().extend(*args, **kwargs)
-        self._reassign_field() if hasattr(self, "_reassign_field") else None
-
-    def pop(self, *args, **kwargs):
-        """Remove an element.
-
-        Args:
-            args: The args passed.
-            kwargs: The kwargs passed.
-        """
-        super().pop(*args, **kwargs)
-        self._reassign_field()
-
-    def remove(self, *args, **kwargs):
-        """Remove an element.
-
-        Args:
-            args: The args passed.
-            kwargs: The kwargs passed.
-        """
-        super().remove(*args, **kwargs)
-        self._reassign_field()
-
-
-class ReflexDict(dict):
-    """A custom dict that reflex can detect its mutation."""
-
-    def __init__(
-        self,
-        original_dict: Dict,
-        reassign_field: Callable = lambda _field_name: None,
-        field_name: str = "",
-    ):
-        """Initialize ReflexDict.
-
-        Args:
-            original_dict: The original dict
-            reassign_field:
-                The method in the parent state to reassign the field.
-                Default to be a no-op function
-            field_name: the name of field in the parent state
-        """
-        super().__init__(original_dict)
-        self._reassign_field = lambda: reassign_field(field_name)
-
-    def clear(self):
-        """Remove all item from the list."""
-        super().clear()
-
-        self._reassign_field()
-
-    def setdefault(self, *args, **kwargs):
-        """Return value of key if or set default.
-
-        Args:
-            args: The args passed.
-            kwargs: The kwargs passed.
-        """
-        super().setdefault(*args, **kwargs)
-        self._reassign_field()
-
-    def popitem(self):
-        """Pop last item."""
-        super().popitem()
-        self._reassign_field()
-
-    def pop(self, k, d=None):
-        """Remove an element.
-
-        Args:
-            k: The args passed.
-            d: The kwargs passed.
-        """
-        super().pop(k, d)
-        self._reassign_field()
-
-    def update(self, *args, **kwargs):
-        """Update the dict with another dict.
-
-        Args:
-            args: The args passed.
-            kwargs: The kwargs passed.
-        """
-        super().update(*args, **kwargs)
-        self._reassign_field()
-
-    def __setitem__(self, *args, **kwargs):
-        """Set an item in the dict.
-
-        Args:
-            args: The args passed.
-            kwargs: The kwargs passed.
-        """
-        super().__setitem__(*args, **kwargs)
-        self._reassign_field() if hasattr(self, "_reassign_field") else None
-
-    def __delitem__(self, *args, **kwargs):
-        """Delete an item in the dict.
-
-        Args:
-            args: The args passed.
-            kwargs: The kwargs passed.
-        """
-        super().__delitem__(*args, **kwargs)
-        self._reassign_field()
-
-
-class ReflexSet(set):
-    """A custom set that reflex can detect its mutation."""
-
-    def __init__(
-        self,
-        original_set: Set,
-        reassign_field: Callable = lambda _field_name: None,
-        field_name: str = "",
-    ):
-        """Initialize ReflexSet.
-
-        Args:
-            original_set (Set): The original set
-            reassign_field (Callable):
-                The method in the parent state to reassign the field.
-                Default to be a no-op function
-            field_name (str): the name of field in the parent state
-        """
-        self._reassign_field = lambda: reassign_field(field_name)
-
-        super().__init__(original_set)
-
-    def add(self, *args, **kwargs):
-        """Add an element to set.
-
-        Args:
-            args: The args passed.
-            kwargs: The kwargs passed.
-        """
-        super().add(*args, **kwargs)
-        self._reassign_field()
-
-    def remove(self, *args, **kwargs):
-        """Remove an element.
-        Raise key error if element not found.
-
-        Args:
-            args: The args passed.
-            kwargs: The kwargs passed.
-        """
-        super().remove(*args, **kwargs)
-        self._reassign_field()
-
-    def discard(self, *args, **kwargs):
-        """Remove an element.
-        Does not raise key error if element not found.
-
-        Args:
-            args: The args passed.
-            kwargs: The kwargs passed.
-        """
-        super().discard(*args, **kwargs)
-        self._reassign_field()
-
-    def pop(self, *args, **kwargs):
-        """Remove an element.
-
-        Args:
-            args: The args passed.
-            kwargs: The kwargs passed.
-        """
-        super().pop(*args, **kwargs)
-        self._reassign_field()
-
-    def clear(self, *args, **kwargs):
-        """Remove all elements from the set.
-
-        Args:
-            args: The args passed.
-            kwargs: The kwargs passed.
-        """
-        super().clear(*args, **kwargs)
-        self._reassign_field()
-
-    def update(self, *args, **kwargs):
-        """Adds elements from an iterable to the set.
-
-        Args:
-            args: The args passed.
-            kwargs: The kwargs passed.
-        """
-        super().update(*args, **kwargs)
-        self._reassign_field()
-
-
 class ImportVar(Base):
 class ImportVar(Base):
     """An import var."""
     """An import var."""
 
 

+ 0 - 36
reflex/vars.pyi

@@ -116,42 +116,6 @@ class ComputedVar(Var):
 
 
 def cached_var(fget: Callable[[Any], Any]) -> ComputedVar: ...
 def cached_var(fget: Callable[[Any], Any]) -> ComputedVar: ...
 
 
-class ReflexList(list):
-    def __init__(
-        self, original_list: List, reassign_field: Callable = ..., field_name: str = ...
-    ) -> None: ...
-    def append(self, *args, **kwargs) -> None: ...
-    def insert(self, *args, **kwargs) -> None: ...
-    def __setitem__(self, *args, **kwargs) -> None: ...
-    def __delitem__(self, *args, **kwargs) -> None: ...
-    def clear(self, *args, **kwargs) -> None: ...
-    def extend(self, *args, **kwargs) -> None: ...
-    def pop(self, *args, **kwargs) -> None: ...
-    def remove(self, *args, **kwargs) -> None: ...
-
-class ReflexDict(dict):
-    def __init__(
-        self, original_dict: Dict, reassign_field: Callable = ..., field_name: str = ...
-    ) -> None: ...
-    def clear(self) -> None: ...
-    def setdefault(self, *args, **kwargs) -> None: ...
-    def popitem(self) -> None: ...
-    def pop(self, k, d: Incomplete | None = ...) -> None: ...
-    def update(self, *args, **kwargs) -> None: ...
-    def __setitem__(self, *args, **kwargs) -> None: ...
-    def __delitem__(self, *args, **kwargs) -> None: ...
-
-class ReflexSet(set):
-    def __init__(
-        self, original_set: Set, reassign_field: Callable = ..., field_name: str = ...
-    ) -> None: ...
-    def add(self, *args, **kwargs) -> None: ...
-    def remove(self, *args, **kwargs) -> None: ...
-    def discard(self, *args, **kwargs) -> None: ...
-    def pop(self, *args, **kwargs) -> None: ...
-    def clear(self, *args, **kwargs) -> None: ...
-    def update(self, *args, **kwargs) -> None: ...
-
 class ImportVar(Base):
 class ImportVar(Base):
     tag: Optional[str]
     tag: Optional[str]
     is_default: Optional[bool] = False
     is_default: Optional[bool] = False

+ 12 - 0
tests/conftest.py

@@ -547,6 +547,16 @@ def mutable_state():
         A state object.
         A state object.
     """
     """
 
 
+    class OtherBase(rx.Base):
+        bar: str = ""
+
+    class CustomVar(rx.Base):
+        foo: str = ""
+        array: List[str] = []
+        hashmap: Dict[str, str] = {}
+        test_set: Set[str] = set()
+        custom: OtherBase = OtherBase()
+
     class MutableTestState(rx.State):
     class MutableTestState(rx.State):
         """A test state."""
         """A test state."""
 
 
@@ -561,6 +571,8 @@ def mutable_state():
             "third_key": {"key": "value"},
             "third_key": {"key": "value"},
         }
         }
         test_set: Set[Union[str, int]] = {1, 2, 3, 4, "five"}
         test_set: Set[Union[str, int]] = {1, 2, 3, 4, "five"}
+        custom: CustomVar = CustomVar()
+        _be_custom: CustomVar = CustomVar()
 
 
         def reassign_mutables(self):
         def reassign_mutables(self):
             self.array = ["modified_value", [1, 2, 3], {"mod_key": "mod_value"}]
             self.array = ["modified_value", [1, 2, 3], {"mod_key": "mod_value"}]

+ 227 - 19
tests/test_state.py

@@ -2,6 +2,7 @@ from __future__ import annotations
 
 
 import datetime
 import datetime
 import functools
 import functools
+import sys
 from typing import Dict, List
 from typing import Dict, List
 
 
 import pytest
 import pytest
@@ -11,9 +12,9 @@ import reflex as rx
 from reflex.base import Base
 from reflex.base import Base
 from reflex.constants import IS_HYDRATED, RouteVar
 from reflex.constants import IS_HYDRATED, RouteVar
 from reflex.event import Event, EventHandler
 from reflex.event import Event, EventHandler
-from reflex.state import State
+from reflex.state import MutableProxy, State
 from reflex.utils import format
 from reflex.utils import format
-from reflex.vars import BaseVar, ComputedVar, ReflexDict, ReflexList, ReflexSet
+from reflex.vars import BaseVar, ComputedVar
 
 
 
 
 class Object(Base):
 class Object(Base):
@@ -1310,31 +1311,54 @@ def test_setattr_of_mutable_types(mutable_state):
     hashmap = mutable_state.hashmap
     hashmap = mutable_state.hashmap
     test_set = mutable_state.test_set
     test_set = mutable_state.test_set
 
 
-    assert isinstance(array, ReflexList)
-    assert isinstance(array[1], ReflexList)
-    assert isinstance(array[2], ReflexDict)
-
-    assert isinstance(hashmap, ReflexDict)
-    assert isinstance(hashmap["key"], ReflexList)
-    assert isinstance(hashmap["third_key"], ReflexDict)
-
+    assert isinstance(array, MutableProxy)
+    assert isinstance(array, list)
+    assert isinstance(array[1], MutableProxy)
+    assert isinstance(array[1], list)
+    assert isinstance(array[2], MutableProxy)
+    assert isinstance(array[2], dict)
+
+    assert isinstance(hashmap, MutableProxy)
+    assert isinstance(hashmap, dict)
+    assert isinstance(hashmap["key"], MutableProxy)
+    assert isinstance(hashmap["key"], list)
+    assert isinstance(hashmap["third_key"], MutableProxy)
+    assert isinstance(hashmap["third_key"], dict)
+
+    assert isinstance(test_set, MutableProxy)
     assert isinstance(test_set, set)
     assert isinstance(test_set, set)
 
 
+    assert isinstance(mutable_state.custom, MutableProxy)
+    assert isinstance(mutable_state.custom.array, MutableProxy)
+    assert isinstance(mutable_state.custom.array, list)
+    assert isinstance(mutable_state.custom.hashmap, MutableProxy)
+    assert isinstance(mutable_state.custom.hashmap, dict)
+    assert isinstance(mutable_state.custom.test_set, MutableProxy)
+    assert isinstance(mutable_state.custom.test_set, set)
+    assert isinstance(mutable_state.custom.custom, MutableProxy)
+
     mutable_state.reassign_mutables()
     mutable_state.reassign_mutables()
 
 
     array = mutable_state.array
     array = mutable_state.array
     hashmap = mutable_state.hashmap
     hashmap = mutable_state.hashmap
     test_set = mutable_state.test_set
     test_set = mutable_state.test_set
 
 
-    assert isinstance(array, ReflexList)
-    assert isinstance(array[1], ReflexList)
-    assert isinstance(array[2], ReflexDict)
-
-    assert isinstance(hashmap, ReflexDict)
-    assert isinstance(hashmap["mod_key"], ReflexList)
-    assert isinstance(hashmap["mod_third_key"], ReflexDict)
-
-    assert isinstance(test_set, ReflexSet)
+    assert isinstance(array, MutableProxy)
+    assert isinstance(array, list)
+    assert isinstance(array[1], MutableProxy)
+    assert isinstance(array[1], list)
+    assert isinstance(array[2], MutableProxy)
+    assert isinstance(array[2], dict)
+
+    assert isinstance(hashmap, MutableProxy)
+    assert isinstance(hashmap, dict)
+    assert isinstance(hashmap["mod_key"], MutableProxy)
+    assert isinstance(hashmap["mod_key"], list)
+    assert isinstance(hashmap["mod_third_key"], MutableProxy)
+    assert isinstance(hashmap["mod_third_key"], dict)
+
+    assert isinstance(test_set, MutableProxy)
+    assert isinstance(test_set, set)
 
 
 
 
 def test_error_on_state_method_shadow():
 def test_error_on_state_method_shadow():
@@ -1375,3 +1399,187 @@ def test_state_with_invalid_yield():
         "must only return/yield: None, Events or other EventHandlers"
         "must only return/yield: None, Events or other EventHandlers"
         in err.value.args[0]
         in err.value.args[0]
     )
     )
+
+
+def test_mutable_list(mutable_state):
+    """Test that mutable lists are tracked correctly.
+
+    Args:
+        mutable_state: A test state.
+    """
+    assert not mutable_state.dirty_vars
+
+    def assert_array_dirty():
+        assert mutable_state.dirty_vars == {"array"}
+        mutable_state._clean()
+        assert not mutable_state.dirty_vars
+
+    # Test all list operations
+    mutable_state.array.append(42)
+    assert_array_dirty()
+    mutable_state.array.extend([1, 2, 3])
+    assert_array_dirty()
+    mutable_state.array.insert(0, 0)
+    assert_array_dirty()
+    mutable_state.array.pop()
+    assert_array_dirty()
+    mutable_state.array.remove(42)
+    assert_array_dirty()
+    mutable_state.array.clear()
+    assert_array_dirty()
+    mutable_state.array += [1, 2, 3]
+    assert_array_dirty()
+    mutable_state.array.reverse()
+    assert_array_dirty()
+    mutable_state.array.sort()
+    assert_array_dirty()
+    mutable_state.array[0] = 666
+    assert_array_dirty()
+    del mutable_state.array[0]
+    assert_array_dirty()
+
+    # Test nested list operations
+    mutable_state.array[0] = [1, 2, 3]
+    assert_array_dirty()
+    mutable_state.array[0].append(4)
+    assert_array_dirty()
+    assert isinstance(mutable_state.array[0], MutableProxy)
+
+
+def test_mutable_dict(mutable_state):
+    """Test that mutable dicts are tracked correctly.
+
+    Args:
+        mutable_state: A test state.
+    """
+    assert not mutable_state.dirty_vars
+
+    def assert_hashmap_dirty():
+        assert mutable_state.dirty_vars == {"hashmap"}
+        mutable_state._clean()
+        assert not mutable_state.dirty_vars
+
+    # Test all dict operations
+    mutable_state.hashmap.update({"new_key": 43})
+    assert_hashmap_dirty()
+    mutable_state.hashmap.setdefault("another_key", 66)
+    assert_hashmap_dirty()
+    mutable_state.hashmap.pop("new_key")
+    assert_hashmap_dirty()
+    mutable_state.hashmap.popitem()
+    assert_hashmap_dirty()
+    mutable_state.hashmap.clear()
+    assert_hashmap_dirty()
+    mutable_state.hashmap["new_key"] = 42
+    assert_hashmap_dirty()
+    del mutable_state.hashmap["new_key"]
+    assert_hashmap_dirty()
+    if sys.version_info >= (3, 9):
+        mutable_state.hashmap |= {"new_key": 44}
+        assert_hashmap_dirty()
+
+    # Test nested dict operations
+    mutable_state.hashmap["array"] = []
+    assert_hashmap_dirty()
+    mutable_state.hashmap["array"].append(1)
+    assert_hashmap_dirty()
+    mutable_state.hashmap["dict"] = {}
+    assert_hashmap_dirty()
+    mutable_state.hashmap["dict"]["key"] = 42
+    assert_hashmap_dirty()
+    mutable_state.hashmap["dict"]["dict"] = {}
+    assert_hashmap_dirty()
+    mutable_state.hashmap["dict"]["dict"]["key"] = 43
+    assert_hashmap_dirty()
+
+
+def test_mutable_set(mutable_state):
+    """Test that mutable sets are tracked correctly.
+
+    Args:
+        mutable_state: A test state.
+    """
+    assert not mutable_state.dirty_vars
+
+    def assert_set_dirty():
+        assert mutable_state.dirty_vars == {"test_set"}
+        mutable_state._clean()
+        assert not mutable_state.dirty_vars
+
+    # Test all set operations
+    mutable_state.test_set.add(42)
+    assert_set_dirty()
+    mutable_state.test_set.update([1, 2, 3])
+    assert_set_dirty()
+    mutable_state.test_set.remove(42)
+    assert_set_dirty()
+    mutable_state.test_set.discard(3)
+    assert_set_dirty()
+    mutable_state.test_set.pop()
+    assert_set_dirty()
+    mutable_state.test_set.intersection_update([1, 2, 3])
+    assert_set_dirty()
+    mutable_state.test_set.difference_update([99])
+    assert_set_dirty()
+    mutable_state.test_set.symmetric_difference_update([102, 99])
+    assert_set_dirty()
+    mutable_state.test_set |= {1, 2, 3}
+    assert_set_dirty()
+    mutable_state.test_set &= {2, 3, 4}
+    assert_set_dirty()
+    mutable_state.test_set -= {2}
+    assert_set_dirty()
+    mutable_state.test_set ^= {42}
+    assert_set_dirty()
+    mutable_state.test_set.clear()
+    assert_set_dirty()
+
+
+def test_mutable_custom(mutable_state):
+    """Test that mutable custom types derived from Base are tracked correctly.
+
+    Args:
+        mutable_state: A test state.
+    """
+    assert not mutable_state.dirty_vars
+
+    def assert_custom_dirty():
+        assert mutable_state.dirty_vars == {"custom"}
+        mutable_state._clean()
+        assert not mutable_state.dirty_vars
+
+    mutable_state.custom.foo = "bar"
+    assert_custom_dirty()
+    mutable_state.custom.array.append(42)
+    assert_custom_dirty()
+    mutable_state.custom.hashmap["key"] = 68
+    assert_custom_dirty()
+    mutable_state.custom.test_set.add(42)
+    assert_custom_dirty()
+    mutable_state.custom.custom.bar = "baz"
+    assert_custom_dirty()
+
+
+def test_mutable_backend(mutable_state):
+    """Test that mutable backend vars are tracked correctly.
+
+    Args:
+        mutable_state: A test state.
+    """
+    assert not mutable_state.dirty_vars
+
+    def assert_custom_dirty():
+        assert mutable_state.dirty_vars == {"_be_custom"}
+        mutable_state._clean()
+        assert not mutable_state.dirty_vars
+
+    mutable_state._be_custom.foo = "bar"
+    assert_custom_dirty()
+    mutable_state._be_custom.array.append(42)
+    assert_custom_dirty()
+    mutable_state._be_custom.hashmap["key"] = 68
+    assert_custom_dirty()
+    mutable_state._be_custom.test_set.add(42)
+    assert_custom_dirty()
+    mutable_state._be_custom.custom.bar = "baz"
+    assert_custom_dirty()

+ 0 - 34
tests/test_var.py

@@ -2,7 +2,6 @@ import json
 import typing
 import typing
 from typing import Dict, List, Set, Tuple
 from typing import Dict, List, Set, Tuple
 
 
-import cloudpickle
 import pytest
 import pytest
 from pandas import DataFrame
 from pandas import DataFrame
 
 
@@ -12,9 +11,6 @@ from reflex.vars import (
     BaseVar,
     BaseVar,
     ComputedVar,
     ComputedVar,
     ImportVar,
     ImportVar,
-    ReflexDict,
-    ReflexList,
-    ReflexSet,
     Var,
     Var,
     get_local_storage,
     get_local_storage,
 )
 )
@@ -586,36 +582,6 @@ def test_computed_var_with_annotation_error(request, fixture, full_name):
     )
     )
 
 
 
 
-def test_pickleable_rx_list():
-    """Test that ReflexList is pickleable."""
-    rx_list = ReflexList(
-        original_list=[1, 2, 3], reassign_field=lambda x: x, field_name="random"
-    )
-
-    pickled_list = cloudpickle.dumps(rx_list)
-    assert cloudpickle.loads(pickled_list) == rx_list
-
-
-def test_pickleable_rx_dict():
-    """Test that ReflexDict is pickleable."""
-    rx_dict = ReflexDict(
-        original_dict={1: 2, 3: 4}, reassign_field=lambda x: x, field_name="random"
-    )
-
-    pickled_dict = cloudpickle.dumps(rx_dict)
-    assert cloudpickle.loads(pickled_dict) == rx_dict
-
-
-def test_pickleable_rx_set():
-    """Test that ReflexSet is pickleable."""
-    rx_set = ReflexSet(
-        original_set={1, 2, 3}, reassign_field=lambda x: x, field_name="random"
-    )
-
-    pickled_set = cloudpickle.dumps(rx_set)
-    assert cloudpickle.loads(pickled_set) == rx_set
-
-
 @pytest.mark.parametrize(
 @pytest.mark.parametrize(
     "import_var,expected",
     "import_var,expected",
     zip(
     zip(