diff --git a/LICENSE b/LICENSE index 206754f..4275a6f 100644 --- a/LICENSE +++ b/LICENSE @@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. \ No newline at end of file +SOFTWARE. diff --git a/README.md b/README.md index 4fd0ff8..a867a85 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ React Query có cơ chế caching hơi khác một chút so với RTK Query, nê - `inactive`: là khi data đó không còn component nào subcribe cả ```tsx -const result = useQuery({ queryKey: ['todos'], queryFn: fetchTodoList }) +const result = useQuery({ queryKey: ["todos"], queryFn: fetchTodoList }); ``` `result` là một object chứa một vài state rất quan trọng: `status`, `fetchStatus`,... @@ -88,13 +88,13 @@ Giả sử chúng ta dùng `cacheTime` mặc định là **5 phút** và `staleT ```jsx function A() { - const result = useQuery({ queryKey: ['todos'], queryFn: fetchTodos }) + const result = useQuery({ queryKey: ["todos"], queryFn: fetchTodos }); } function B() { - const result = useQuery({ queryKey: ['todos'], queryFn: fetchTodos }) + const result = useQuery({ queryKey: ["todos"], queryFn: fetchTodos }); } function C() { - const result = useQuery({ queryKey: ['todos'], queryFn: fetchTodos }) + const result = useQuery({ queryKey: ["todos"], queryFn: fetchTodos }); } ``` diff --git a/server/db.json b/server/db.json index 690b7df..27b5450 100644 --- a/server/db.json +++ b/server/db.json @@ -1,45 +1,5 @@ { "students": [ - { - "id": 1, - "first_name": "Martie", - "last_name": "Kite", - "email": "mkite0@statcounter.com", - "gender": "Female", - "country": "Portugal", - "avatar": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAALzSURBVDjLdZPdT5JhGMaZf4Bbh3ZaBzn/gU46MM1iNrf8WExNVyz1IDUOHCFpfq4PmmZmtTTT8GMJNGAO0hgi8a0oIBAGfoS4N12gmUpmL1fPy9LGpu/27N2zPdfvvu7rfh4WANZRa2pqims2mycMBsME+bjHnUvYOJ3O1JmZmWQiTiLi9dXVVYRCIUil0nWVSpWkVCqTZTJZ6pEAIu4mYtpms1EmkylFr9dTi4uLWFhYABFSCoUihYCo4eFhWiwWdycAHA6HMBAIIBwOw+/3g4i3tVptzOv1wu12Qy6Xx4h4mzgDRVFQq9UQiUTCQwCpWkd6jTEQRjA/Px8HHQCYvcvlAgOwWCwQCAQxHo9Xl9CC0WhsZQ5PT0/DarVifHwcPT096O/vZxyBtAXbx6fwDXLgV7TC8ToLzq60EhYh2kn1FZ1OF/H5fLDb7dBoNGhqajLx+fzM+vr6zPb2dtPMxCNQWj42XaPAz1VsuKTQiy7/ZhExvba2FrfLVCZBYnBwEC0tLZkH7qzS7Kbv7nvYW1GC0omwO/cef5YNxEUZWGTGdDAYjPfI2GQckMTR2dkZB4TUpy9F3Hdj9K4Buwu3ELZV4rOYC9ebMvAqSmkWGYmdWFzp6+uLzM7OxiFMDr29vabJoSu1kTkB6KgZO4FSRINF2PLWwtOVQd/m5n/Ly8uzH4ZIxtRKgsTk5CSYv17+GNQU/5+4BNGvhfjhFmL++UW01XB6E+6BRCKpGxoaink8nrjYMNoB71gN6F09oktc/ApewyZx8oWI7Z/GUFVVFSsvL/8/xoGBASGTPHPz3rbx4FHVwql+gpC1ADtLRQg77sDZcSEmE7+IZ9XQ0IDc3Fxhwj1obGzsrq6upkceZgBby/C9yoe29iSML9n4cO8sLRGcT+dwOFROTg7NZrO7j3xMBJDafDNtf8/8DHvWDlhb0zFScQqV13MjhYWFScXFxckEkHrsa2RWwbkT0fulZ/Y1D9j0u+asjRtXsy3E7rHP+S+qJels2qSd5wAAAABJRU5ErkJggg==", - "btc_address": "1JADzZcCizitPooetjtrTpFUuThFmi5qvi" - }, - { - "id": 2, - "first_name": "Sherilyn", - "last_name": "Paddon", - "email": "spaddon1@washington.edu", - "gender": "Female", - "country": "Cameroon", - "avatar": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAIsSURBVDjLpZPNbhJRFMf/w9fwNUiVlZWPncSmqemm7FjZaGJcsdCkS17AJ8AH6BvU6BKfgFIXLqArTQngSoPUDSGEhAUCLQN3xnPOMLSJGBdOcnK45J7f+Z//vVezbRv/8/luLxqNxvvlcnm0WCx8lEFZwjRNzh8KhcKrjYBWq3WHCt7G4/GCrocxpyJYFkQbKdzaiuPs7PQlrf4ENJvNp1ycSCS2I5EILn/2wN0ty4ZlWwSyoQg2n883j0CbT1OplCyurq6xoOKvrQsCWOt4cvhMxtgI4BkDgQDG47HTWSk82nks3e1Vdzb6rwqYrKiIN/GBsPRW8wtlJd35ixl5JJNJVKvVIf0XpP0/KH+kfCIKHK9sATFgZ3dfuosH9gJGNIx8Po9oNJrweDyYTqd7g8Fgr91uv/C4szkKWDJBlDP70LjEtZoik8lA1/W1bB6ZFVHDhwJYF6sb4xj07tsJzKUp3vR6PQyHQ/T7fVBnuR+s2MPmuLeRJcvxWU5+fe8NQlpYOrJ0PubRaIRgMIhYLCYAMZE3eL1eZ27FrhOAVdEathK4O6rrGX+sjBV8rtfrmEwmMAwDwZBON1ATkFoZ627m36trfQMoFosHnU7neaVSuTg/r0EPaMikHyCV2kY6eZ/tFfmscjabwe/3IxQKCYDH126/xlKpdEj0Y4pd9zFls1nkcjmk0+m1F6yi2+2iVqt91/71nMvlcoCkFymOKO4S2EedfxHkE8Xxb6/5rMKMZgtBAAAAAElFTkSuQmCC", - "btc_address": "14dvHxKVi6Cqic1dmitcfXUsikr6X7URgt" - }, - { - "id": 3, - "first_name": "Winona", - "last_name": "Kennerley", - "email": "wkennerley2@slate.com", - "gender": "Female", - "country": "Portugal", - "avatar": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAKOSURBVDjLpVPbThNRFOUD/IJ+B6gvJr740mg0JsQUX0yMiUl9MJJIwGBsMOgDQSEz0mANhEIKoqhBBTEpahm5SWxLKTO9WBAKBTqtw9DpDbqcfUprvbw5ycpJTtZae+09+1QBqKrEqS4YdNToMOowHcJ4eGf4k18pPKKjuumFYh75vMbPuIPC4tKy7PX55akFUXj6McI3DCfMxCHubwaH4hODrg2LdzksxeIKVC2H/EGBYTeVRXQrgekFn9QzEbYQt2RSMqgmsU/8pqYzedCnZfeRUHOQdexl9tmdqmUxNedRbWMBMqlmBtQXxabKJfGOksW952HEklmG1mdhRBOZssnE5LR0fSBG7RjIoGZE+M5T7JJ4I5mBqW0Bqztp3OyTcLyuBde6/ViLpxlHjGyCG57hSUsGxumvAYF6pqhUcV1O4+IDD+aDCs7dfsfOM02jzFBJ5bGd1NA79FogLRmYPItLMg0rvptjBkSsa3ez864e/2htM24NBBDZ0liKbP4AA44hmbTMwO31MQOKT6T6XhHHLtxhsYMbKZy3OBGOaQzFFDn09vWXDYyf5v1CUs1gV8tjNvADZ5vH8SWk4HTjK4jRvbJBaDPF/kxwPQnO5ii3UONwhvjVaByFArCyrbGpn7zCoak/wBLUtgrsDERTyO0XMDnrh8X6tjxEQ/3gjtnpmpNkpThlqnajR2QVSWj/EGVJqEBIr97ZbZcuW4PF31haJO6l1zL63qXGEkVici/P+qVEtExUObieQIdtUG3kxn4tUuUqtznmLPbhUWnGE8JqTGEzoZ79KzLGXV60cT1SQ+ebv1e58jFdfbJibnk8zrd39QncI6v8sJOX73fYhMaOEf4SL/77Mf3Pc/4JB00Hw5wBy+AAAAAASUVORK5CYII=", - "btc_address": "1QFU1svgpSfvKrXsLHmECf92D9vn6tNwoy" - }, - { - "id": 4, - "first_name": "Urbanus", - "last_name": "Kille", - "email": "ukille3@google.ca", - "gender": "Male", - "country": "Solomon Islands", - "avatar": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHBSURBVDjLlVM9aMJQEP6eNEF0sbiUouLgoLRkKXS1IG4dC6Xg2LXQRXATHbqVzq4iQjc3sVscnUSnYIdIB9GC4L/xL333IEFsBj04jpf77nt3l+8x0zRxaMViMbTdbtXVahVer9dYLBY/0+k0mcvltEPsGRzMMIyPQCAQ9ng8IAJd14OdTuedp+4PsS4ngslkctFoNNBsNgWB2+3GaDQKOWEdCTgY2WyW9Xo9QbBcLoUfTSDLsoiMMUFgkRxNwHeAdDpt+nw+8EUKp29O5rhEvnEoigJJktBqteD3+0/rgINNulHTNCjzGR5++1Bvb67x+vLF/dmxg3K5HOZB2+12MncxfzAYxJ25wcXjE5ixZCu9m/wufybfUqnLUqmUtwmomAtKi0ajcrVaxWAwQKFQEHOfK1dQajUwrwdSrw8ZEiKRSC4ej0NV1TwjJXI2IxaLyZwA4/FYFHL12T6fz+3o9XrhcrmQyWTQbreZ6IAnZS5dVCoVEpFYmFVEPpvNxJm+0zmRSIhoj0AJunU4HNogq3C/EwtHuqBfaxNQkhJ8NpGwAPtxs9n8c5ug2+2iXq/bojl0S41URKPuv2Dm9JxPsT8W0mO2IJm2EgAAAABJRU5ErkJggg==", - "btc_address": "1PWkrbbLAqyjbAa9H2wFMKuyWG5bd4TgpN" - }, { "id": 5, "first_name": "Shea", @@ -70,26 +30,6 @@ "avatar": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAQAAAC1+jfqAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAGSSURBVCjPVVFNSwJhEF78Ad79Cf6PvXQRsotUlzKICosuRYmR2RJR0KE6lBFFZVEbpFBSqKu2rum6llFS9HHI4iUhT153n6ZtIWMOM+/MM88z7wwH7s9Ub16SJcnbmrNcxVm2q7Z8/QPvEOtntpj92NkCqITLepEpjix7xQtiLOoQ2b6+E7YAN/5nfOEJ2WbKqOIOJ4bYVMEQx4LfBBQDsvFMhUcCVU1/CxVXmDBGA5ZETrhDCQVcYAPbyEJBhvrnBVPiSpNr6cYDNCQwo4zzU/ySckkgDYuNuVpI42T9k4gLKGMPs/xPzzovQiY2hQYe0jlJfyNNhTqiWDYBq/wBMcSRpnyPzu1oS7WtxjVBSthU1vgVksiQ3Dn6Gp5ah2YOKQo5GiuHPA6xT1EKpxQNCNYejgIR457KKio0S56YckjSa9jo//3mrj+BV0QQagqGTOo+Y7gZIf1puP3WHoLhEb2PjTlCTCWGXtbp8DCX3hZuOdaIc9A+aQvWk4ihq95p67a7nP+u+Ws+r0dql9z/zv0NCYhdCPKZ7oYAAAAASUVORK5CYII=", "btc_address": "1CZXCy88sbPxThH3fvpWSA34hhLoVzWBwT" }, - { - "id": 8, - "first_name": "Christean", - "last_name": "McAleese", - "email": "cmcaleese7@earthlink.net", - "gender": "Agender", - "country": "Vietnam", - "avatar": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAJQSURBVDjLpZNLiI1xGMZ//+98Zy6G4zIJkyg0xEyWLlkMC6JkIYoke1kQUrKwsJGFlaVLsbBAuYSJBTEJCTkZQ8clJsYxx5y5fZfzfx+Lk8swbDz1rt635/319rxOEv+j4F/NzzeW5Xval/5zg5NE6c5yyYSkRZhmybRK0ra6prVIYujNBWS6JLOrMnWYtw6ZxszY+Ng5SfTeblNt01oqpTxB7WTCCQupm7oCMmMBQ2mJ6FMHUc8jkt4X1E9v4+vTk8zc9NSFADLlou728sTFx0GGfD9p3z3C3BKcC0l6zpId00LN3C0gT/H+Yczbyh83aGy71W/pIMhIi+dIi1ew6C3IAGHxR+LuMwy9PAAy4t4CMnsIEH4/hkxVXJ+ApeAMG+7AZVLko2pVYpAnyOaoDJWnAaWfBt5AAkuQT3GBB4bBpTjFoAgsrhJ5j3mzPwlUJZBSUAaIgBgRI58iHyMZmEPesiNyIG+YxQT1s8ESkIcggaBKISUgQIaP+7GKlUYamB0tPz9FOH4xLjsJFzpcmOLChKBG4FIaFhyh72U7cbn4bN72wvsRBk3rHu4cfHOdctdZaqasJ6hvIKhJceEgmbEhDc2HiHry9HXdwCfphlGjLG+TBwrXGChcJ9u4GRfOwQWDZBv3ERW7KHVeY/jzuw0tu953jojyr3p9umV+kM3lc82ryc1fQ6a2jnLXTb48uczA2/y61t0fLv7xC7/r1Yl5U+RtRxA27B/X3EbxwfmCTypbW/d8uDvqM/1NncdmTfJJ5aAllb2te7uHR5v5BtnsZt4skcPVAAAAAElFTkSuQmCC", - "btc_address": "19sQuXkrNyVXB2D8i3SbBci4AHnLobpsqs" - }, - { - "id": 9, - "first_name": "Max", - "last_name": "de Tocqueville", - "email": "mdetocqueville8@oaic.gov.au", - "gender": "Agender", - "country": "Indonesia", - "avatar": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAJcSURBVDjLbZPfa85xFMdf36/nebbGBcLaMmtM2DybElsjQm5c+PEnoKSQCzfEnZtduLRdSJFc4UK5sEizSLtQNCYXJMXKlMeG7fv5cc5x8X02w06dzqdPn/M673M6n8TM2H/6WruZ3TaoYPQYhhlgRh5s1lUCwU18GLpxfjVAgfzBMYP15bZVyfjXCcxmkiCHKabwfeIX085QK7RQtRwAO8ptTcmujiZWNZSxnICa5lU1r758cR11tQW2HTjOXwDMlpTbm7n//B2VyhSmCoDOqDDD1Pg+OUXmPHOt2gJJoVRg7cZmWuuXIgJmiqohYogqUY3pLHDrzuP5AIaI8nF8klJaJMsygvNEze8jCygUSyxbWIOazQMAVJQoAecch7a25vJzdZgZ1wffEmqL/JP/R0EQRUIkSsrNx29wIRLFkKhEEoqlEj7mQ50XEKPiQ8ArWFpDUixCamiiqCpeErz8D0irQyREIWYRF4QsClkQXIi4KDgvuKlnfP50iZZ1A5R3j7PvXOeFvxWIElzABcnbEcOLEkWR6ac01r9h84YuVi5dy+DoXYZfP7nYfbJxcTq3heBzgI/KdBB8EFxUpn48YtP6TiQVOhv2Ikmgu9wDcCKdWRgfhegiLihZEELQXL4TKj+/UEwWsX/DKQDO7LnCmhUdALWzMxAxxAsu5J6FiHOK98q3yQqjY8/ofXgYgN4Hh3k/PgKQzc6ANGVLVweWJIgYUQytxsQdZHhkgJ6O7dx71U8pKfD05RBAX2Jm7DzSd9WMo/nPm7P/GFTP1A5BzQtIPMAPoH/48tjZ3wRCz4QMKdc8AAAAAElFTkSuQmCC", - "btc_address": "18U4YqWvv5EGgh9mxUNEswFUkWTDspgKFi" - }, { "id": 10, "first_name": "Fernande", @@ -120,16 +60,6 @@ "avatar": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAALeSURBVDjLjZNLTxNRGIb5A2Xh0rULjJqw1BCDprXQAuICDBEBE7xFJQY1giCKEARCF9rUljuEFiOIBokFhYIFK6Q2Ei6V3oW21BKFdnq/93VmQomACyd5M+d8Oc9zvjM5kwQg6e/Mzs6myeVywcTExLxUKv09PDzsHBwc1EkkElG/WHRq7/qdgUqlYpCwkITDFosFDocDbrcbPp8PLpcLGs0KRC8aIm3CerGA35S8S7ANfzCZTIjH4wgEAqAkZrMZm5ubiEajiEf9iPh1kMteoqW54nNTY03yjoDamYLD4TCoJxKJgJobjUYQBEFLQ/6fCHpXEAsaMD7ai7rHZWJaQMLHybZjXq+XBjweDy2x2WzQarWg6tFoBD6XFiGvhu4iTOZO+fXYvbs305JIWGi1WkFFp9PREHVmSmS32+lxKOgiBWq4Vgeg7S+A4V0DFrrY6HzEmUxKTU3F/+T2pSOwT1WAWHoPeGxwLg3hUzM3QgvUajX0ej0UCgX6+vpogHqvr6/T9bFXFfi1+BAh6wjs8hb4l98iuqYgu7gapwXUogRESahQNSql54/Boa5BzK+A33QLW1/LoBGXYqn3Gi4X5RP7BFQSsGG+H47lB4gF5uAzliBgKYR7pQqmnlxMymXIzM1b2XeEhKg4OwV2VcU2XIyA+QJc6mroW7lQqRTofD2KzOzcjn9+xItZh7EkLSfbnkFgtRRBSxEIshODiIMZpRLfzE6kn+HGWOyME/RF4vP5wunpaWwQVkxLefg+WoXFsWdYV+bDt1qIrYVK6NvP4odaCfPGJhoFXWBmcDt2biKPx2PUP62TDYxIMNjMBNxr0LbnYarqIL60kru2MKFZ+Ain04nu7m4wWezx9PR0xq6fqba2llFZfb+j/spRhOYECCmfQ9lwGkM3DmFmSgzJmx5wuJwwi8USJuBdgkTyTx4INZakxGRNnLjkCTtSXJBJ5JzLUmflcNpIOG3v+j9k/eSwcE1V7wAAAABJRU5ErkJggg==", "btc_address": "15v7JuRYocw4djokjJPQrt7HPohoEjyDMV" }, - { - "id": 13, - "first_name": "Glenine", - "last_name": "Duffit", - "email": "gduffitc@addthis.com", - "gender": "Female", - "country": "Syria", - "avatar": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAQAAAC1+jfqAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAEZSURBVCjPY/jPgB9iEVoqPefllFPdNk2GWBUsVpz9ctL1rkcNW/v+59VhKFgkPfP+xI0dF+uC/jPkWCR/Q1MwX2TGvf7Nretr/UG8BO2I5ygK5olP/dCzpWV+dVAhd+bB+JawrT7ICubIT3nbvaFpVkVqgVDa0diO4CneN91E4Qpmq0560jW/YXp5XB5nyq2YrqCFno9cJeG+mKk48UHHjLruMu8czuSbkfUBizxeucrDw2GGev/71uW1jMVrsq4nPIto8F/g8caFDymgetxbHlVLgDjxnWExPjPdb7sIoYRkk17FywJRECdY1Xux201nMbSgLufO25qyJUY1yNrzsus9JxkscZHMG+kVcN7jqWueowARkUWiAgBEUvolGfpITwAAAABJRU5ErkJggg==", - "btc_address": "17APzB8nXDDo3MVfSRuKR9YneMLrFUAiJM" - }, { "id": 14, "first_name": "Rae", @@ -999,6 +929,36 @@ "country": "Colombia", "avatar": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAACwSURBVDjLY/j//z8DJZhh1ADsBuRPiazJmxLxKa3P/39ki8Mnzwq9GqINyOgPbGhcnfh/y5Wp/y882/W/f3fW//B+3f/m2ZI9RBkQ2+7yfePlCf83Xpv0HwR69qT+79+TDjLgO1EG+FQb/t92Zc5/ZLDp0lSQAf+JMsAyR/p7966k/+27EsCa23cmkOYCoMKW4B6N/727UsA2g2gQn+gwgBrSAcSfQM6G0h2jSRk3BgASP+M7I0sypgAAAABJRU5ErkJggg==", "btc_address": "12tTumUNmtwbrLz3Xf6mDXn3qqBuNN2UPB" + }, + { + "avatar": "123123", + "email": "lspals@gmail.com", + "btc_address": "12312312", + "country": "21312", + "first_name": "123312", + "last_name": "123123", + "gender": "male", + "id": 101 + }, + { + "avatar": "123123", + "email": "lspals@gmail.com", + "btc_address": "12312312", + "country": "213", + "first_name": "123312", + "last_name": "123123", + "gender": "male", + "id": 102 + }, + { + "avatar": "3123", + "email": "lspals@gmail.com", + "btc_address": "3123123", + "country": "12312", + "first_name": "312312", + "last_name": "31231", + "gender": "other", + "id": 103 } ] -} +} \ No newline at end of file diff --git a/server/server.js b/server/server.js index 724d025..6bb9385 100644 --- a/server/server.js +++ b/server/server.js @@ -1,64 +1,64 @@ -const jsonServer = require('json-server') -const server = jsonServer.create() -const router = jsonServer.router('db.json') -const middlewares = jsonServer.defaults() +const jsonServer = require("json-server"); +const server = jsonServer.create(); +const router = jsonServer.router("db.json"); +const middlewares = jsonServer.defaults(); -const PORT = 4000 -const DELAY = 8000 +const PORT = 4000; +const DELAY = 8000; const validateEmail = (email) => { return String(email) .toLowerCase() .match( /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/ - ) -} + ); +}; // Set default middlewares (logger, static, cors and no-cache) -server.use(middlewares) +server.use(middlewares); // To handle POST, PUT and PATCH you need to use a body-parser // You can use the one used by JSON Server -server.use(jsonServer.bodyParser) +server.use(jsonServer.bodyParser); server.use((req, res, next) => { - if (['POST', 'PUT', 'PATCH'].includes(req.method)) { + if (["POST", "PUT", "PATCH"].includes(req.method)) { if (!validateEmail(req.body.email)) { return res.status(422).send({ error: { - email: 'Email không đúng định dạng' - } - }) + email: "Email không đúng định dạng", + }, + }); } - if (req.body.last_name === 'admin') { + if (req.body.last_name === "admin") { return res.status(500).send({ - error: 'Server bị lỗi' - }) + error: "Server bị lỗi", + }); } } setTimeout(() => { - next() - }, DELAY) -}) + next(); + }, DELAY); +}); router.render = (req, res) => { - let data = res.locals.data - const { originalUrl } = req + let data = res.locals.data; + const { originalUrl } = req; if ( - req.method === 'GET' && - (originalUrl === '/students' || /^\/students\?.*$/.test(originalUrl)) + req.method === "GET" && + (originalUrl === "/students" || /^\/students\?.*$/.test(originalUrl)) ) { data = data.map((student) => ({ id: student.id, avatar: student.avatar, last_name: student.last_name, - email: student.email - })) + email: student.email, + })); } - res.jsonp(data) -} + res.jsonp(data); +}; // Use default router -server.use(router) +server.use(router); server.listen(PORT, () => { - console.log(`JSON Server is running at http://localhost:${PORT}`) -}) + console.log(`JSON Server is running at http://localhost:${PORT}`); +}); diff --git a/starter-template/package.json b/starter-template/package.json index fcb6627..1a89da0 100644 --- a/starter-template/package.json +++ b/starter-template/package.json @@ -3,6 +3,8 @@ "version": "0.1.0", "private": true, "dependencies": { + "@tanstack/react-query": "^5.56.2", + "@tanstack/react-query-devtools": "^5.56.2", "@testing-library/jest-dom": "^5.14.1", "@testing-library/react": "^13.0.0", "@testing-library/user-event": "^13.2.1", @@ -11,10 +13,12 @@ "@types/react": "^18.0.0", "@types/react-dom": "^18.0.0", "axios": "^1.1.3", + "classnames": "^2.5.1", "react": "^18.2.0", "react-dom": "^18.2.0", "react-router-dom": "^6.4.2", "react-scripts": "5.0.1", + "react-toastify": "^10.0.5", "typescript": "^4.4.2", "web-vitals": "^2.1.0" }, diff --git a/starter-template/src/App.tsx b/starter-template/src/App.tsx index ef30c96..b230f98 100644 --- a/starter-template/src/App.tsx +++ b/starter-template/src/App.tsx @@ -1,3 +1,5 @@ +import { useIsFetching, useIsMutating } from '@tanstack/react-query' +import Spinner from 'components/Spinner' import MainLayout from 'layouts/MainLayout' import About from 'pages/About' import AddStudent from 'pages/AddStudent' @@ -5,6 +7,8 @@ import Dashboard from 'pages/Dashboard' import NotFound from 'pages/NotFound' import Students from 'pages/Students' import { useRoutes } from 'react-router-dom' +import { ToastContainer } from 'react-toastify' +import 'react-toastify/dist/ReactToastify.css' function App() { const elements = useRoutes([ @@ -34,8 +38,13 @@ function App() { } ]) + const isFetching = useIsFetching() + const isMutating = useIsMutating() + return (
+ {isFetching + isMutating !== 0 && } + {elements}
) diff --git a/starter-template/src/apis/students.api.ts b/starter-template/src/apis/students.api.ts new file mode 100644 index 0000000..3ff6ac8 --- /dev/null +++ b/starter-template/src/apis/students.api.ts @@ -0,0 +1,19 @@ +import { Student, Students } from 'types/students.type' +import http from 'utils/http' + +export const getStudents = (page: number | string, limit: number | string, signal?: AbortSignal) => + http.get('students', { + params: { + _page: page, + _limit: limit + }, + signal + }) + +export const getStudent = (id: number | string) => http.get(`students/${id}`) + +export const addStudent = (student: Omit) => http.post('/students', student) + +export const updateStudent = (id: number | string, student: Student) => http.put(`students/${id}`, student) + +export const deleteStudent = (id: number | string) => http.delete<{}>(`students/${id}`) diff --git a/starter-template/src/components/Spinner/Spinner.tsx b/starter-template/src/components/Spinner/Spinner.tsx new file mode 100644 index 0000000..561c224 --- /dev/null +++ b/starter-template/src/components/Spinner/Spinner.tsx @@ -0,0 +1,23 @@ +export default function Spinner() { + return ( +
+ + Loading... +
+ ) +} diff --git a/starter-template/src/components/Spinner/index.ts b/starter-template/src/components/Spinner/index.ts new file mode 100644 index 0000000..52d2a50 --- /dev/null +++ b/starter-template/src/components/Spinner/index.ts @@ -0,0 +1,3 @@ +import Spinner from './Spinner' + +export default Spinner diff --git a/starter-template/src/index.tsx b/starter-template/src/index.tsx index efe2c92..769602d 100644 --- a/starter-template/src/index.tsx +++ b/starter-template/src/index.tsx @@ -4,14 +4,27 @@ import './index.css' import App from './App' import reportWebVitals from './reportWebVitals' import { BrowserRouter } from 'react-router-dom' +import { QueryClient, QueryClientProvider } from '@tanstack/react-query' +import { ReactQueryDevtools } from '@tanstack/react-query-devtools' + +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + refetchOnWindowFocus: false + } + } +}) const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement) root.render( - - + // + + - - + + + + // ) // If you want to start measuring performance in your app, pass a function diff --git a/starter-template/src/pages/AddStudent/AddStudent.tsx b/starter-template/src/pages/AddStudent/AddStudent.tsx index 20a67d7..c454d17 100644 --- a/starter-template/src/pages/AddStudent/AddStudent.tsx +++ b/starter-template/src/pages/AddStudent/AddStudent.tsx @@ -1,23 +1,133 @@ +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' +import { addStudent, getStudent, updateStudent } from 'apis/students.api' +import { useEffect, useMemo, useState } from 'react' +import { useMatch, useParams } from 'react-router-dom' +import { toast } from 'react-toastify' +import { Student } from 'types/students.type' +import { isAxiosError } from 'utils/utils' + +type FormStateType = Omit | Student + +const initialFormState: FormStateType = { + avatar: '', + email: '', + btc_address: '', + country: '', + first_name: '', + last_name: '', + gender: 'other' +} + +type FormError = + | { + [key in keyof FormStateType]: string + } + | null + +const gender = { + male: 'Male', + female: 'Female', + other: 'Other' +} + export default function AddStudent() { + const [formState, setFormState] = useState(initialFormState) + const addMatch = useMatch('/students/add') + const isAddMode = Boolean(addMatch) + const { id } = useParams() + const queryClient = useQueryClient() + + const addStudentMutation = useMutation({ + mutationFn: (body: FormStateType) => { + return addStudent(body) + } + }) + + // get student for info + const studentQuery = useQuery({ + queryKey: ['student', id], + queryFn: () => getStudent(id as string), + enabled: id !== undefined, + staleTime: 1000 * 10 + }) + + useEffect(() => { + if (studentQuery.data) { + setFormState(studentQuery.data.data) + } + }, [studentQuery.data]) + + const updateStudentMutation = useMutation({ + mutationFn: (_) => { + return updateStudent(id as string, formState as Student) + }, + onSuccess: (data) => { + queryClient.setQueryData(['student', id], data) + } + }) + + const errorForm: FormError = useMemo(() => { + const error = isAddMode ? addStudentMutation.error : updateStudentMutation.error + if (isAxiosError<{ error: FormError }>(error) && error.response?.status === 422) { + return error.response?.data?.error + } + return null + }, [isAddMode, addStudentMutation.error, updateStudentMutation.error]) + + // Dùng currying function + const handleChange = (name: keyof FormStateType) => (event: React.ChangeEvent) => { + setFormState((prev) => ({ ...prev, [name]: event.target.value })) + if (addStudentMutation.data || addStudentMutation.error) { + addStudentMutation.reset() + } + } + + const handleSubmit = (event: React.FormEvent) => { + event.preventDefault() + + if (isAddMode) { + addStudentMutation.mutate(formState, { + onSuccess: () => { + setFormState(initialFormState) + toast.success('Add thành công') + } + }) + } else { + updateStudentMutation.mutate(undefined, { + onSuccess: (_) => { + toast.success('Update thành công') + } + }) + } + } + return (
-

Add/Edit Student

-
+

{isAddMode ? 'Add' : 'Edit'} Student

+
+ {errorForm && ( +

+ Lỗi! + {errorForm.email} +

+ )}
@@ -28,33 +138,40 @@ export default function AddStudent() { id='gender-1' type='radio' name='gender' - className='h-4 w-4 border-gray-300 bg-gray-100 text-blue-600 focus:ring-2 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700 dark:ring-offset-gray-800 dark:focus:ring-blue-600' + value={gender.male} + checked={formState.gender === gender.male} + onChange={handleChange('gender')} + className='h-4 w-4 border-gray-300 bg-gray-100 text-blue-600 focus:ring-2 focus:ring-blue-500 ' /> -
-
-
@@ -66,13 +183,15 @@ export default function AddStudent() { type='text' name='country' id='country' - className='peer block w-full appearance-none border-0 border-b-2 border-gray-300 bg-transparent py-2.5 px-0 text-sm text-gray-900 focus:border-blue-600 focus:outline-none focus:ring-0 dark:border-gray-600 dark:text-white dark:focus:border-blue-500' + value={formState.country} + onChange={handleChange('country')} + className='peer block w-full appearance-none border-0 border-b-2 border-gray-300 bg-transparent py-2.5 px-0 text-sm text-gray-900 focus:border-blue-600 focus:outline-none focus:ring-0' placeholder=' ' required /> @@ -81,16 +200,17 @@ export default function AddStudent() {
@@ -100,13 +220,15 @@ export default function AddStudent() { type='text' name='last_name' id='last_name' - className='peer block w-full appearance-none border-0 border-b-2 border-gray-300 bg-transparent py-2.5 px-0 text-sm text-gray-900 focus:border-blue-600 focus:outline-none focus:ring-0 dark:border-gray-600 dark:text-white dark:focus:border-blue-500' + className='peer block w-full appearance-none border-0 border-b-2 border-gray-300 bg-transparent py-2.5 px-0 text-sm text-gray-900 focus:border-blue-600 focus:outline-none focus:ring-0 ' placeholder=' ' + value={formState.last_name} + onChange={handleChange('last_name')} required /> @@ -118,13 +240,15 @@ export default function AddStudent() { type='text' name='avatar' id='avatar' - className='peer block w-full appearance-none border-0 border-b-2 border-gray-300 bg-transparent py-2.5 px-0 text-sm text-gray-900 focus:border-blue-600 focus:outline-none focus:ring-0 dark:border-gray-600 dark:text-white dark:focus:border-blue-500' + className='peer block w-full appearance-none border-0 border-b-2 border-gray-300 bg-transparent py-2.5 px-0 text-sm text-gray-900 focus:border-blue-600 focus:outline-none focus:ring-0' placeholder=' ' + value={formState.avatar} + onChange={handleChange('avatar')} required /> @@ -134,13 +258,15 @@ export default function AddStudent() { type='text' name='btc_address' id='btc_address' - className='peer block w-full appearance-none border-0 border-b-2 border-gray-300 bg-transparent py-2.5 px-0 text-sm text-gray-900 focus:border-blue-600 focus:outline-none focus:ring-0 dark:border-gray-600 dark:text-white dark:focus:border-blue-500' + className='peer block w-full appearance-none border-0 border-b-2 border-gray-300 bg-transparent py-2.5 px-0 text-sm text-gray-900 focus:border-blue-600 focus:outline-none focus:ring-0' placeholder=' ' + value={formState.btc_address} + onChange={handleChange('btc_address')} required /> @@ -149,9 +275,9 @@ export default function AddStudent() {
diff --git a/starter-template/src/pages/Students/Students.tsx b/starter-template/src/pages/Students/Students.tsx index c5b00a0..0889678 100644 --- a/starter-template/src/pages/Students/Students.tsx +++ b/starter-template/src/pages/Students/Students.tsx @@ -1,98 +1,248 @@ +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' +import { deleteStudent, getStudent, getStudents } from 'apis/students.api' import { Link } from 'react-router-dom' +import { useQueryString } from 'utils/utils' +import classNames from 'classnames' +import { toast } from 'react-toastify' +const LIMIT = 10 export default function Students() { + const queryClient = useQueryClient() + + const queryString: { page?: string } = useQueryString() + const page = Number(queryString.page) || 1 + + const studentsQuery = useQuery({ + queryKey: ['students', page], + queryFn: () => { + const controller = new AbortController() + setTimeout(() => { + controller.abort() + }, 5000) + return getStudents(page, LIMIT, controller.signal) + }, + retry: 10, + staleTime: 60 * 1000, + gcTime: 5 * 1000 + }) + + const deleteStudentMutation = useMutation({ + mutationFn: (id: number | string) => deleteStudent(id), + onSuccess: (_, id) => { + toast.success(`Xoá thành công student với id là ${id}`) + queryClient.invalidateQueries({ queryKey: ['students', page], exact: true }) + } + }) + + const totalStudentsCount = Number(studentsQuery.data?.headers['x-total-count'] || 0) + const totalPage = Math.ceil(totalStudentsCount / LIMIT) + + const handleDelete = (id: number) => { + deleteStudentMutation.mutate(id) + } + + const handlePrefetchStudent = (id: number) => { + // queryClient.prefetchQuery({ + // queryKey: ['student', String(id)], + // queryFn: () => getStudent(id), + // staleTime: 1000 * 10 + // }) + } + + const fetchStudent = (second: number) => { + const id = '6' + queryClient.prefetchQuery({ + queryKey: ['student', String(id)], + queryFn: () => getStudent(id), + staleTime: second * 1000 + }) + } + + const refetchStudent = () => { + studentsQuery.refetch() + } + + const cancelRequestStudents = () => { + queryClient.cancelQueries({ queryKey: ['students', page] }) + } + return (

Students

- {/*
-
-
-
-
-
-
-
-
-
-
-
-
-
- Loading... -
*/} -
- - - - - - - - - - - - - - - - - - - -
- # - - Avatar - - Name - - Email - - Action -
1 - student - - Apple - apple@gmail.com - - Edit - - -
+
+
-
- +
+
+ +
+ +
+ +
+ +
+ +
+ + Add Student + +
+ + {studentsQuery.isLoading && ( +
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Loading... +
+ )} + + {!studentsQuery.isLoading && ( + <> +
+ + + + + + + + + + + + {studentsQuery.data?.data.map((student) => ( + handlePrefetchStudent(student.id)} + > + + + + + + + ))} + +
+ # + + Avatar + + Name + + Email + + Action +
{student.id} + student + + {student.last_name} + {student.email} + + Edit + + +
+
+ +
+ +
+ + )}
) } diff --git a/starter-template/src/types/students.type.ts b/starter-template/src/types/students.type.ts new file mode 100644 index 0000000..f254b4e --- /dev/null +++ b/starter-template/src/types/students.type.ts @@ -0,0 +1,12 @@ +export interface Student { + id: number + first_name: string + last_name: string + email: string + gender: string + country: string + avatar: string + btc_address: string +} + +export type Students = Pick[] diff --git a/starter-template/src/utils/http.ts b/starter-template/src/utils/http.ts new file mode 100644 index 0000000..ccdc7e3 --- /dev/null +++ b/starter-template/src/utils/http.ts @@ -0,0 +1,18 @@ +import axios, { AxiosInstance } from 'axios' + +class Http { + instance: AxiosInstance + constructor() { + this.instance = axios.create({ + baseURL: 'http://localhost:4000/', + timeout: 10000, + headers: { + 'Content-Type': 'application/json' + } + }) + } +} + +const http = new Http().instance + +export default http diff --git a/starter-template/src/utils/utils.ts b/starter-template/src/utils/utils.ts new file mode 100644 index 0000000..1c22d2b --- /dev/null +++ b/starter-template/src/utils/utils.ts @@ -0,0 +1,12 @@ +import axios, { AxiosError } from 'axios' +import { useSearchParams } from 'react-router-dom' + +export const useQueryString = () => { + const [searchParams] = useSearchParams() + const searchParamsObject = Object.fromEntries([...searchParams]) + return searchParamsObject +} + +export function isAxiosError(error: unknown): error is AxiosError { + return axios.isAxiosError(error) +} diff --git a/starter-template/tsconfig.json b/starter-template/tsconfig.json index 860b1d5..f70bdd0 100644 --- a/starter-template/tsconfig.json +++ b/starter-template/tsconfig.json @@ -1,11 +1,7 @@ { "compilerOptions": { - "target": "es5", - "lib": [ - "dom", - "dom.iterable", - "esnext" - ], + "target": "ES2015", + "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, "skipLibCheck": true, "esModuleInterop": true, @@ -21,7 +17,5 @@ "jsx": "react-jsx", "baseUrl": "src" }, - "include": [ - "src" - ] + "include": ["src"] } diff --git a/starter-template/yarn.lock b/starter-template/yarn.lock index 134fa8c..5e6818d 100644 --- a/starter-template/yarn.lock +++ b/starter-template/yarn.lock @@ -1751,18 +1751,29 @@ "@svgr/plugin-svgo" "^5.5.0" loader-utils "^2.0.0" -"@tanstack/query-core@4.14.1": - version "4.14.1" - resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-4.14.1.tgz#a74c4da03e79a8be07fa5ab2ebc12865146915e2" - integrity sha512-mUejKoFDe4NZB8jQJR1uuAl6IwvkUpOD2m8NcuTVPOu0pcxeeFPdrnHaljwOEFPtlqXoiiIIQGYy6whjCMN+iQ== +"@tanstack/query-core@5.56.2": + version "5.56.2" + resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-5.56.2.tgz#2def2fb0290cd2836bbb08afb0c175595bb8109b" + integrity sha512-gor0RI3/R5rVV3gXfddh1MM+hgl0Z4G7tj6Xxpq6p2I03NGPaJ8dITY9Gz05zYYb/EJq9vPas/T4wn9EaDPd4Q== -"@tanstack/react-query@^4.13.5": - version "4.14.1" - resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-4.14.1.tgz#336545119b191e2096c394a3f2df2744cdc5e041" - integrity sha512-cRgNzigw4GSPwGlTEkXi8hi/xgUnSEt9jCkiC8oAT3PEIdsQ50onZcpXd+JNJcZk2RTh8KM1fGyWz6xYLiY8bg== +"@tanstack/query-devtools@5.56.1": + version "5.56.1" + resolved "https://registry.yarnpkg.com/@tanstack/query-devtools/-/query-devtools-5.56.1.tgz#319c362dd19c6cfe005e74a8777baefa4a4f72de" + integrity sha512-xnp9jq/9dHfSCDmmf+A5DjbIjYqbnnUL2ToqlaaviUQGRTapXQ8J+GxusYUu1IG0vZMaWdiVUA4HRGGZYAUU+A== + +"@tanstack/react-query-devtools@^5.56.2": + version "5.56.2" + resolved "https://registry.yarnpkg.com/@tanstack/react-query-devtools/-/react-query-devtools-5.56.2.tgz#c129cdb811927085434ea27691e4b7f605eb4128" + integrity sha512-7nINJtRZZVwhTTyDdMIcSaXo+EHMLYJu1S2e6FskvvD5prx87LlAXXWZDfU24Qm4HjshEtM5lS3HIOszNGblcw== + dependencies: + "@tanstack/query-devtools" "5.56.1" + +"@tanstack/react-query@^5.56.2": + version "5.56.2" + resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-5.56.2.tgz#3a0241b9d010910905382f5e99160997b8795f91" + integrity sha512-SR0GzHVo6yzhN72pnRhkEFRAHMsUo5ZPzAxfTMvUxFIDVS6W9LYUp6nXW3fcHVdg0ZJl8opSH85jqahvm6DSVg== dependencies: - "@tanstack/query-core" "4.14.1" - use-sync-external-store "^1.2.0" + "@tanstack/query-core" "5.56.2" "@testing-library/dom@^8.5.0": version "8.19.0" @@ -3077,6 +3088,11 @@ cjs-module-lexer@^1.0.0: resolved "https://registry.yarnpkg.com/cjs-module-lexer/-/cjs-module-lexer-1.2.2.tgz#9f84ba3244a512f3a54e5277e8eef4c489864e40" integrity sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA== +classnames@^2.5.1: + version "2.5.1" + resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.5.1.tgz#ba774c614be0f016da105c858e7159eae8e7687b" + integrity sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow== + clean-css@^5.2.2: version "5.3.1" resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-5.3.1.tgz#d0610b0b90d125196a2894d35366f734e5d7aa32" @@ -3093,6 +3109,11 @@ cliui@^7.0.2: strip-ansi "^6.0.0" wrap-ansi "^7.0.0" +clsx@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.1.1.tgz#eed397c9fd8bd882bfb18deab7102049a2f32999" + integrity sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA== + co@^4.6.0: version "4.6.0" resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" @@ -7572,6 +7593,13 @@ react-scripts@5.0.1: optionalDependencies: fsevents "^2.3.2" +react-toastify@^10.0.5: + version "10.0.5" + resolved "https://registry.yarnpkg.com/react-toastify/-/react-toastify-10.0.5.tgz#6b8f8386060c5c856239f3036d1e76874ce3bd1e" + integrity sha512-mNKt2jBXJg4O7pSdbNUfDdTsK9FIdikfsIE/yUCxbAEXl4HMyJaivrVFcn3Elvt5xvCQYhUZm+hqTIu1UXM3Pw== + dependencies: + clsx "^2.1.0" + react@^18.2.0: version "18.2.0" resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5" @@ -8709,11 +8737,6 @@ url-parse@^1.5.3: querystringify "^2.1.1" requires-port "^1.0.0" -use-sync-external-store@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a" - integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA== - util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"