浏览代码

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_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-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-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"},
     {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_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-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-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-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"},
@@ -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_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-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-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"},
     {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_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-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-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"},
     {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"},
@@ -2189,6 +2179,90 @@ files = [
     {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]]
 name = "wsproto"
 version = "1.2.0"
@@ -2221,4 +2295,4 @@ testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more
 [metadata]
 lock-version = "2.0"
 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"
 distro = {version = "^1.8.0", platform = "linux"}
 python-engineio = "!=4.6.0"
+wrapt = "^1.15.0"
 
 [tool.poetry.group.dev.dependencies]
 pytest = "^7.1.2"

+ 169 - 99
reflex/state.py

@@ -22,18 +22,18 @@ from typing import (
     Sequence,
     Set,
     Type,
-    Union,
 )
 
 import cloudpickle
 import pydantic
+import wrapt
 from redis import Redis
 
 from reflex import constants
 from reflex.base import Base
 from reflex.event import Event, EventHandler, EventSpec, fix_events, window_alert
 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]
 
@@ -129,32 +129,6 @@ class State(Base, ABC, extra=pydantic.Extra.allow):
         # Create a fresh copy of the backend variables for this instance
         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):
         """Initialize event handlers.
 
@@ -178,20 +152,6 @@ class State(Base, ABC, extra=pydantic.Extra.allow):
         if state.parent_state is not None:
             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:
         """Get the string representation of the state.
 
@@ -636,9 +596,20 @@ class State(Base, ABC, extra=pydantic.Extra.allow):
         }
         if name in inherited_vars:
             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):
         """Set the attribute.
@@ -649,18 +620,16 @@ class State(Base, ABC, extra=pydantic.Extra.allow):
             name: The name 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.
         inherited_vars = {**self.inherited_vars, **self.inherited_backend_vars}
         if name in inherited_vars:
             setattr(self.parent_state, name, value)
             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":
             self._backend_vars.__setitem__(name, value)
             self.dirty_vars.add(name)
@@ -1087,54 +1056,6 @@ class StateManager(Base):
         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:
     """Base class for client-side storage."""
 
@@ -1234,3 +1155,152 @@ class LocalStorage(ClientStorageBase, str):
             inst = super().__new__(cls, object)
         inst.name = name
         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,
     List,
     Optional,
-    Set,
     Tuple,
     Type,
     Union,
@@ -1321,277 +1320,6 @@ def cached_var(fget: Callable[[Any], Any]) -> ComputedVar:
     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):
     """An import var."""
 

+ 0 - 36
reflex/vars.pyi

@@ -116,42 +116,6 @@ class ComputedVar(Var):
 
 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):
     tag: Optional[str]
     is_default: Optional[bool] = False

+ 12 - 0
tests/conftest.py

@@ -547,6 +547,16 @@ def mutable_state():
         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):
         """A test state."""
 
@@ -561,6 +571,8 @@ def mutable_state():
             "third_key": {"key": "value"},
         }
         test_set: Set[Union[str, int]] = {1, 2, 3, 4, "five"}
+        custom: CustomVar = CustomVar()
+        _be_custom: CustomVar = CustomVar()
 
         def reassign_mutables(self):
             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 functools
+import sys
 from typing import Dict, List
 
 import pytest
@@ -11,9 +12,9 @@ import reflex as rx
 from reflex.base import Base
 from reflex.constants import IS_HYDRATED, RouteVar
 from reflex.event import Event, EventHandler
-from reflex.state import State
+from reflex.state import MutableProxy, State
 from reflex.utils import format
-from reflex.vars import BaseVar, ComputedVar, ReflexDict, ReflexList, ReflexSet
+from reflex.vars import BaseVar, ComputedVar
 
 
 class Object(Base):
@@ -1310,31 +1311,54 @@ def test_setattr_of_mutable_types(mutable_state):
     hashmap = mutable_state.hashmap
     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(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()
 
     array = mutable_state.array
     hashmap = mutable_state.hashmap
     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():
@@ -1375,3 +1399,187 @@ def test_state_with_invalid_yield():
         "must only return/yield: None, Events or other EventHandlers"
         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
 from typing import Dict, List, Set, Tuple
 
-import cloudpickle
 import pytest
 from pandas import DataFrame
 
@@ -12,9 +11,6 @@ from reflex.vars import (
     BaseVar,
     ComputedVar,
     ImportVar,
-    ReflexDict,
-    ReflexList,
-    ReflexSet,
     Var,
     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(
     "import_var,expected",
     zip(