├── .editorconfig ├── .eslintrc.cjs ├── .gitignore ├── .node-version ├── .prettierignore ├── .prettierrc ├── .vscode ├── cspell.json └── settings.json ├── README-2022.md ├── README-vi-2022.md ├── README.md ├── db └── sample.db.json ├── index.html ├── package.json ├── postcss.config.js ├── public └── vite.svg ├── rest.http ├── src ├── api │ ├── api-client.ts │ ├── index.ts │ └── users │ │ ├── index.ts │ │ ├── use-create-user.ts │ │ ├── use-delete-user.ts │ │ ├── use-edit-user.ts │ │ ├── use-infinite-users.ts │ │ ├── use-paginated-users.ts │ │ ├── use-user.ts │ │ ├── use-users.ts │ │ └── user-query-keys.ts ├── app.tsx ├── assets │ └── react.svg ├── components │ ├── app-layout.tsx │ ├── delete-modal.tsx │ ├── hello-world.tsx │ ├── index.tsx │ ├── ui │ │ ├── index.tsx │ │ └── toast │ │ │ ├── toast.tsx │ │ │ └── use-toast.tsx │ ├── user-form │ │ ├── index.tsx │ │ └── user-form.css │ └── user-table │ │ ├── index.tsx │ │ └── user-table.css ├── icons │ ├── close.tsx │ ├── delete.tsx │ ├── edit.tsx │ ├── exclamation.tsx │ └── index.tsx ├── index.css ├── main.tsx ├── preview.PNG ├── providers │ ├── app-provider.tsx │ ├── react-query-provider.tsx │ ├── routing-provider.tsx │ └── toaster-provider.tsx ├── types │ └── index.ts ├── utils │ ├── cn.ts │ └── index.ts ├── views │ ├── create-user.tsx │ ├── edit-user.tsx │ ├── index.tsx │ ├── infinite-users.tsx │ ├── paginated-users.tsx │ └── users.tsx └── vite-env.d.ts ├── tailwind.config.js ├── tsconfig.json ├── tsconfig.node.json ├── vite.config.ts └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.{js, jsx, ts, tsx}] 13 | max_line_length = 120 14 | 15 | [*.md] 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { browser: true, es2020: true }, 4 | extends: [ 5 | 'eslint:recommended', 6 | 'plugin:@typescript-eslint/recommended', 7 | 'plugin:react-hooks/recommended', 8 | ], 9 | ignorePatterns: ['dist', '.eslintrc.cjs'], 10 | parser: '@typescript-eslint/parser', 11 | plugins: ['react-refresh'], 12 | rules: {}, 13 | }; 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .idea 17 | .DS_Store 18 | *.suo 19 | *.ntvs* 20 | *.njsproj 21 | *.sln 22 | *.sw? 23 | 24 | db/db.json 25 | -------------------------------------------------------------------------------- /.node-version: -------------------------------------------------------------------------------- 1 | 18.17.0 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | package.json 2 | package-lock.json 3 | yarn.lock 4 | node_modules 5 | dist 6 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "singleQuote": true, 4 | "arrowParens": "always", 5 | "bracketSpacing": true, 6 | "trailingComma": "es5", 7 | "jsxSingleQuote": false, 8 | "bracketSameLine": false, 9 | "tabWidth": 2, 10 | "printWidth": 80, 11 | "organizeImportsSkipDestructiveCodeActions": true 12 | } 13 | -------------------------------------------------------------------------------- /.vscode/cspell.json: -------------------------------------------------------------------------------- 1 | { 2 | // Version of the setting file. Always 0.2 3 | "version": "0.2", 4 | // language - current active spelling language 5 | "language": "en", 6 | // words - list of words to be always considered correct 7 | "words": [ 8 | "mkdirp", 9 | "tsmerge", 10 | "githubusercontent", 11 | "streetsidesoftware", 12 | "vsmarketplacebadge", 13 | "visualstudio", 14 | "behance", 15 | "pinterest", 16 | "dribble", 17 | "tumblr", 18 | "twitter", 19 | 20 | // For React projects 21 | "tailwindcss", 22 | "tailwindlabs", 23 | "headlessui", 24 | "svgs", 25 | "persistor", 26 | "zustand", 27 | "tanstack", 28 | "clsx", 29 | "tfjs", 30 | "lucide" 31 | ], 32 | // flagWords - list of words to be always considered incorrect 33 | // This is useful for offensive words and common spelling errors. 34 | // For example "hte" should be "the" 35 | "flagWords": ["hte"] 36 | } 37 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.defaultFormatter": "esbenp.prettier-vscode", 3 | "editor.formatOnSave": true, 4 | "editor.formatOnPaste": true, 5 | "editor.lineHeight": 21, 6 | "editor.tabSize": 2, 7 | "tailwindCSS.suggestions": true, 8 | "tailwindCSS.classAttributes": [ 9 | // Default 10 | "class", 11 | "className", 12 | 13 | // Additional Tailwind CSS Intellisense for Transition API from Headless UI 14 | // https://headlessui.com/react/transition#component-api 15 | "enter", 16 | "enterFrom", 17 | "enterTo", 18 | "entered", 19 | "leave", 20 | "leaveFrom", 21 | "leaveTo" 22 | ], 23 | // https://cva.style/docs/getting-started/installation#intellisense 24 | "tailwindCSS.experimental.classRegex": [ 25 | ["cva\\(([^)]*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"], 26 | ["cx\\(([^)]*)\\)", "(?:'|\"|`)([^']*)(?:'|\"|`)"] 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /README-2022.md: -------------------------------------------------------------------------------- 1 | # React Query Example 2 | 3 | ## Preview 4 | 5 | ![Site Preview](./src/preview.PNG) 6 | 7 | **NOTE: There's a README file in Vietnamese.** 8 | https://github.com/nguyenhieptech/react-query-example/blob/main/README-vi.md 9 | 10 | This project is a demonstration of how to use [React Query](https://react-query.tanstack.com) library to perform the following tasks: 11 | 12 | - CRUD Operations (using useQuery, useMutation). 13 | - Pagination (using useQuery) 14 | - Load-more like feature (using useInfiniteQuery) 15 | 16 | This project is setup with: 17 | 18 | - [Vite](https://vitejs.dev/): "Next Generation Frontend Tooling". It's faster than [create-react-app](https://create-react-app.dev/). Consider using Vite for all your personal projects. It's worth it. 19 | - [WindiCSS](https://windicss.org/guide/): "An **on-demand** alternative to Tailwind, which provides faster load times, **full compatibility with Tailwind v2.0**, and a bunch of additional cool features." 20 | - [React Hook Form](https://react-hook-form.com/): Performant, flexible and extensible forms with easy-to-use validation. 21 | - [React Modal](http://reactcommunity.org/react-modal/): Accessible modal dialog component for React. 22 | - [Axios](https://github.com/axios/axios): Promise based HTTP client for the browser and Node.js. 23 | - [JSON Server](https://github.com/typicode/json-server): Full fake REST API server with zero coding in less than 30 seconds. 24 | 25 | ## Run Locally 26 | 27 | - Clone the project 28 | 29 | ```bash 30 | git clone https://github.com/nguyenhieptech/react-query-ts 31 | ``` 32 | 33 | - Go to the project directory: 34 | 35 | ```bash 36 | cd react-query-ts 37 | ``` 38 | 39 | - Install dependencies 40 | 41 | ```bash 42 | yarn install 43 | ``` 44 | 45 | - Setup database file 46 | 47 | ```bash 48 | cp api/sample.db.json api/db.json 49 | ``` 50 | 51 | - Start the `json-server` 52 | 53 | ```bash 54 | yarn run json-server 55 | ``` 56 | 57 | - Launch another terminal and start the Vite server 58 | 59 | ```bash 60 | yarn run dev 61 | ``` 62 | 63 | Head over to your browser and open the URL to access the application. 64 | 65 | ## How do I approach this project? 66 | 67 | - Why React Query is the way to go? 68 | 69 | https://www.youtube.com/watch?v=aLQbVd-2tIo 70 | 71 | https://www.youtube.com/watch?v=seU46c6Jz7E 72 | 73 | - It uses custom hooks for better structure, so that if you want to change implementation details, it doesn't affect code inside components. These two articles below are really **IMPORTANT**, please read them carefully. 74 | 75 | https://kyleshevlin.com/use-encapsulation 76 | 77 | https://log.seruco.io/hide-usequery/ 78 | 79 | - It includes Optimistic Updates. What is Optimistic Updates? 80 | 81 | https://react-query.tanstack.com/guides/optimistic-updates 82 | 83 | https://stackoverflow.com/questions/33009657/what-is-optimistic-updates-in-front-end-development 84 | 85 | https://resthooks.io/docs/guides/optimistic-updates 86 | 87 | - How to setup Vite projects with WindiCSS: 88 | 89 | https://windicss.org/integrations/vite.html 90 | 91 | ## Reference 92 | 93 | - https://www.sitepoint.com/react-query-fetch-manage-data by [Michael Wanyoike](https://twitter.com/myxsys) 94 | - https://react-query.tanstack.com/overview 95 | -------------------------------------------------------------------------------- /README-vi-2022.md: -------------------------------------------------------------------------------- 1 | # Ví dụ về một dự án dùng React Query 2 | 3 | ## Xem trước 4 | 5 | ![Site Preview](./src/preview.PNG) 6 | 7 | (Một số từ chuyên ngành/mô tả đặc thù sẽ không dịch mà để nguyên nghĩa) 8 | 9 | Đây là một project ví dụ cho thấy cách triển khai thư viện [React Query](https://react-query.tanstack.com) để thực hiện các tác vụ sau: 10 | 11 | - CRUD Operations - đọc, thêm, xóa, sửa dữ liệu (sử dụng _useQuery_, _useMutation_). 12 | - Pagination (phân trang) (sử dụng _useQuery_) 13 | - Load more (Chức năng tải thêm), thường sử dụng khi số lượng dữ liệu nhiều. (sử dụng _useInfiniteQuery_) 14 | 15 | Project gồm những dependencies sau (có thể tìm hiểu thêm ở documentation chính thức của các thư viện/framework này): 16 | 17 | - [Vite](https://vitejs.dev/): "Next Generation Frontend Tooling". It's faster than [create-react-app](https://create-react-app.dev/). Hãy dùng Vite cho các project cá nhân của bạn, create-react-app phù hợp với những dự án lớn và rất lớn (large-scale) hơn. 18 | - [WindiCSS](https://windicss.org/guide/): "An **on-demand** alternative to Tailwind, which provides faster load times, **full compatibility with Tailwind v2.0**, and a bunch of additional cool features." 19 | - [React Hook Form](https://react-hook-form.com/): Performant, flexible and extensible forms with easy-to-use validation. 20 | - [React Modal](http://reactcommunity.org/react-modal/): Accessible modal dialog component for React. 21 | - [Axios](https://github.com/axios/axios): Promise based HTTP client for the browser and Node.js. 22 | - [JSON Server](https://github.com/typicode/json-server): Full fake REST API server with zero coding in less than 30 seconds. 23 | 24 | ## Làm sao chạy được ở máy tính bạn (local) 25 | 26 | - Tải repository về máy 27 | 28 | ```bash 29 | git clone https://github.com/nguyenhieptech/react-query-example 30 | ``` 31 | 32 | - Thay đổi đường dẫn command line đến thư mục project (hoặc mở cmd trong VS Code với _Ctrl + `_) 33 | 34 | ```bash 35 | cd react-query-example 36 | ``` 37 | 38 | - Cài đặt dependencies 39 | 40 | ```bash 41 | yarn install 42 | ``` 43 | 44 | - Cài đặt dữ liệu mẫu (demo database) 45 | 46 | ```bash 47 | cp api/sample.db.json api/db.json 48 | ``` 49 | 50 | - Bắt đầu chạy `json-server` để có thể tương tác được với CSDL mẫu 51 | 52 | ```bash 53 | yarn run json-server 54 | ``` 55 | 56 | - Mở một command line khác và khởi chạy Vite server 57 | 58 | ```bash 59 | yarn run dev 60 | ``` 61 | 62 | Mở trình duyệt với URL để truy cập ứng dụng. 63 | 64 | ## Hiểu thêm về project này 65 | 66 | 1. Trước tiên cần hiểu tại sao bạn cần React Query - thư viện này giúp bạn làm gì. 67 | 68 | https://vntalking.com/react-tai-sao-minh-khong-muon-su-dung-redux.html 69 | 70 | https://www.youtube.com/watch?v=aLQbVd-2tIo 71 | 72 | https://www.youtube.com/watch?v=seU46c6Jz7E 73 | 74 | 2. Dự án sử dụng _Custom hooks_, thế nên chúng ta có thể thay đổi cách xử lý dữ liệu mà không ảnh hưởng đến giao diện (UI) của ứng dụng. Hai bài đọc bên dưới có thể cho bạn hiểu hơn về điều này. 75 | 76 | https://log.seruco.io/hide-usequery/ 77 | 78 | https://kyleshevlin.com/use-encapsulation 79 | 80 | Việc này có thể giúp bạn quản lý project tốt hơn. Bên dưới là cấu trúc thư mục. 81 | 82 | ```shell 83 | ├── .src 84 | ├── components 85 | ├── icons 86 | ├── layouts 87 | ... 88 | ├── views 89 | ├── Order 90 | ├── Todos 91 | ├── Users 92 | ├── hooks 93 | ├── queryKeys.ts 94 | ├── useCreateUsers.ts 95 | ├── useEditUser.ts 96 | ├── useGetUsers.ts 97 | ├── useLoadMoreUsers.ts 98 | ├── usePaginatedUsers.ts 99 | ├── CreateUser.tsx 100 | ├── EditUser.tsx 101 | ├── InfiniteQuery.tsx 102 | ├── PaginatedQuery.tsx 103 | ├── SearchUser.tsx 104 | ├── Users.tsx 105 | ``` 106 | 107 | Khi làm thêm chức năng/nghiệp vụ mới, bạn có thể triển khai cấu trúc như bên dưới (có thể đổi thư mục _views_ thành tên khác) 108 | 109 | ```shell 110 | ├── .src 111 | ├── components 112 | ├── icons 113 | ├── layouts 114 | ... 115 | ├── views 116 | ├── Products 117 | | ├── hooks 118 | | ├── queryKeys.ts 119 | | ├── useCreateProducts.ts 120 | | ├── useEditProduct.ts 121 | | ├── useGetProducts.ts 122 | | ├── useLoadMoreProducts.ts 123 | | ├── usePaginatedProducts.ts 124 | | ├── CreateProducts.tsx 125 | | ├── EditProducts.tsx 126 | | ├── InfiniteProducts.tsx 127 | | ├── PaginatedProducts.tsx 128 | | ├── SearchProducts.tsx 129 | | ├── Products.tsx 130 | ├── Todos 131 | | ├── hooks 132 | | ├── queryKeys.ts 133 | | ├── useCreateTodos.ts 134 | | ├── useEditTodo.ts 135 | | ├── useGetTodos.ts 136 | | ├── useLoadMoreTodos.ts 137 | | ├── usePaginatedTodos.ts 138 | | ├── CreateTodo.tsx 139 | | ├── EditTodo.tsx 140 | | ├── InfiniteTodos.tsx 141 | | ├── PaginatedTodos.tsx 142 | | ├── SearchTodo.tsx 143 | | ├── Todos.tsx 144 | ├── Users 145 | | ├── hooks 146 | | ├── queryKeys.ts 147 | | ├── useCreateUsers.ts 148 | | ├── useEditUser.ts 149 | | ├── useGetUsers.ts 150 | | ├── useLoadMoreUsers.ts 151 | | ├── usePaginatedUsers.ts 152 | | ├── CreateUser.tsx 153 | | ├── EditUser.tsx 154 | | ├── InfiniteUsers.tsx 155 | | ├── PaginatedUsers.tsx 156 | | ├── SearchUser.tsx 157 | | ├── Users.tsx 158 | ├── 159 | ... 160 | ``` 161 | 162 | - Bonus: Nếu làm thêm test/styled components/scss có thể phân chia như sau: 163 | 164 | ```shell 165 | ├── .src 166 | ├── components 167 | ├── icons 168 | ├── layouts 169 | ... 170 | ├── views 171 | ├── Users 172 | | ├── hooks 173 | | ├── queryKeys.ts 174 | | ├── useCreateUsers.ts 175 | | ├── useEditUser.ts 176 | | ├── useGetUsers.ts 177 | | ├── useLoadMoreUsers.ts 178 | | ├── usePaginatedUsers.ts 179 | | ├── CreateUser 180 | | ├── index.tsx 181 | | ├── CreateUser.test.tsx 182 | | ├── CreateUser.styled.ts hoặc CreateUser.scss 183 | | ├── EditUser 184 | | ├── index.tsx 185 | | ├── EditUser.test.tsx 186 | | ├── EditUser.styled.ts hoặc CreateUser.scss 187 | | ├── InfiniteUsers 188 | | ├── index.tsx 189 | | ├── InfiniteUsers.test.tsx 190 | | ├── InfiniteUsers.styled.ts hoặc InfiniteUsers.scss 191 | | ├── PaginatedUsers 192 | | ├── index.tsx 193 | | ├── PaginatedUsers.test.tsx 194 | | ├── PaginatedUsers.styled.ts hoặc PaginatedUsers.scss 195 | | ├── SearchUser 196 | | ├── index.tsx 197 | | ├── SearchUser.test.tsx 198 | | ├── SearchUser.styled.ts hoặc SearchUser.scss 199 | | ├── Users 200 | | ├── index.tsx 201 | | ├── Users.test.tsx 202 | | ├── Users.styled.ts hoặc Users.scss 203 | ├── Products 204 | | ├──... 205 | ├── Todos 206 | ├──... 207 | ... 208 | (Other Folders) 209 | ``` 210 | 211 | - Bonus: Để tránh phải import quá dài dòng như dưới 212 | 213 | ```js 214 | import { classNames } from '../../../../../../utils/classNames.ts'; 215 | ``` 216 | 217 | thì hãy tìm hiểu cấu hình cho Absolute Path cho tất cả các project bạn làm (recommend). Ví dụ trên sẽ thành: 218 | 219 | ```js 220 | import { classNames } from 'src/utils/classNames.ts'; 221 | ``` 222 | 223 | Từ khóa tìm kiếm các bài hướng dẫn: _config absolute path for react app_. Đôi khi bạn sẽ phải kết hợp cấu hình từ 2-3 bài thì mới thành công. 224 | 225 | 3. Quay trở lại với việc tách UI và logic. Cùng phân tích `Users.tsx` làm ví dụ. 226 | 227 | ```js 228 | import UserTable from 'src/components/UserTable'; 229 | import { useGetUsers } from './hooks/useGetUsers'; 230 | 231 | function Users() { 232 | const getUsers = useGetUsers(); 233 | 234 | return ( 235 |
236 |

Basic Query Example

237 |
238 | {getUsers.isLoading &&
Loading...
} 239 | 240 | {getUsers.isFetching &&
Fetching...
} 241 | 242 | {getUsers.error instanceof Error &&
{getUsers.error.message}
} 243 | 244 | {getUsers.isSuccess && ( 245 |
246 | 247 |
248 | )} 249 |
250 |
251 | ); 252 | } 253 | 254 | export default Users; 255 | ``` 256 | 257 | Vì phần giao diện (UI) được tách rời với logic, bạn có thể thay đổi code của phần logic (ví dụ bạn muốn xử lý dữ liệu với thư viện _swr_ thay vì dùng _react-query_, hay đổi từ `axios` về `fetch` hoặc `ky`) mà không làm ảnh hưởng đến UI, chỉ cần cung cấp đủ những state cần thiết như `isLoading, isSuccess, isFetching, error` (nếu không thích Boolean, có thể thay bằng `status === 'loading'` hoặc `status === 'success'`, v.v). 258 | Xử lý dữ liệu từ phía server/backend cần xử lý bất đồng bộ (asynchronous), cho nên sẽ cần những state như trên để đảm bảo UX/UI mượt mà hơn cho ứng dụng. 259 | 260 | 4. Dự án có bao gồm Optimistic Updates. Optimistic Updates là gì? 261 | 262 | https://react-query.tanstack.com/guides/optimistic-updates 263 | 264 | https://stackoverflow.com/questions/33009657/what-is-optimistic-updates-in-front-end-development 265 | 266 | https://resthooks.io/docs/guides/optimistic-updates 267 | 268 | 5. Nếu bạn muốn setup WindiCSS cho 1 dự án mới với Vite: 269 | 270 | https://windicss.org/integrations/vite.html 271 | 272 | ## Tài liệu tham khảo 273 | 274 | - https://www.sitepoint.com/react-query-fetch-manage-data bởi [Michael Wanyoike](https://twitter.com/myxsys) 275 | - https://react-query.tanstack.com/overview 276 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tanstack Query Example (aka React Query) 2 | 3 | ## Preview 4 | 5 | ![Site Preview](./src/preview.PNG) 6 | 7 | _Note: old image from README 2022_ 8 | 9 | This project is a demonstration of how to use [Tanstack Query](https://tanstack.com/query/latest/docs/react/overview) and [Axios](https://github.com/axios/axios) to perform the following tasks: 10 | 11 | - CRUD Operations (`useQuery`, `useMutation`). 12 | - Pagination (`useQuery`) 13 | - Load-more like feature (`useInfiniteQuery`) 14 | 15 | ## Run Locally 16 | 17 | - Clone the project 18 | 19 | ```bash 20 | git clone https://github.com/nguyenhieptech/react-query-example 21 | ``` 22 | 23 | - Go to the project directory: 24 | 25 | ```bash 26 | cd react-query-example 27 | # or cd react-query-example-main 28 | ``` 29 | 30 | - Install dependencies 31 | 32 | ```bash 33 | yarn install 34 | ``` 35 | 36 | - Setup database file 37 | 38 | ```bash 39 | cp db/sample.db.json db/db.json 40 | ``` 41 | 42 | - Start the `json-server` 43 | 44 | ```bash 45 | yarn json-server 46 | ``` 47 | 48 | - Launch another terminal and start the `Vite` server 49 | 50 | ```bash 51 | yarn dev 52 | ``` 53 | 54 | Head over to your browser and open the URL to access the application. You can change the port in `vite.config.ts` 55 | 56 | ## Reference 57 | 58 | https://youtu.be/seU46c6Jz7E?si=JrfOKqLaY2udPQ1V 59 | 60 | https://youtu.be/r8Dg0KVnfMA?si=f2-B6c96MoVVGWto 61 | 62 | https://kyleshevlin.com/use-encapsulation 63 | 64 | https://log.seruco.io/hide-usequery/ 65 | 66 | https://stackoverflow.com/q/33009657/18459116 67 | 68 | https://resthooks.io/rest/guides/optimistic-updates 69 | 70 | https://www.sitepoint.com/react-query-fetch-manage-data 71 | 72 | https://tanstack.com/query/latest/docs/react/overview 73 | -------------------------------------------------------------------------------- /db/sample.db.json: -------------------------------------------------------------------------------- 1 | { 2 | "users": [ 3 | { 4 | "id": 1, 5 | "first_name": "Siffre", 6 | "last_name": "Timmes", 7 | "email": "stimmes0@nasa.gov", 8 | "gender": "Male" 9 | }, 10 | { 11 | "id": 2, 12 | "first_name": "Fonzie", 13 | "last_name": "Coggen", 14 | "email": "fcoggen1@weather.com", 15 | "gender": "Female" 16 | }, 17 | { 18 | "id": 3, 19 | "first_name": "Shell", 20 | "last_name": "Kos", 21 | "email": "skos2@prweb.com", 22 | "gender": "Female" 23 | }, 24 | { 25 | "id": 4, 26 | "first_name": "Matthiew", 27 | "last_name": "Rasell", 28 | "email": "mrasell3@oaic.gov.au", 29 | "gender": "Female" 30 | }, 31 | { 32 | "id": 5, 33 | "first_name": "Phillipe", 34 | "last_name": "Sedgwick", 35 | "email": "psedgwick4@sciencedirect.com", 36 | "gender": "Female" 37 | }, 38 | { 39 | "id": 6, 40 | "first_name": "Lammond", 41 | "last_name": "Urion", 42 | "email": "lurion5@statcounter.com", 43 | "gender": "Male" 44 | }, 45 | { 46 | "id": 7, 47 | "first_name": "Jeanine", 48 | "last_name": "Wicks", 49 | "email": "jwicks6@opera.com", 50 | "gender": "Female" 51 | }, 52 | { 53 | "id": 8, 54 | "first_name": "Jemimah", 55 | "last_name": "Leisk", 56 | "email": "jleisk7@t.co", 57 | "gender": "Female" 58 | }, 59 | { 60 | "id": 9, 61 | "first_name": "Rania", 62 | "last_name": "Ibeson", 63 | "email": "ribeson8@smugmug.com", 64 | "gender": "Male" 65 | }, 66 | { 67 | "id": 10, 68 | "first_name": "Cathleen", 69 | "last_name": "Totterdell", 70 | "email": "ctotterdell9@washingtonpost.com", 71 | "gender": "Male" 72 | }, 73 | { 74 | "id": 11, 75 | "first_name": "Guthrey", 76 | "last_name": "Iron", 77 | "email": "girona@infoseek.co.jp", 78 | "gender": "Female" 79 | }, 80 | { 81 | "id": 12, 82 | "first_name": "Hale", 83 | "last_name": "Sorey", 84 | "email": "hsoreyb@epa.gov", 85 | "gender": "Male" 86 | }, 87 | { 88 | "id": 13, 89 | "first_name": "Trey", 90 | "last_name": "Yatman", 91 | "email": "tyatmanc@amazon.de", 92 | "gender": "Female" 93 | }, 94 | { 95 | "id": 14, 96 | "first_name": "Ermina", 97 | "last_name": "Trickey", 98 | "email": "etrickeyd@sciencedirect.com", 99 | "gender": "Female" 100 | }, 101 | { 102 | "id": 15, 103 | "first_name": "Valentino", 104 | "last_name": "Tabor", 105 | "email": "vtabore@youtu.be", 106 | "gender": "Female" 107 | }, 108 | { 109 | "id": 16, 110 | "first_name": "Annabela", 111 | "last_name": "Joska", 112 | "email": "ajoskaf@fda.gov", 113 | "gender": "Female" 114 | }, 115 | { 116 | "id": 17, 117 | "first_name": "Dionis", 118 | "last_name": "Karoly", 119 | "email": "dkarolyg@census.gov", 120 | "gender": "Female" 121 | }, 122 | { 123 | "id": 18, 124 | "first_name": "Eleanore", 125 | "last_name": "Abbs", 126 | "email": "eabbsh@gov.uk", 127 | "gender": "Female" 128 | }, 129 | { 130 | "id": 19, 131 | "first_name": "Roddy", 132 | "last_name": "Bullas", 133 | "email": "rbullasi@devhub.com", 134 | "gender": "Female" 135 | }, 136 | { 137 | "id": 20, 138 | "first_name": "Carline", 139 | "last_name": "Stanners", 140 | "email": "cstannersj@newsvine.com", 141 | "gender": "Male" 142 | }, 143 | { 144 | "id": 21, 145 | "first_name": "Rayner", 146 | "last_name": "McGloin", 147 | "email": "rmcgloink@businessinsider.com", 148 | "gender": "Male" 149 | }, 150 | { 151 | "id": 22, 152 | "first_name": "Leoine", 153 | "last_name": "Abele", 154 | "email": "labelel@yellowbook.com", 155 | "gender": "Male" 156 | }, 157 | { 158 | "id": 23, 159 | "first_name": "Eryn", 160 | "last_name": "Feragh", 161 | "email": "eferaghm@china.com.cn", 162 | "gender": "Female" 163 | }, 164 | { 165 | "id": 24, 166 | "first_name": "Jocko", 167 | "last_name": "Sansam", 168 | "email": "jsansamn@sciencedaily.com", 169 | "gender": "Female" 170 | }, 171 | { 172 | "id": 25, 173 | "first_name": "Free", 174 | "last_name": "Pimblott", 175 | "email": "fpimblotto@yale.edu", 176 | "gender": "Female" 177 | }, 178 | { 179 | "id": 26, 180 | "first_name": "Kalila", 181 | "last_name": "Mariyushkin", 182 | "email": "kmariyushkinp@google.nl", 183 | "gender": "Female" 184 | }, 185 | { 186 | "id": 27, 187 | "first_name": "Natty", 188 | "last_name": "Juanico", 189 | "email": "njuanicoq@studiopress.com", 190 | "gender": "Female" 191 | }, 192 | { 193 | "id": 28, 194 | "first_name": "Nathaniel", 195 | "last_name": "Henrys", 196 | "email": "nhenrysr@bluehost.com", 197 | "gender": "Female" 198 | }, 199 | { 200 | "id": 29, 201 | "first_name": "Kalil", 202 | "last_name": "Quantick", 203 | "email": "kquanticks@ox.ac.uk", 204 | "gender": "Female" 205 | }, 206 | { 207 | "id": 30, 208 | "first_name": "Antony", 209 | "last_name": "Bubbins", 210 | "email": "abubbinst@latimes.com", 211 | "gender": "Male" 212 | }, 213 | { 214 | "id": 31, 215 | "first_name": "Corissa", 216 | "last_name": "Heaps", 217 | "email": "cheapsu@cam.ac.uk", 218 | "gender": "Female" 219 | }, 220 | { 221 | "id": 32, 222 | "first_name": "Lou", 223 | "last_name": "Hawket", 224 | "email": "lhawketv@cdbaby.com", 225 | "gender": "Female" 226 | }, 227 | { 228 | "id": 33, 229 | "first_name": "Tyson", 230 | "last_name": "Brindley", 231 | "email": "tbrindleyw@clickbank.net", 232 | "gender": "Male" 233 | }, 234 | { 235 | "id": 34, 236 | "first_name": "Burnard", 237 | "last_name": "Gleder", 238 | "email": "bglederx@netscape.com", 239 | "gender": "Female" 240 | }, 241 | { 242 | "id": 35, 243 | "first_name": "Eben", 244 | "last_name": "Crinidge", 245 | "email": "ecrinidgey@dmoz.org", 246 | "gender": "Female" 247 | }, 248 | { 249 | "id": 36, 250 | "first_name": "Rahel", 251 | "last_name": "Fruin", 252 | "email": "rfruinz@addtoany.com", 253 | "gender": "Female" 254 | }, 255 | { 256 | "id": 37, 257 | "first_name": "Lewie", 258 | "last_name": "Slot", 259 | "email": "lslot10@t.co", 260 | "gender": "Male" 261 | }, 262 | { 263 | "id": 38, 264 | "first_name": "Erinn", 265 | "last_name": "Mardlin", 266 | "email": "emardlin11@taobao.com", 267 | "gender": "Female" 268 | }, 269 | { 270 | "id": 39, 271 | "first_name": "Ole", 272 | "last_name": "Brotherick", 273 | "email": "obrotherick12@macromedia.com", 274 | "gender": "Female" 275 | }, 276 | { 277 | "id": 40, 278 | "first_name": "Lionel", 279 | "last_name": "Tuckey", 280 | "email": "ltuckey13@princeton.edu", 281 | "gender": "Male" 282 | }, 283 | { 284 | "id": 41, 285 | "first_name": "Alfie", 286 | "last_name": "Beevis", 287 | "email": "abeevis14@rambler.ru", 288 | "gender": "Male" 289 | }, 290 | { 291 | "id": 42, 292 | "first_name": "Whitby", 293 | "last_name": "Damrell", 294 | "email": "wdamrell15@i2i.jp", 295 | "gender": "Female" 296 | }, 297 | { 298 | "id": 43, 299 | "first_name": "Fairleigh", 300 | "last_name": "Staner", 301 | "email": "fstaner16@tripod.com", 302 | "gender": "Female" 303 | }, 304 | { 305 | "id": 44, 306 | "first_name": "Kori", 307 | "last_name": "Doe", 308 | "email": "kdoe17@latimes.com", 309 | "gender": "Male" 310 | }, 311 | { 312 | "id": 45, 313 | "first_name": "Shandie", 314 | "last_name": "Roseveare", 315 | "email": "sroseveare18@zimbio.com", 316 | "gender": "Female" 317 | }, 318 | { 319 | "id": 46, 320 | "first_name": "Maryann", 321 | "last_name": "Reily", 322 | "email": "mreily19@domainmarket.com", 323 | "gender": "Female" 324 | }, 325 | { 326 | "id": 47, 327 | "first_name": "Yuma", 328 | "last_name": "Vane", 329 | "email": "yvane1a@nsw.gov.au", 330 | "gender": "Female" 331 | }, 332 | { 333 | "id": 48, 334 | "first_name": "Celie", 335 | "last_name": "Kembry", 336 | "email": "ckembry1b@jalbum.net", 337 | "gender": "Male" 338 | }, 339 | { 340 | "id": 49, 341 | "first_name": "Sanford", 342 | "last_name": "Kingerby", 343 | "email": "skingerby1c@mozilla.com", 344 | "gender": "Female" 345 | }, 346 | { 347 | "id": 50, 348 | "first_name": "Tad", 349 | "last_name": "Malden", 350 | "email": "tmalden1d@comsenz.com", 351 | "gender": "Male" 352 | }, 353 | { 354 | "id": 51, 355 | "first_name": "Elenore", 356 | "last_name": "Denekamp", 357 | "email": "edenekamp1e@latimes.com", 358 | "gender": "Male" 359 | }, 360 | { 361 | "id": 52, 362 | "first_name": "Aubree", 363 | "last_name": "Dearnly", 364 | "email": "adearnly1f@gov.uk", 365 | "gender": "Male" 366 | }, 367 | { 368 | "id": 53, 369 | "first_name": "Mirelle", 370 | "last_name": "Teacy", 371 | "email": "mteacy1g@psu.edu", 372 | "gender": "Female" 373 | }, 374 | { 375 | "id": 54, 376 | "first_name": "Lib", 377 | "last_name": "Chippindall", 378 | "email": "lchippindall1h@senate.gov", 379 | "gender": "Female" 380 | }, 381 | { 382 | "id": 55, 383 | "first_name": "Bradly", 384 | "last_name": "Dener", 385 | "email": "bdener1i@rediff.com", 386 | "gender": "Female" 387 | }, 388 | { 389 | "id": 56, 390 | "first_name": "Babara", 391 | "last_name": "Andreolli", 392 | "email": "bandreolli1j@cbsnews.com", 393 | "gender": "Male" 394 | }, 395 | { 396 | "id": 57, 397 | "first_name": "Osborne", 398 | "last_name": "Traise", 399 | "email": "otraise1k@oracle.com", 400 | "gender": "Female" 401 | }, 402 | { 403 | "id": 58, 404 | "first_name": "Reynold", 405 | "last_name": "Donlon", 406 | "email": "rdonlon1l@photobucket.com", 407 | "gender": "Female" 408 | }, 409 | { 410 | "id": 59, 411 | "first_name": "Berne", 412 | "last_name": "Minto", 413 | "email": "bminto1m@pcworld.com", 414 | "gender": "Female" 415 | }, 416 | { 417 | "id": 60, 418 | "first_name": "Tymon", 419 | "last_name": "Eglise", 420 | "email": "teglise1n@bloomberg.com", 421 | "gender": "Male" 422 | }, 423 | { 424 | "id": 61, 425 | "first_name": "Tallulah", 426 | "last_name": "Futcher", 427 | "email": "tfutcher1o@so-net.ne.jp", 428 | "gender": "Male" 429 | }, 430 | { 431 | "id": 62, 432 | "first_name": "Deeann", 433 | "last_name": "Stansbie", 434 | "email": "dstansbie1p@cyberchimps.com", 435 | "gender": "Female" 436 | }, 437 | { 438 | "id": 63, 439 | "first_name": "Glory", 440 | "last_name": "Savin", 441 | "email": "gsavin1q@vinaora.com", 442 | "gender": "Female" 443 | }, 444 | { 445 | "id": 64, 446 | "first_name": "Corbet", 447 | "last_name": "Roffe", 448 | "email": "croffe1r@youku.com", 449 | "gender": "Male" 450 | }, 451 | { 452 | "id": 65, 453 | "first_name": "Rhett", 454 | "last_name": "Shute", 455 | "email": "rshute1s@addtoany.com", 456 | "gender": "Female" 457 | }, 458 | { 459 | "id": 66, 460 | "first_name": "Vidovik", 461 | "last_name": "Albertson", 462 | "email": "valbertson1t@naver.com", 463 | "gender": "Female" 464 | }, 465 | { 466 | "id": 67, 467 | "first_name": "Carny", 468 | "last_name": "Heminsley", 469 | "email": "cheminsley1u@mit.edu", 470 | "gender": "Female" 471 | }, 472 | { 473 | "id": 68, 474 | "first_name": "Bernardine", 475 | "last_name": "Counsell", 476 | "email": "bcounsell1v@networkadvertising.org", 477 | "gender": "Female" 478 | }, 479 | { 480 | "id": 69, 481 | "first_name": "Beck", 482 | "last_name": "Wadge", 483 | "email": "bwadge1w@sitemeter.com", 484 | "gender": "Female" 485 | }, 486 | { 487 | "id": 70, 488 | "first_name": "Edik", 489 | "last_name": "MacKall", 490 | "email": "emackall1x@reference.com", 491 | "gender": "Female" 492 | }, 493 | { 494 | "id": 71, 495 | "first_name": "Rikki", 496 | "last_name": "Blount", 497 | "email": "rblount1y@google.com.hk", 498 | "gender": "Male" 499 | }, 500 | { 501 | "id": 72, 502 | "first_name": "Marga", 503 | "last_name": "Gawthrop", 504 | "email": "mgawthrop1z@cafepress.com", 505 | "gender": "Female" 506 | }, 507 | { 508 | "id": 73, 509 | "first_name": "Nina", 510 | "last_name": "Pippard", 511 | "email": "npippard20@harvard.edu", 512 | "gender": "Male" 513 | }, 514 | { 515 | "id": 74, 516 | "first_name": "Perkin", 517 | "last_name": "Turvie", 518 | "email": "pturvie21@npr.org", 519 | "gender": "Female" 520 | }, 521 | { 522 | "id": 75, 523 | "first_name": "Errol", 524 | "last_name": "Koschek", 525 | "email": "ekoschek22@narod.ru", 526 | "gender": "Male" 527 | }, 528 | { 529 | "id": 76, 530 | "first_name": "Alister", 531 | "last_name": "Heinsius", 532 | "email": "aheinsius23@mapquest.com", 533 | "gender": "Male" 534 | }, 535 | { 536 | "id": 77, 537 | "first_name": "Igor", 538 | "last_name": "Wallentin", 539 | "email": "iwallentin24@issuu.com", 540 | "gender": "Female" 541 | }, 542 | { 543 | "id": 78, 544 | "first_name": "Sarine", 545 | "last_name": "Ottam", 546 | "email": "sottam25@baidu.com", 547 | "gender": "Female" 548 | }, 549 | { 550 | "id": 79, 551 | "first_name": "Irvine", 552 | "last_name": "Beechcraft", 553 | "email": "ibeechcraft26@rambler.ru", 554 | "gender": "Male" 555 | }, 556 | { 557 | "id": 80, 558 | "first_name": "Bondon", 559 | "last_name": "Tulley", 560 | "email": "btulley27@nasa.gov", 561 | "gender": "Female" 562 | }, 563 | { 564 | "id": 81, 565 | "first_name": "Aeriela", 566 | "last_name": "Hurford", 567 | "email": "ahurford28@nationalgeographic.com", 568 | "gender": "Male" 569 | }, 570 | { 571 | "id": 82, 572 | "first_name": "Sybil", 573 | "last_name": "Bagott", 574 | "email": "sbagott29@skype.com", 575 | "gender": "Female" 576 | }, 577 | { 578 | "id": 83, 579 | "first_name": "Budd", 580 | "last_name": "Doeg", 581 | "email": "bdoeg2a@redcross.org", 582 | "gender": "Female" 583 | }, 584 | { 585 | "id": 84, 586 | "first_name": "Karlyn", 587 | "last_name": "McKinnell", 588 | "email": "kmckinnell2b@upenn.edu", 589 | "gender": "Male" 590 | }, 591 | { 592 | "id": 85, 593 | "first_name": "Lorianna", 594 | "last_name": "Piburn", 595 | "email": "lpiburn2c@stanford.edu", 596 | "gender": "Female" 597 | }, 598 | { 599 | "id": 86, 600 | "first_name": "Alfi", 601 | "last_name": "Ferrier", 602 | "email": "aferrier2d@twitpic.com", 603 | "gender": "Male" 604 | }, 605 | { 606 | "id": 87, 607 | "first_name": "Cherie", 608 | "last_name": "Theyer", 609 | "email": "ctheyer2e@prlog.org", 610 | "gender": "Male" 611 | }, 612 | { 613 | "id": 88, 614 | "first_name": "Jard", 615 | "last_name": "Issacov", 616 | "email": "jissacov2f@vinaora.com", 617 | "gender": "Male" 618 | }, 619 | { 620 | "id": 89, 621 | "first_name": "Hamilton", 622 | "last_name": "Lynas", 623 | "email": "hlynas2g@yahoo.co.jp", 624 | "gender": "Female" 625 | }, 626 | { 627 | "id": 90, 628 | "first_name": "Nettie", 629 | "last_name": "Zellner", 630 | "email": "nzellner2h@nbcnews.com", 631 | "gender": "Female" 632 | }, 633 | { 634 | "id": 91, 635 | "first_name": "Yasmin", 636 | "last_name": "Sturridge", 637 | "email": "ysturridge2i@umich.edu", 638 | "gender": "Female" 639 | }, 640 | { 641 | "id": 92, 642 | "first_name": "Gino", 643 | "last_name": "Rawlyns", 644 | "email": "grawlyns2j@g.co", 645 | "gender": "Male" 646 | }, 647 | { 648 | "id": 93, 649 | "first_name": "Kaitlin", 650 | "last_name": "Forstall", 651 | "email": "kforstall2k@surveymonkey.com", 652 | "gender": "Female" 653 | }, 654 | { 655 | "id": 94, 656 | "first_name": "Filberto", 657 | "last_name": "Cocking", 658 | "email": "fcocking2l@themeforest.net", 659 | "gender": "Female" 660 | }, 661 | { 662 | "id": 95, 663 | "first_name": "Malchy", 664 | "last_name": "Eite", 665 | "email": "meite2m@ehow.com", 666 | "gender": "Female" 667 | }, 668 | { 669 | "id": 96, 670 | "first_name": "Vonny", 671 | "last_name": "Antunes", 672 | "email": "vantunes2n@creativecommons.org", 673 | "gender": "Female" 674 | }, 675 | { 676 | "id": 97, 677 | "first_name": "Susy", 678 | "last_name": "Auchterlony", 679 | "email": "sauchterlony2o@dropbox.com", 680 | "gender": "Male" 681 | }, 682 | { 683 | "id": 98, 684 | "first_name": "Fredra", 685 | "last_name": "Pendrey", 686 | "email": "fpendrey2p@hugedomains.com", 687 | "gender": "Male" 688 | }, 689 | { 690 | "id": 99, 691 | "first_name": "Eddie", 692 | "last_name": "Symcock", 693 | "email": "esymcock2q@amazon.de", 694 | "gender": "Female" 695 | }, 696 | { 697 | "id": 100, 698 | "first_name": "Jules", 699 | "last_name": "Petti", 700 | "email": "jpetti2r@elpais.com", 701 | "gender": "Male" 702 | }, 703 | { 704 | "id": 101, 705 | "first_name": "Rich", 706 | "last_name": "Neame", 707 | "email": "rneame2s@google.pl", 708 | "gender": "Male" 709 | }, 710 | { 711 | "id": 102, 712 | "first_name": "Tressa", 713 | "last_name": "Baggaley", 714 | "email": "tbaggaley2t@cocolog-nifty.com", 715 | "gender": "Female" 716 | }, 717 | { 718 | "id": 103, 719 | "first_name": "Lauryn", 720 | "last_name": "Rouse", 721 | "email": "lrouse2u@pen.io", 722 | "gender": "Male" 723 | }, 724 | { 725 | "id": 104, 726 | "first_name": "Saunder", 727 | "last_name": "Holberry", 728 | "email": "sholberry2v@home.pl", 729 | "gender": "Male" 730 | }, 731 | { 732 | "id": 105, 733 | "first_name": "Stoddard", 734 | "last_name": "Leatherborrow", 735 | "email": "sleatherborrow2w@ehow.com", 736 | "gender": "Female" 737 | }, 738 | { 739 | "id": 106, 740 | "first_name": "Leah", 741 | "last_name": "Whittick", 742 | "email": "lwhittick2x@jimdo.com", 743 | "gender": "Male" 744 | }, 745 | { 746 | "id": 107, 747 | "first_name": "Liam", 748 | "last_name": "Brecon", 749 | "email": "lbrecon2y@seattletimes.com", 750 | "gender": "Male" 751 | }, 752 | { 753 | "id": 108, 754 | "first_name": "Idalia", 755 | "last_name": "Shervil", 756 | "email": "ishervil2z@over-blog.com", 757 | "gender": "Female" 758 | }, 759 | { 760 | "id": 109, 761 | "first_name": "Farah", 762 | "last_name": "Van Halen", 763 | "email": "fvanhalen30@nps.gov", 764 | "gender": "Female" 765 | }, 766 | { 767 | "id": 110, 768 | "first_name": "Crichton", 769 | "last_name": "Croal", 770 | "email": "ccroal31@xing.com", 771 | "gender": "Female" 772 | }, 773 | { 774 | "id": 111, 775 | "first_name": "Garrard", 776 | "last_name": "Edgcombe", 777 | "email": "gedgcombe32@geocities.jp", 778 | "gender": "Male" 779 | }, 780 | { 781 | "id": 112, 782 | "first_name": "Neale", 783 | "last_name": "Sheepy", 784 | "email": "nsheepy33@epa.gov", 785 | "gender": "Female" 786 | }, 787 | { 788 | "id": 113, 789 | "first_name": "Maren", 790 | "last_name": "Calverd", 791 | "email": "mcalverd34@bloomberg.com", 792 | "gender": "Female" 793 | }, 794 | { 795 | "id": 114, 796 | "first_name": "Lorrin", 797 | "last_name": "Lorente", 798 | "email": "llorente35@loc.gov", 799 | "gender": "Male" 800 | }, 801 | { 802 | "id": 115, 803 | "first_name": "Andonis", 804 | "last_name": "Matokhnin", 805 | "email": "amatokhnin36@hatena.ne.jp", 806 | "gender": "Female" 807 | }, 808 | { 809 | "id": 116, 810 | "first_name": "Blisse", 811 | "last_name": "Hickin", 812 | "email": "bhickin37@europa.eu", 813 | "gender": "Female" 814 | }, 815 | { 816 | "id": 117, 817 | "first_name": "Neal", 818 | "last_name": "Overell", 819 | "email": "noverell38@discovery.com", 820 | "gender": "Female" 821 | }, 822 | { 823 | "id": 118, 824 | "first_name": "Gaspard", 825 | "last_name": "Rennels", 826 | "email": "grennels39@dailymotion.com", 827 | "gender": "Male" 828 | }, 829 | { 830 | "id": 119, 831 | "first_name": "Beverie", 832 | "last_name": "Bushen", 833 | "email": "bbushen3a@vistaprint.com", 834 | "gender": "Female" 835 | }, 836 | { 837 | "id": 120, 838 | "first_name": "Leanor", 839 | "last_name": "Ravillas", 840 | "email": "lravillas3b@flavors.me", 841 | "gender": "Male" 842 | }, 843 | { 844 | "id": 121, 845 | "first_name": "Avigdor", 846 | "last_name": "Jahncke", 847 | "email": "ajahncke3c@homestead.com", 848 | "gender": "Female" 849 | }, 850 | { 851 | "id": 122, 852 | "first_name": "Gawain", 853 | "last_name": "Schuler", 854 | "email": "gschuler3d@earthlink.net", 855 | "gender": "Male" 856 | }, 857 | { 858 | "id": 123, 859 | "first_name": "Karoly", 860 | "last_name": "Jenkerson", 861 | "email": "kjenkerson3e@indiegogo.com", 862 | "gender": "Female" 863 | }, 864 | { 865 | "id": 124, 866 | "first_name": "Smith", 867 | "last_name": "Andreotti", 868 | "email": "sandreotti3f@wix.com", 869 | "gender": "Male" 870 | }, 871 | { 872 | "id": 125, 873 | "first_name": "Correy", 874 | "last_name": "Britney", 875 | "email": "cbritney3g@google.nl", 876 | "gender": "Male" 877 | }, 878 | { 879 | "id": 126, 880 | "first_name": "Lyndsie", 881 | "last_name": "Adger", 882 | "email": "ladger3h@booking.com", 883 | "gender": "Female" 884 | }, 885 | { 886 | "id": 127, 887 | "first_name": "Erda", 888 | "last_name": "Litel", 889 | "email": "elitel3i@virginia.edu", 890 | "gender": "Female" 891 | }, 892 | { 893 | "id": 128, 894 | "first_name": "Penny", 895 | "last_name": "Lauritsen", 896 | "email": "plauritsen3j@psu.edu", 897 | "gender": "Female" 898 | }, 899 | { 900 | "id": 129, 901 | "first_name": "Fernanda", 902 | "last_name": "Silvers", 903 | "email": "fsilvers3k@creativecommons.org", 904 | "gender": "Female" 905 | }, 906 | { 907 | "id": 130, 908 | "first_name": "Pippo", 909 | "last_name": "Hutchinson", 910 | "email": "phutchinson3l@spiegel.de", 911 | "gender": "Female" 912 | }, 913 | { 914 | "id": 131, 915 | "first_name": "Ciro", 916 | "last_name": "Foxworthy", 917 | "email": "cfoxworthy3m@vistaprint.com", 918 | "gender": "Male" 919 | }, 920 | { 921 | "id": 132, 922 | "first_name": "Whitman", 923 | "last_name": "Bowditch", 924 | "email": "wbowditch3n@imdb.com", 925 | "gender": "Female" 926 | }, 927 | { 928 | "id": 133, 929 | "first_name": "Guglielma", 930 | "last_name": "Strangeways", 931 | "email": "gstrangeways3o@flavors.me", 932 | "gender": "Female" 933 | }, 934 | { 935 | "id": 134, 936 | "first_name": "Rodrigo", 937 | "last_name": "Shord", 938 | "email": "rshord3p@1688.com", 939 | "gender": "Female" 940 | }, 941 | { 942 | "id": 135, 943 | "first_name": "Mada", 944 | "last_name": "Peeter", 945 | "email": "mpeeter3q@jalbum.net", 946 | "gender": "Female" 947 | }, 948 | { 949 | "id": 136, 950 | "first_name": "Dalis", 951 | "last_name": "Plaister", 952 | "email": "dplaister3r@about.com", 953 | "gender": "Male" 954 | }, 955 | { 956 | "id": 137, 957 | "first_name": "Shirline", 958 | "last_name": "Coulbeck", 959 | "email": "scoulbeck3s@answers.com", 960 | "gender": "Female" 961 | }, 962 | { 963 | "id": 138, 964 | "first_name": "Kalvin", 965 | "last_name": "Miell", 966 | "email": "kmiell3t@yandex.ru", 967 | "gender": "Male" 968 | }, 969 | { 970 | "id": 139, 971 | "first_name": "Cyrill", 972 | "last_name": "Hennington", 973 | "email": "chennington3u@cnbc.com", 974 | "gender": "Male" 975 | }, 976 | { 977 | "id": 140, 978 | "first_name": "Theressa", 979 | "last_name": "Kennler", 980 | "email": "tkennler3v@flavors.me", 981 | "gender": "Female" 982 | }, 983 | { 984 | "id": 141, 985 | "first_name": "Berton", 986 | "last_name": "Kerwin", 987 | "email": "bkerwin3w@cpanel.net", 988 | "gender": "Male" 989 | }, 990 | { 991 | "id": 142, 992 | "first_name": "Skippy", 993 | "last_name": "Karp", 994 | "email": "skarp3x@wordpress.com", 995 | "gender": "Female" 996 | }, 997 | { 998 | "id": 143, 999 | "first_name": "Casper", 1000 | "last_name": "McKeggie", 1001 | "email": "cmckeggie3y@e-recht24.de", 1002 | "gender": "Male" 1003 | }, 1004 | { 1005 | "id": 144, 1006 | "first_name": "Carrie", 1007 | "last_name": "Balloch", 1008 | "email": "cballoch3z@buzzfeed.com", 1009 | "gender": "Female" 1010 | }, 1011 | { 1012 | "id": 145, 1013 | "first_name": "Sandye", 1014 | "last_name": "McCumskay", 1015 | "email": "smccumskay40@youku.com", 1016 | "gender": "Female" 1017 | }, 1018 | { 1019 | "id": 146, 1020 | "first_name": "Rickey", 1021 | "last_name": "Siggee", 1022 | "email": "rsiggee41@surveymonkey.com", 1023 | "gender": "Male" 1024 | }, 1025 | { 1026 | "id": 147, 1027 | "first_name": "Lelia", 1028 | "last_name": "Cheng", 1029 | "email": "lcheng42@cdc.gov", 1030 | "gender": "Male" 1031 | }, 1032 | { 1033 | "id": 148, 1034 | "first_name": "Kippar", 1035 | "last_name": "Donnett", 1036 | "email": "kdonnett43@tmall.com", 1037 | "gender": "Female" 1038 | }, 1039 | { 1040 | "id": 149, 1041 | "first_name": "Bobbie", 1042 | "last_name": "Sambles", 1043 | "email": "bsambles44@wordpress.org", 1044 | "gender": "Female" 1045 | }, 1046 | { 1047 | "id": 150, 1048 | "first_name": "Farrah", 1049 | "last_name": "Oag", 1050 | "email": "foag45@elpais.com", 1051 | "gender": "Female" 1052 | }, 1053 | { 1054 | "id": 151, 1055 | "first_name": "Inigo", 1056 | "last_name": "Gronowe", 1057 | "email": "igronowe46@posterous.com", 1058 | "gender": "Female" 1059 | }, 1060 | { 1061 | "id": 152, 1062 | "first_name": "Nicolis", 1063 | "last_name": "Sutherden", 1064 | "email": "nsutherden47@nature.com", 1065 | "gender": "Male" 1066 | }, 1067 | { 1068 | "id": 153, 1069 | "first_name": "Karna", 1070 | "last_name": "Quarterman", 1071 | "email": "kquarterman48@umich.edu", 1072 | "gender": "Female" 1073 | }, 1074 | { 1075 | "id": 154, 1076 | "first_name": "Barret", 1077 | "last_name": "MacFarlane", 1078 | "email": "bmacfarlane49@elpais.com", 1079 | "gender": "Female" 1080 | }, 1081 | { 1082 | "id": 155, 1083 | "first_name": "Aili", 1084 | "last_name": "Andrzejowski", 1085 | "email": "aandrzejowski4a@zimbio.com", 1086 | "gender": "Male" 1087 | }, 1088 | { 1089 | "id": 156, 1090 | "first_name": "Bevan", 1091 | "last_name": "Rosenhaupt", 1092 | "email": "brosenhaupt4b@amazon.de", 1093 | "gender": "Male" 1094 | }, 1095 | { 1096 | "id": 157, 1097 | "first_name": "Paula", 1098 | "last_name": "Shirtliff", 1099 | "email": "pshirtliff4c@phpbb.com", 1100 | "gender": "Female" 1101 | }, 1102 | { 1103 | "id": 158, 1104 | "first_name": "Pandora", 1105 | "last_name": "Verring", 1106 | "email": "pverring4d@ted.com", 1107 | "gender": "Male" 1108 | }, 1109 | { 1110 | "id": 159, 1111 | "first_name": "Amerigo", 1112 | "last_name": "Bettington", 1113 | "email": "abettington4e@icq.com", 1114 | "gender": "Female" 1115 | }, 1116 | { 1117 | "id": 160, 1118 | "first_name": "Pate", 1119 | "last_name": "Bunn", 1120 | "email": "pbunn4f@geocities.jp", 1121 | "gender": "Male" 1122 | }, 1123 | { 1124 | "id": 161, 1125 | "first_name": "Noam", 1126 | "last_name": "Winram", 1127 | "email": "nwinram4g@cyberchimps.com", 1128 | "gender": "Male" 1129 | }, 1130 | { 1131 | "id": 162, 1132 | "first_name": "Fabian", 1133 | "last_name": "Jerrome", 1134 | "email": "fjerrome4h@wikia.com", 1135 | "gender": "Male" 1136 | }, 1137 | { 1138 | "id": 163, 1139 | "first_name": "Magnum", 1140 | "last_name": "Reader", 1141 | "email": "mreader4i@si.edu", 1142 | "gender": "Male" 1143 | }, 1144 | { 1145 | "id": 164, 1146 | "first_name": "Eleanora", 1147 | "last_name": "Dekeyser", 1148 | "email": "edekeyser4j@chronoengine.com", 1149 | "gender": "Female" 1150 | }, 1151 | { 1152 | "id": 165, 1153 | "first_name": "Rosalind", 1154 | "last_name": "Stolze", 1155 | "email": "rstolze4k@multiply.com", 1156 | "gender": "Male" 1157 | }, 1158 | { 1159 | "id": 166, 1160 | "first_name": "Kinny", 1161 | "last_name": "Ambroisin", 1162 | "email": "kambroisin4l@dailymotion.com", 1163 | "gender": "Male" 1164 | }, 1165 | { 1166 | "id": 167, 1167 | "first_name": "Dorree", 1168 | "last_name": "Kuschel", 1169 | "email": "dkuschel4m@nymag.com", 1170 | "gender": "Female" 1171 | }, 1172 | { 1173 | "id": 168, 1174 | "first_name": "Consuela", 1175 | "last_name": "Gerhold", 1176 | "email": "cgerhold4n@gnu.org", 1177 | "gender": "Male" 1178 | }, 1179 | { 1180 | "id": 169, 1181 | "first_name": "Kaylyn", 1182 | "last_name": "Waddington", 1183 | "email": "kwaddington4o@woothemes.com", 1184 | "gender": "Female" 1185 | }, 1186 | { 1187 | "id": 170, 1188 | "first_name": "Kordula", 1189 | "last_name": "Taylorson", 1190 | "email": "ktaylorson4p@themeforest.net", 1191 | "gender": "Female" 1192 | }, 1193 | { 1194 | "id": 171, 1195 | "first_name": "Ermentrude", 1196 | "last_name": "Seath", 1197 | "email": "eseath4q@spiegel.de", 1198 | "gender": "Male" 1199 | }, 1200 | { 1201 | "id": 172, 1202 | "first_name": "Stanleigh", 1203 | "last_name": "Orpin", 1204 | "email": "sorpin4r@miitbeian.gov.cn", 1205 | "gender": "Female" 1206 | }, 1207 | { 1208 | "id": 173, 1209 | "first_name": "Fern", 1210 | "last_name": "Reagan", 1211 | "email": "freagan4s@so-net.ne.jp", 1212 | "gender": "Male" 1213 | }, 1214 | { 1215 | "id": 174, 1216 | "first_name": "Thorn", 1217 | "last_name": "Kleewein", 1218 | "email": "tkleewein4t@photobucket.com", 1219 | "gender": "Female" 1220 | }, 1221 | { 1222 | "id": 175, 1223 | "first_name": "Elianore", 1224 | "last_name": "Threadgold", 1225 | "email": "ethreadgold4u@icio.us", 1226 | "gender": "Female" 1227 | }, 1228 | { 1229 | "id": 176, 1230 | "first_name": "Merrill", 1231 | "last_name": "Embra", 1232 | "email": "membra4v@hibu.com", 1233 | "gender": "Female" 1234 | }, 1235 | { 1236 | "id": 177, 1237 | "first_name": "Brier", 1238 | "last_name": "Johnsson", 1239 | "email": "bjohnsson4w@tiny.cc", 1240 | "gender": "Male" 1241 | }, 1242 | { 1243 | "id": 178, 1244 | "first_name": "Binny", 1245 | "last_name": "Assiter", 1246 | "email": "bassiter4x@unc.edu", 1247 | "gender": "Female" 1248 | }, 1249 | { 1250 | "id": 179, 1251 | "first_name": "Emlen", 1252 | "last_name": "Rutland", 1253 | "email": "erutland4y@cpanel.net", 1254 | "gender": "Female" 1255 | }, 1256 | { 1257 | "id": 180, 1258 | "first_name": "Lianne", 1259 | "last_name": "Duff", 1260 | "email": "lduff4z@acquirethisname.com", 1261 | "gender": "Female" 1262 | }, 1263 | { 1264 | "id": 181, 1265 | "first_name": "Leonanie", 1266 | "last_name": "Diggens", 1267 | "email": "ldiggens50@example.com", 1268 | "gender": "Male" 1269 | }, 1270 | { 1271 | "id": 182, 1272 | "first_name": "Maegan", 1273 | "last_name": "Beech", 1274 | "email": "mbeech51@nytimes.com", 1275 | "gender": "Female" 1276 | }, 1277 | { 1278 | "id": 183, 1279 | "first_name": "Lothaire", 1280 | "last_name": "Bucktrout", 1281 | "email": "lbucktrout52@nhs.uk", 1282 | "gender": "Female" 1283 | }, 1284 | { 1285 | "id": 184, 1286 | "first_name": "Vernice", 1287 | "last_name": "Brandenberg", 1288 | "email": "vbrandenberg53@t-online.de", 1289 | "gender": "Female" 1290 | }, 1291 | { 1292 | "id": 185, 1293 | "first_name": "Ophelie", 1294 | "last_name": "Cookney", 1295 | "email": "ocookney54@cisco.com", 1296 | "gender": "Male" 1297 | }, 1298 | { 1299 | "id": 186, 1300 | "first_name": "Frasquito", 1301 | "last_name": "Keith", 1302 | "email": "fkeith55@adobe.com", 1303 | "gender": "Female" 1304 | }, 1305 | { 1306 | "id": 187, 1307 | "first_name": "Tani", 1308 | "last_name": "Lutas", 1309 | "email": "tlutas56@cbslocal.com", 1310 | "gender": "Female" 1311 | }, 1312 | { 1313 | "id": 188, 1314 | "first_name": "Jehu", 1315 | "last_name": "Nanninini", 1316 | "email": "jnanninini57@technorati.com", 1317 | "gender": "Male" 1318 | }, 1319 | { 1320 | "id": 189, 1321 | "first_name": "Nessi", 1322 | "last_name": "Gusticke", 1323 | "email": "ngusticke58@ezinearticles.com", 1324 | "gender": "Male" 1325 | }, 1326 | { 1327 | "id": 190, 1328 | "first_name": "Letitia", 1329 | "last_name": "Ilott", 1330 | "email": "lilott59@bloglovin.com", 1331 | "gender": "Female" 1332 | }, 1333 | { 1334 | "id": 191, 1335 | "first_name": "Gerardo", 1336 | "last_name": "Mortlock", 1337 | "email": "gmortlock5a@dot.gov", 1338 | "gender": "Female" 1339 | }, 1340 | { 1341 | "id": 192, 1342 | "first_name": "Jenni", 1343 | "last_name": "Gosnoll", 1344 | "email": "jgosnoll5b@gravatar.com", 1345 | "gender": "Male" 1346 | }, 1347 | { 1348 | "id": 193, 1349 | "first_name": "Billie", 1350 | "last_name": "Hiner", 1351 | "email": "bhiner5c@elpais.com", 1352 | "gender": "Male" 1353 | }, 1354 | { 1355 | "id": 194, 1356 | "first_name": "Nick", 1357 | "last_name": "Lewer", 1358 | "email": "nlewer5d@skyrock.com", 1359 | "gender": "Female" 1360 | }, 1361 | { 1362 | "id": 195, 1363 | "first_name": "Morly", 1364 | "last_name": "Sanchis", 1365 | "email": "msanchis5e@netlog.com", 1366 | "gender": "Male" 1367 | }, 1368 | { 1369 | "id": 196, 1370 | "first_name": "Raye", 1371 | "last_name": "Manshaw", 1372 | "email": "rmanshaw5f@dyndns.org", 1373 | "gender": "Male" 1374 | }, 1375 | { 1376 | "id": 197, 1377 | "first_name": "Filbert", 1378 | "last_name": "Andriss", 1379 | "email": "fandriss5g@free.fr", 1380 | "gender": "Male" 1381 | }, 1382 | { 1383 | "id": 198, 1384 | "first_name": "Ashely", 1385 | "last_name": "Hussey", 1386 | "email": "ahussey5h@ebay.co.uk", 1387 | "gender": "Male" 1388 | }, 1389 | { 1390 | "id": 199, 1391 | "first_name": "Alikee", 1392 | "last_name": "Mennell", 1393 | "email": "amennell5i@amazon.co.uk", 1394 | "gender": "Male" 1395 | }, 1396 | { 1397 | "id": 200, 1398 | "first_name": "Edvard", 1399 | "last_name": "Maddin", 1400 | "email": "emaddin5j@de.vu", 1401 | "gender": "Female" 1402 | }, 1403 | { 1404 | "id": 201, 1405 | "first_name": "Maryanna", 1406 | "last_name": "Calverd", 1407 | "email": "mcalverd5k@omniture.com", 1408 | "gender": "Female" 1409 | }, 1410 | { 1411 | "id": 202, 1412 | "first_name": "Revkah", 1413 | "last_name": "Kyle", 1414 | "email": "rkyle5l@163.com", 1415 | "gender": "Female" 1416 | }, 1417 | { 1418 | "id": 203, 1419 | "first_name": "Delphinia", 1420 | "last_name": "Geydon", 1421 | "email": "dgeydon5m@google.cn", 1422 | "gender": "Female" 1423 | }, 1424 | { 1425 | "id": 204, 1426 | "first_name": "Anallese", 1427 | "last_name": "Hirsthouse", 1428 | "email": "ahirsthouse5n@dailymotion.com", 1429 | "gender": "Male" 1430 | }, 1431 | { 1432 | "id": 205, 1433 | "first_name": "Peria", 1434 | "last_name": "Flageul", 1435 | "email": "pflageul5o@ed.gov", 1436 | "gender": "Female" 1437 | }, 1438 | { 1439 | "id": 206, 1440 | "first_name": "Tan", 1441 | "last_name": "Gammidge", 1442 | "email": "tgammidge5p@webs.com", 1443 | "gender": "Male" 1444 | }, 1445 | { 1446 | "id": 207, 1447 | "first_name": "Alphonse", 1448 | "last_name": "Goom", 1449 | "email": "agoom5q@123-reg.co.uk", 1450 | "gender": "Female" 1451 | }, 1452 | { 1453 | "id": 208, 1454 | "first_name": "Rosmunda", 1455 | "last_name": "Romaynes", 1456 | "email": "rromaynes5r@people.com.cn", 1457 | "gender": "Male" 1458 | }, 1459 | { 1460 | "id": 209, 1461 | "first_name": "Devlin", 1462 | "last_name": "Galley", 1463 | "email": "dgalley5s@devhub.com", 1464 | "gender": "Male" 1465 | }, 1466 | { 1467 | "id": 210, 1468 | "first_name": "Anthea", 1469 | "last_name": "Perrins", 1470 | "email": "aperrins5t@squarespace.com", 1471 | "gender": "Female" 1472 | }, 1473 | { 1474 | "id": 211, 1475 | "first_name": "Henri", 1476 | "last_name": "Rontsch", 1477 | "email": "hrontsch5u@nih.gov", 1478 | "gender": "Male" 1479 | }, 1480 | { 1481 | "id": 212, 1482 | "first_name": "Camey", 1483 | "last_name": "Quilleash", 1484 | "email": "cquilleash5v@diigo.com", 1485 | "gender": "Female" 1486 | }, 1487 | { 1488 | "id": 213, 1489 | "first_name": "Kristina", 1490 | "last_name": "Kiefer", 1491 | "email": "kkiefer5w@hugedomains.com", 1492 | "gender": "Male" 1493 | }, 1494 | { 1495 | "id": 214, 1496 | "first_name": "Whitman", 1497 | "last_name": "Cornhill", 1498 | "email": "wcornhill5x@yahoo.com", 1499 | "gender": "Female" 1500 | }, 1501 | { 1502 | "id": 215, 1503 | "first_name": "Toby", 1504 | "last_name": "Reuben", 1505 | "email": "treuben5y@vk.com", 1506 | "gender": "Female" 1507 | }, 1508 | { 1509 | "id": 216, 1510 | "first_name": "Martainn", 1511 | "last_name": "Massimi", 1512 | "email": "mmassimi5z@exblog.jp", 1513 | "gender": "Male" 1514 | }, 1515 | { 1516 | "id": 217, 1517 | "first_name": "Terri", 1518 | "last_name": "Leason", 1519 | "email": "tleason60@cpanel.net", 1520 | "gender": "Male" 1521 | }, 1522 | { 1523 | "id": 218, 1524 | "first_name": "Dyanna", 1525 | "last_name": "Ancell", 1526 | "email": "dancell61@php.net", 1527 | "gender": "Male" 1528 | }, 1529 | { 1530 | "id": 219, 1531 | "first_name": "Brandais", 1532 | "last_name": "Klus", 1533 | "email": "bklus62@telegraph.co.uk", 1534 | "gender": "Female" 1535 | }, 1536 | { 1537 | "id": 220, 1538 | "first_name": "Beth", 1539 | "last_name": "Bault", 1540 | "email": "bbault63@blogger.com", 1541 | "gender": "Male" 1542 | }, 1543 | { 1544 | "id": 221, 1545 | "first_name": "Vaughan", 1546 | "last_name": "Radborn", 1547 | "email": "vradborn64@dedecms.com", 1548 | "gender": "Male" 1549 | }, 1550 | { 1551 | "id": 222, 1552 | "first_name": "Randie", 1553 | "last_name": "Langran", 1554 | "email": "rlangran65@economist.com", 1555 | "gender": "Female" 1556 | }, 1557 | { 1558 | "id": 223, 1559 | "first_name": "Ealasaid", 1560 | "last_name": "Longworthy", 1561 | "email": "elongworthy66@123-reg.co.uk", 1562 | "gender": "Male" 1563 | }, 1564 | { 1565 | "id": 224, 1566 | "first_name": "Lauryn", 1567 | "last_name": "Joanaud", 1568 | "email": "ljoanaud67@dagondesign.com", 1569 | "gender": "Female" 1570 | }, 1571 | { 1572 | "id": 225, 1573 | "first_name": "Helsa", 1574 | "last_name": "Deavall", 1575 | "email": "hdeavall68@salon.com", 1576 | "gender": "Male" 1577 | }, 1578 | { 1579 | "id": 226, 1580 | "first_name": "Adey", 1581 | "last_name": "Taffee", 1582 | "email": "ataffee69@dyndns.org", 1583 | "gender": "Female" 1584 | }, 1585 | { 1586 | "id": 227, 1587 | "first_name": "Joseito", 1588 | "last_name": "Heatley", 1589 | "email": "jheatley6a@hugedomains.com", 1590 | "gender": "Male" 1591 | }, 1592 | { 1593 | "id": 228, 1594 | "first_name": "Clea", 1595 | "last_name": "McGarvey", 1596 | "email": "cmcgarvey6b@mapquest.com", 1597 | "gender": "Female" 1598 | }, 1599 | { 1600 | "id": 229, 1601 | "first_name": "Crissie", 1602 | "last_name": "Grout", 1603 | "email": "cgrout6c@booking.com", 1604 | "gender": "Female" 1605 | }, 1606 | { 1607 | "id": 230, 1608 | "first_name": "Dotty", 1609 | "last_name": "Bly", 1610 | "email": "dbly6d@ow.ly", 1611 | "gender": "Female" 1612 | }, 1613 | { 1614 | "id": 231, 1615 | "first_name": "Georas", 1616 | "last_name": "MacHoste", 1617 | "email": "gmachoste6e@dion.ne.jp", 1618 | "gender": "Male" 1619 | }, 1620 | { 1621 | "id": 232, 1622 | "first_name": "Darin", 1623 | "last_name": "Leavens", 1624 | "email": "dleavens6f@theguardian.com", 1625 | "gender": "Male" 1626 | }, 1627 | { 1628 | "id": 233, 1629 | "first_name": "Bertha", 1630 | "last_name": "Kleinstub", 1631 | "email": "bkleinstub6g@free.fr", 1632 | "gender": "Female" 1633 | }, 1634 | { 1635 | "id": 234, 1636 | "first_name": "Diarmid", 1637 | "last_name": "Bagniuk", 1638 | "email": "dbagniuk6h@simplemachines.org", 1639 | "gender": "Male" 1640 | }, 1641 | { 1642 | "id": 235, 1643 | "first_name": "Millisent", 1644 | "last_name": "Totterdill", 1645 | "email": "mtotterdill6i@mapquest.com", 1646 | "gender": "Female" 1647 | }, 1648 | { 1649 | "id": 236, 1650 | "first_name": "Lauree", 1651 | "last_name": "Berni", 1652 | "email": "lberni6j@digg.com", 1653 | "gender": "Male" 1654 | }, 1655 | { 1656 | "id": 237, 1657 | "first_name": "Flora", 1658 | "last_name": "Maymond", 1659 | "email": "fmaymond6k@zimbio.com", 1660 | "gender": "Female" 1661 | }, 1662 | { 1663 | "id": 238, 1664 | "first_name": "Butch", 1665 | "last_name": "Filippucci", 1666 | "email": "bfilippucci6l@tamu.edu", 1667 | "gender": "Male" 1668 | }, 1669 | { 1670 | "id": 239, 1671 | "first_name": "Marvin", 1672 | "last_name": "Pasquale", 1673 | "email": "mpasquale6m@wikispaces.com", 1674 | "gender": "Male" 1675 | }, 1676 | { 1677 | "id": 240, 1678 | "first_name": "Rex", 1679 | "last_name": "Derington", 1680 | "email": "rderington6n@google.pl", 1681 | "gender": "Female" 1682 | }, 1683 | { 1684 | "id": 241, 1685 | "first_name": "Fiann", 1686 | "last_name": "Loveridge", 1687 | "email": "floveridge6o@ucla.edu", 1688 | "gender": "Male" 1689 | }, 1690 | { 1691 | "id": 242, 1692 | "first_name": "Norri", 1693 | "last_name": "Kowalik", 1694 | "email": "nkowalik6p@examiner.com", 1695 | "gender": "Male" 1696 | }, 1697 | { 1698 | "id": 243, 1699 | "first_name": "Benny", 1700 | "last_name": "Loyns", 1701 | "email": "bloyns6q@nationalgeographic.com", 1702 | "gender": "Male" 1703 | }, 1704 | { 1705 | "id": 244, 1706 | "first_name": "Jaimie", 1707 | "last_name": "Looby", 1708 | "email": "jlooby6r@ihg.com", 1709 | "gender": "Male" 1710 | }, 1711 | { 1712 | "id": 245, 1713 | "first_name": "Casey", 1714 | "last_name": "Kiellor", 1715 | "email": "ckiellor6s@adobe.com", 1716 | "gender": "Male" 1717 | }, 1718 | { 1719 | "id": 246, 1720 | "first_name": "Glynis", 1721 | "last_name": "Heisler", 1722 | "email": "gheisler6t@instagram.com", 1723 | "gender": "Female" 1724 | }, 1725 | { 1726 | "id": 247, 1727 | "first_name": "Barnabas", 1728 | "last_name": "Tampin", 1729 | "email": "btampin6u@com.com", 1730 | "gender": "Male" 1731 | }, 1732 | { 1733 | "id": 248, 1734 | "first_name": "Shelba", 1735 | "last_name": "Stillmann", 1736 | "email": "sstillmann6v@taobao.com", 1737 | "gender": "Male" 1738 | }, 1739 | { 1740 | "id": 249, 1741 | "first_name": "Rhiamon", 1742 | "last_name": "De Mars", 1743 | "email": "rdemars6w@123-reg.co.uk", 1744 | "gender": "Female" 1745 | }, 1746 | { 1747 | "id": 250, 1748 | "first_name": "Lanette", 1749 | "last_name": "Corsor", 1750 | "email": "lcorsor6x@infoseek.co.jp", 1751 | "gender": "Female" 1752 | } 1753 | ] 1754 | } 1755 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React + TS 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-query-example", 3 | "private": true, 4 | "version": "0.1.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc && vite build", 9 | "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", 10 | "preview": "vite preview", 11 | "json-server": "json-server --watch db/db.json --port 3004" 12 | }, 13 | "dependencies": { 14 | "@headlessui/react": "^1.7.17", 15 | "@radix-ui/react-toast": "^1.1.4", 16 | "@tanstack/react-query": "^4.35.0", 17 | "@tanstack/react-query-devtools": "^4.35.0", 18 | "axios": "^1.5.0", 19 | "class-variance-authority": "^0.7.0", 20 | "clsx": "^2.0.0", 21 | "lucide-react": "^0.274.0", 22 | "react": "^18.2.0", 23 | "react-dom": "^18.2.0", 24 | "react-hook-form": "^7.46.1", 25 | "react-router-dom": "^6.15.0", 26 | "tailwind-merge": "^1.14.0" 27 | }, 28 | "devDependencies": { 29 | "@types/node": "^18.17.0", 30 | "@types/react": "^18.2.15", 31 | "@types/react-dom": "^18.2.7", 32 | "@typescript-eslint/eslint-plugin": "^6.0.0", 33 | "@typescript-eslint/parser": "^6.0.0", 34 | "@vitejs/plugin-react": "^4.0.3", 35 | "autoprefixer": "^10.4.15", 36 | "eslint": "^8.45.0", 37 | "eslint-plugin-react-hooks": "^4.6.0", 38 | "eslint-plugin-react-refresh": "^0.4.3", 39 | "json-server": "^0.17.3", 40 | "postcss": "^8.4.29", 41 | "prettier": "^3.0.3", 42 | "prettier-plugin-organize-imports": "^3.2.3", 43 | "prettier-plugin-tailwindcss": "^0.5.4", 44 | "tailwindcss": "^3.3.3", 45 | "typescript": "^5.0.2", 46 | "vite": "^4.4.5" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /rest.http: -------------------------------------------------------------------------------- 1 | # GET users request 2 | GET http://localhost:3004/users/ 3 | 4 | ### 5 | 6 | # GET sorted users request 7 | GET http://localhost:3004/users?_sort=id&_order=desc 8 | 9 | ### 10 | 11 | # GET paginated users request 12 | GET http://localhost:3004/users?_page=5&_limit=10 13 | 14 | ### 15 | 16 | 17 | # DELETE request 18 | GET http://localhost:3004/users/9 19 | 20 | ### 21 | -------------------------------------------------------------------------------- /src/api/api-client.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | export const apiClient = axios.create({ 4 | baseURL: 'http://localhost:3004/users/', 5 | }); 6 | 7 | export const API_METHODS = { 8 | GET: 'GET', 9 | POST: 'POST', 10 | PUT: 'PUT', 11 | PATCH: 'PATCH', 12 | DELETE: 'DELETE', 13 | OPTIONS: 'OPTIONS', 14 | } as const; 15 | -------------------------------------------------------------------------------- /src/api/index.ts: -------------------------------------------------------------------------------- 1 | export * from './api-client'; 2 | export * from './users'; 3 | -------------------------------------------------------------------------------- /src/api/users/index.ts: -------------------------------------------------------------------------------- 1 | export * from './use-create-user'; 2 | export * from './use-delete-user'; 3 | export * from './use-edit-user'; 4 | export * from './use-infinite-users'; 5 | export * from './use-paginated-users'; 6 | export * from './use-users'; 7 | export * from './user-query-keys'; 8 | export * from './use-user'; 9 | -------------------------------------------------------------------------------- /src/api/users/use-create-user.ts: -------------------------------------------------------------------------------- 1 | import { useMutation, useQueryClient } from '@tanstack/react-query'; 2 | import { apiClient, userQueryKeys } from '@/api'; 3 | import { useToast } from '@/components/ui'; 4 | import { TSFixMe, User } from '@/types'; 5 | 6 | const createUserFn = async (newUser: User) => { 7 | const response = await apiClient.post('', newUser); 8 | return response.data; 9 | }; 10 | 11 | // https://tanstack.com/query/latest/docs/react/guides/optimistic-updates 12 | export function useCreateUser() { 13 | const queryClient = useQueryClient(); 14 | const { toast } = useToast(); 15 | 16 | return useMutation({ 17 | mutationFn: createUserFn, 18 | onMutate: async () => { 19 | await queryClient.cancelQueries({ queryKey: userQueryKeys.all }); 20 | }, 21 | onSuccess: (data) => { 22 | toast({ 23 | title: 'New User Created', 24 | description: `Id: ${data.id} Name: ${data.first_name} ${data.last_name}`, 25 | }); 26 | }, 27 | onError: (err, newUser, context?: TSFixMe) => { 28 | queryClient.setQueryData(userQueryKeys.all, context.previousUsers); 29 | }, 30 | onSettled: () => { 31 | queryClient.invalidateQueries({ queryKey: userQueryKeys.all }); 32 | }, 33 | }); 34 | } 35 | -------------------------------------------------------------------------------- /src/api/users/use-delete-user.ts: -------------------------------------------------------------------------------- 1 | import { useMutation, useQueryClient } from '@tanstack/react-query'; 2 | import { apiClient, userQueryKeys } from '@/api'; 3 | import { toast } from '@/components/ui'; 4 | 5 | export interface Props { 6 | closeModal: () => void; 7 | id?: number; 8 | } 9 | 10 | export function useDeleteUser({ closeModal }: Props) { 11 | const queryClient = useQueryClient(); 12 | 13 | const deleteUserFn = async (id: number) => { 14 | const response = await apiClient.delete(`${id}`); 15 | return response; 16 | }; 17 | 18 | return useMutation({ 19 | mutationFn: deleteUserFn, 20 | onMutate: async () => { 21 | await queryClient.cancelQueries({ queryKey: userQueryKeys.all }); 22 | }, 23 | onSuccess: () => { 24 | toast({ 25 | title: 'Delete user successfully', 26 | }); 27 | }, 28 | onError: () => { 29 | queryClient.invalidateQueries({ queryKey: userQueryKeys.all }); 30 | }, 31 | onSettled: () => { 32 | queryClient.invalidateQueries({ queryKey: userQueryKeys.all }); 33 | closeModal(); 34 | }, 35 | }); 36 | } 37 | -------------------------------------------------------------------------------- /src/api/users/use-edit-user.ts: -------------------------------------------------------------------------------- 1 | import { useMutation, useQueryClient } from '@tanstack/react-query'; 2 | import { useParams } from 'react-router-dom'; 3 | import { apiClient, userQueryKeys } from '@/api'; 4 | import { TSFixMe, User } from '@/types'; 5 | 6 | export function useEditUser() { 7 | const { id } = useParams(); 8 | const queryClient = useQueryClient(); 9 | 10 | // https://react-query.tanstack.com/guides/optimistic-updates#updating-a-single-todo 11 | const editUserFn = async (updatedUser: User) => { 12 | const response = await apiClient.put(`/${id}`, updatedUser); 13 | return response; 14 | }; 15 | 16 | return useMutation({ 17 | mutationFn: editUserFn, 18 | onMutate: async (updatedUser) => { 19 | await queryClient.cancelQueries(userQueryKeys.detail(Number(id))); 20 | const previousUser = queryClient.getQueryData( 21 | userQueryKeys.detail(Number(id)) 22 | ); 23 | queryClient.setQueryData(userQueryKeys.detail(Number(id)), updatedUser); 24 | return { previousUser: previousUser, updatedUser: updatedUser }; 25 | }, 26 | onError: (err, updatedUser, context?: TSFixMe) => { 27 | queryClient.setQueryData( 28 | userQueryKeys.detail(Number(id)), 29 | context.previousUser 30 | ); 31 | }, 32 | onSettled: () => { 33 | queryClient.invalidateQueries(userQueryKeys.all); 34 | }, 35 | }); 36 | } 37 | -------------------------------------------------------------------------------- /src/api/users/use-infinite-users.ts: -------------------------------------------------------------------------------- 1 | import { apiClient, userQueryKeys } from '@/api'; 2 | import { useInfiniteQuery } from '@tanstack/react-query'; 3 | 4 | export function useInfiniteUsers({ pageLimit }: { pageLimit: number }) { 5 | const getInfiniteUsersFn = async ({ pageParam = 1 }) => { 6 | const response = await apiClient.get( 7 | `?_page=${pageParam}&_limit=${pageLimit}` 8 | ); 9 | return response; 10 | }; 11 | 12 | // https://joshgoestoflatiron.medium.com/february-10-pagination-in-a-json-server-api-with-the-link-header-dea63eb0a835 13 | const parseLinkHeader = (linkHeader: string) => { 14 | const linkHeadersArray = linkHeader 15 | .split(', ') 16 | .map((header: string) => header.split('; ')); 17 | 18 | const linkHeadersMap = linkHeadersArray.map((header: string[]) => { 19 | const thisHeaderRel = header[1].replace(/"/g, '').replace('rel=', ''); 20 | const thisHeaderUrl = header[0].slice(1, -1); 21 | return [thisHeaderRel, thisHeaderUrl]; 22 | }); 23 | 24 | return Object.fromEntries(linkHeadersMap); 25 | }; 26 | 27 | // https://tanstack.com/query/latest/docs/react/guides/infinite-queries 28 | return useInfiniteQuery({ 29 | queryKey: userQueryKeys.infinite(), 30 | queryFn: getInfiniteUsersFn, 31 | getNextPageParam: (lastPage) => { 32 | // The following code block is specific to json-server api 33 | const nextPageUrl = parseLinkHeader(lastPage.headers.link)['next']; 34 | if (nextPageUrl) { 35 | const queryString = nextPageUrl.substring( 36 | nextPageUrl.indexOf('?'), 37 | nextPageUrl.length 38 | ); 39 | const urlParams = new URLSearchParams(queryString); 40 | const nextPage = urlParams.get('_page'); 41 | return nextPage; 42 | } else { 43 | return undefined; 44 | } 45 | }, 46 | }); 47 | } 48 | -------------------------------------------------------------------------------- /src/api/users/use-paginated-users.ts: -------------------------------------------------------------------------------- 1 | import { apiClient, userQueryKeys } from '@/api'; 2 | import { useQuery } from '@tanstack/react-query'; 3 | 4 | type Props = { 5 | page: number; 6 | pageLimit: number; 7 | }; 8 | 9 | export function usePaginatedUsers({ page, pageLimit }: Props) { 10 | const getPaginatedUsersFn = async (p = page) => { 11 | const response = await apiClient.get(`?_page=${p}&_limit=${pageLimit}`); 12 | return response.data; 13 | }; 14 | 15 | return useQuery( 16 | userQueryKeys.pagination(page), 17 | () => getPaginatedUsersFn(page), 18 | { 19 | keepPreviousData: true, 20 | } 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /src/api/users/use-user.ts: -------------------------------------------------------------------------------- 1 | import { useParams } from 'react-router-dom'; 2 | import { apiClient, userQueryKeys } from '@/api'; 3 | import { useQuery } from '@tanstack/react-query'; 4 | 5 | export function useUser() { 6 | const { id } = useParams(); 7 | 8 | const getUserFn = async () => { 9 | const response = await apiClient.get(`${id}`); 10 | return response.data; 11 | }; 12 | 13 | return useQuery({ 14 | queryKey: userQueryKeys.detail(Number(id)), 15 | queryFn: getUserFn, 16 | retry: 1, 17 | }); 18 | } 19 | -------------------------------------------------------------------------------- /src/api/users/use-users.ts: -------------------------------------------------------------------------------- 1 | import { apiClient, userQueryKeys } from '@/api'; 2 | import { useQuery } from '@tanstack/react-query'; 3 | 4 | const getUsersFn = async () => { 5 | const response = await apiClient.get(''); 6 | return response.data; 7 | }; 8 | 9 | export function useUsers() { 10 | return useQuery({ 11 | queryKey: userQueryKeys.all, 12 | queryFn: getUsersFn, 13 | }); 14 | } 15 | -------------------------------------------------------------------------------- /src/api/users/user-query-keys.ts: -------------------------------------------------------------------------------- 1 | // Effective React Query Keys 2 | // https://tkdodo.eu/blog/effective-react-query-keys#use-query-key-factories 3 | // https://tkdodo.eu/blog/leveraging-the-query-function-context#query-key-factories 4 | 5 | export const userQueryKeys = { 6 | all: ['users'], 7 | details: () => [...userQueryKeys.all, 'detail'], 8 | detail: (id: number) => [...userQueryKeys.details(), id], 9 | pagination: (page: number) => [...userQueryKeys.all, 'pagination', page], 10 | infinite: () => [...userQueryKeys.all, 'infinite'], 11 | }; 12 | -------------------------------------------------------------------------------- /src/app.tsx: -------------------------------------------------------------------------------- 1 | import { AppProvider } from './providers/app-provider'; 2 | 3 | function App() { 4 | return ( 5 |
6 | 7 |
8 | ); 9 | } 10 | 11 | export default App; 12 | -------------------------------------------------------------------------------- /src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/app-layout.tsx: -------------------------------------------------------------------------------- 1 | import { Link, NavLink, Outlet } from 'react-router-dom'; 2 | 3 | export function AppLayout() { 4 | return ( 5 | <> 6 |
7 | 32 |
33 | 34 | 35 | 36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /src/components/delete-modal.tsx: -------------------------------------------------------------------------------- 1 | import { Dialog, Transition } from '@headlessui/react'; 2 | import { Fragment } from 'react'; 3 | 4 | // https://tailwindui.com/components/application-ui/overlays/modals 5 | // https://headlessui.dev/react/dialog 6 | 7 | type ModalProps = { 8 | id: number; 9 | isModalOpen: boolean; 10 | cancelAction: () => void; 11 | deleteAction: (id: number) => Promise; 12 | isLoading: boolean; 13 | }; 14 | 15 | export function DeleteModal(props: ModalProps) { 16 | const { id, isModalOpen, cancelAction, deleteAction, isLoading } = props; 17 | 18 | return ( 19 | 20 | 25 |
26 | 35 | 36 | 37 | 38 | {/* This element is to trick the browser into centering the modal contents. */} 39 | 45 | 54 |
55 |
56 |
57 |
58 | {/*
63 |
64 | 68 | Delete Confirmation 69 | 70 |
71 |

72 | Are you sure you want to delete user {id}? This action 73 | cannot be undone. 74 |

75 |
76 |
77 |
78 |
79 |
80 | 87 | 94 |
95 | 96 | {isLoading && ( 97 |
Deleting...
98 | )} 99 |
100 |
101 |
102 |
103 |
104 | ); 105 | } 106 | -------------------------------------------------------------------------------- /src/components/hello-world.tsx: -------------------------------------------------------------------------------- 1 | export function HelloWorld() { 2 | return
HelloWorld 2
; 3 | } 4 | -------------------------------------------------------------------------------- /src/components/index.tsx: -------------------------------------------------------------------------------- 1 | export * from './app-layout'; 2 | export * from './hello-world'; 3 | export * from './user-table'; 4 | export * from './user-form'; 5 | export * from './delete-modal'; 6 | -------------------------------------------------------------------------------- /src/components/ui/index.tsx: -------------------------------------------------------------------------------- 1 | export * from './toast/toast'; 2 | export * from './toast/use-toast'; 3 | -------------------------------------------------------------------------------- /src/components/ui/toast/toast.tsx: -------------------------------------------------------------------------------- 1 | // https://ui.shadcn.com/docs/components/toast 2 | 3 | import React, { forwardRef } from 'react'; 4 | import * as ToastPrimitives from '@radix-ui/react-toast'; 5 | import { cva, type VariantProps } from 'class-variance-authority'; 6 | import { X } from 'lucide-react'; 7 | import { cn } from '@/utils'; 8 | 9 | const ToastProvider = ToastPrimitives.Provider; 10 | 11 | const ToastViewport = forwardRef< 12 | React.ElementRef, 13 | React.ComponentPropsWithoutRef 14 | >(({ className, ...props }, ref) => ( 15 | 23 | )); 24 | ToastViewport.displayName = ToastPrimitives.Viewport.displayName; 25 | 26 | const toastVariants = cva( 27 | 'group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full', 28 | { 29 | variants: { 30 | variant: { 31 | default: 'border bg-gray-100 text-foreground', 32 | destructive: 33 | 'destructive group border-destructive bg-destructive text-destructive-foreground', 34 | }, 35 | }, 36 | defaultVariants: { 37 | variant: 'default', 38 | }, 39 | } 40 | ); 41 | 42 | const Toast = forwardRef< 43 | React.ElementRef, 44 | React.ComponentPropsWithoutRef & 45 | VariantProps 46 | >(({ className, variant, ...props }, ref) => { 47 | return ( 48 | 53 | ); 54 | }); 55 | Toast.displayName = ToastPrimitives.Root.displayName; 56 | 57 | const ToastAction = forwardRef< 58 | React.ElementRef, 59 | React.ComponentPropsWithoutRef 60 | >(({ className, ...props }, ref) => ( 61 | 69 | )); 70 | ToastAction.displayName = ToastPrimitives.Action.displayName; 71 | 72 | const ToastClose = forwardRef< 73 | React.ElementRef, 74 | React.ComponentPropsWithoutRef 75 | >(({ className, ...props }, ref) => ( 76 | 85 | 86 | 87 | )); 88 | ToastClose.displayName = ToastPrimitives.Close.displayName; 89 | 90 | const ToastTitle = forwardRef< 91 | React.ElementRef, 92 | React.ComponentPropsWithoutRef 93 | >(({ className, ...props }, ref) => ( 94 | 99 | )); 100 | ToastTitle.displayName = ToastPrimitives.Title.displayName; 101 | 102 | const ToastDescription = forwardRef< 103 | React.ElementRef, 104 | React.ComponentPropsWithoutRef 105 | >(({ className, ...props }, ref) => ( 106 | 111 | )); 112 | ToastDescription.displayName = ToastPrimitives.Description.displayName; 113 | 114 | type ToastProps = React.ComponentPropsWithoutRef; 115 | 116 | type ToastActionElement = React.ReactElement; 117 | 118 | export { 119 | type ToastProps, 120 | type ToastActionElement, 121 | ToastProvider, 122 | ToastViewport, 123 | Toast, 124 | ToastTitle, 125 | ToastDescription, 126 | ToastClose, 127 | ToastAction, 128 | }; 129 | -------------------------------------------------------------------------------- /src/components/ui/toast/use-toast.tsx: -------------------------------------------------------------------------------- 1 | // Inspired by react-hot-toast library 2 | import React from 'react'; 3 | import type { ToastActionElement, ToastProps } from '@/components/ui'; 4 | 5 | const TOAST_LIMIT = 1; 6 | const TOAST_REMOVE_DELAY = 1000000; 7 | 8 | type ToasterToast = ToastProps & { 9 | id: string; 10 | title?: React.ReactNode; 11 | description?: React.ReactNode; 12 | action?: ToastActionElement; 13 | }; 14 | 15 | const actionTypes = { 16 | ADD_TOAST: 'ADD_TOAST', 17 | UPDATE_TOAST: 'UPDATE_TOAST', 18 | DISMISS_TOAST: 'DISMISS_TOAST', 19 | REMOVE_TOAST: 'REMOVE_TOAST', 20 | } as const; 21 | 22 | let count = 0; 23 | 24 | function genId() { 25 | count = (count + 1) % Number.MAX_VALUE; 26 | return count.toString(); 27 | } 28 | 29 | type ActionType = typeof actionTypes; 30 | 31 | type Action = 32 | | { 33 | type: ActionType['ADD_TOAST']; 34 | toast: ToasterToast; 35 | } 36 | | { 37 | type: ActionType['UPDATE_TOAST']; 38 | toast: Partial; 39 | } 40 | | { 41 | type: ActionType['DISMISS_TOAST']; 42 | toastId?: ToasterToast['id']; 43 | } 44 | | { 45 | type: ActionType['REMOVE_TOAST']; 46 | toastId?: ToasterToast['id']; 47 | }; 48 | 49 | interface State { 50 | toasts: ToasterToast[]; 51 | } 52 | 53 | const toastTimeouts = new Map>(); 54 | 55 | const addToRemoveQueue = (toastId: string) => { 56 | if (toastTimeouts.has(toastId)) { 57 | return; 58 | } 59 | 60 | const timeout = setTimeout(() => { 61 | toastTimeouts.delete(toastId); 62 | dispatch({ 63 | type: 'REMOVE_TOAST', 64 | toastId: toastId, 65 | }); 66 | }, TOAST_REMOVE_DELAY); 67 | 68 | toastTimeouts.set(toastId, timeout); 69 | }; 70 | 71 | export const reducer = (state: State, action: Action): State => { 72 | switch (action.type) { 73 | case 'ADD_TOAST': 74 | return { 75 | ...state, 76 | toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT), 77 | }; 78 | 79 | case 'UPDATE_TOAST': 80 | return { 81 | ...state, 82 | toasts: state.toasts.map((t) => 83 | t.id === action.toast.id ? { ...t, ...action.toast } : t 84 | ), 85 | }; 86 | 87 | case 'DISMISS_TOAST': { 88 | const { toastId } = action; 89 | 90 | // ! Side effects ! - This could be extracted into a dismissToast() action, 91 | // but I'll keep it here for simplicity 92 | if (toastId) { 93 | addToRemoveQueue(toastId); 94 | } else { 95 | state.toasts.forEach((toast) => { 96 | addToRemoveQueue(toast.id); 97 | }); 98 | } 99 | 100 | return { 101 | ...state, 102 | toasts: state.toasts.map((t) => 103 | t.id === toastId || toastId === undefined 104 | ? { 105 | ...t, 106 | open: false, 107 | } 108 | : t 109 | ), 110 | }; 111 | } 112 | case 'REMOVE_TOAST': 113 | if (action.toastId === undefined) { 114 | return { 115 | ...state, 116 | toasts: [], 117 | }; 118 | } 119 | return { 120 | ...state, 121 | toasts: state.toasts.filter((t) => t.id !== action.toastId), 122 | }; 123 | } 124 | }; 125 | 126 | const listeners: Array<(state: State) => void> = []; 127 | 128 | let memoryState: State = { toasts: [] }; 129 | 130 | function dispatch(action: Action) { 131 | memoryState = reducer(memoryState, action); 132 | listeners.forEach((listener) => { 133 | listener(memoryState); 134 | }); 135 | } 136 | 137 | type Toast = Omit; 138 | 139 | function toast({ ...props }: Toast) { 140 | const id = genId(); 141 | 142 | const update = (props: ToasterToast) => 143 | dispatch({ 144 | type: 'UPDATE_TOAST', 145 | toast: { ...props, id }, 146 | }); 147 | 148 | const dismiss = () => dispatch({ type: 'DISMISS_TOAST', toastId: id }); 149 | 150 | dispatch({ 151 | type: 'ADD_TOAST', 152 | toast: { 153 | ...props, 154 | id, 155 | open: true, 156 | onOpenChange: (open) => { 157 | if (!open) dismiss(); 158 | }, 159 | }, 160 | }); 161 | 162 | return { 163 | id: id, 164 | dismiss, 165 | update, 166 | }; 167 | } 168 | 169 | function useToast() { 170 | const [state, setState] = React.useState(memoryState); 171 | 172 | React.useEffect(() => { 173 | listeners.push(setState); 174 | return () => { 175 | const index = listeners.indexOf(setState); 176 | if (index > -1) { 177 | listeners.splice(index, 1); 178 | } 179 | }; 180 | }, [state]); 181 | 182 | return { 183 | ...state, 184 | toast, 185 | dismiss: (toastId?: string) => dispatch({ type: 'DISMISS_TOAST', toastId }), 186 | }; 187 | } 188 | 189 | export { useToast, toast }; 190 | -------------------------------------------------------------------------------- /src/components/user-form/index.tsx: -------------------------------------------------------------------------------- 1 | import { useForm } from 'react-hook-form'; 2 | import { useNavigate } from 'react-router-dom'; 3 | import './user-form.css'; 4 | 5 | export function UserForm({ user, submitText, submitAction }: any) { 6 | const { 7 | register, 8 | formState: { errors }, 9 | handleSubmit, 10 | } = useForm({ 11 | defaultValues: user || {}, 12 | // https://react-hook-form.com/api/useform, see defaultValues: Record = {} 13 | }); 14 | 15 | const navigate = useNavigate(); 16 | const back = () => navigate('/'); 17 | 18 | return ( 19 |
20 |
21 | {user && ( 22 |
23 | 24 | 25 |
26 | )} 27 | 28 |
29 |
30 | 31 | 35 | 36 | {errors.first_name && 'First name is required'} 37 | 38 |
39 |
40 | 41 | 42 | 43 | {errors.last_name && 'Last name is required'} 44 | 45 |
46 |
47 | 48 |
49 | 50 | 54 | 55 | {errors.email && 56 | errors.email.type === 'required' && 57 | 'Email is required'} 58 | {errors.email && 59 | errors.email.type === 'pattern' && 60 | 'Provide a valid email address'} 61 | 62 |
63 | 64 |
65 | 66 | 71 | 72 | {errors.gender && 'Gender is required'} 73 | 74 |
75 | 76 |
77 | 83 | 90 |
91 |
92 |
93 | ); 94 | } 95 | -------------------------------------------------------------------------------- /src/components/user-form/user-form.css: -------------------------------------------------------------------------------- 1 | .field { 2 | @apply my-2; 3 | } 4 | 5 | label { 6 | @apply block text-sm font-semibold text-gray-700; 7 | } 8 | 9 | input[type='text'], 10 | input[type='email'], 11 | select, 12 | textarea { 13 | @apply w-full px-2 py-1 mt-1 border border-gray-300 rounded-md shadow-sm focus:ring-cyan-500 focus:border-cyan-500 focus:outline-none; 14 | } 15 | 16 | input[type='checkbox'] { 17 | @apply w-4 h-4 border-gray-300 rounded focus:ring-cyan-500 text-cyan-600; 18 | } 19 | 20 | input:disabled { 21 | @apply bg-gray-200 text-gray-500; 22 | } 23 | 24 | .errors { 25 | @apply text-sm text-red-500; 26 | } 27 | -------------------------------------------------------------------------------- /src/components/user-table/index.tsx: -------------------------------------------------------------------------------- 1 | import { useDeleteUser } from '@/api'; 2 | import { DeleteModal } from '@/components'; 3 | import { AiOutlineDelete, AiOutlineEdit } from '@/icons'; 4 | import { User } from '@/types'; 5 | import { useState } from 'react'; 6 | import { Link } from 'react-router-dom'; 7 | import './user-table.css'; 8 | 9 | type Props = { 10 | users: User[]; 11 | }; 12 | 13 | export function UserTable({ users }: Props) { 14 | const [deleteId, setDeleteId] = useState(0); 15 | const [isModalOpen, setIsModalOpen] = useState(false); 16 | 17 | function closeModal() { 18 | setIsModalOpen(false); 19 | } 20 | 21 | function showDeleteModal(id: number) { 22 | setDeleteId(id); 23 | setIsModalOpen(true); 24 | } 25 | 26 | const deleteMutation = useDeleteUser({ closeModal }); 27 | 28 | const handleDelete = async (id: number) => { 29 | deleteMutation.mutateAsync(id); 30 | }; 31 | 32 | return ( 33 | <> 34 | 41 | 42 |
43 | 47 | Create User 48 | 49 |
50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | {users && 63 | users.map((user: User) => ( 64 | 68 | 69 | 70 | 71 | 72 | 73 | 87 | 88 | ))} 89 | 90 |
IdFirst NameLast NameEmailGenderAction
{user.id}{user.first_name}{user.last_name}{user.email}{user.gender} 74 | 78 | 79 | 80 | 86 |
91 | 92 | ); 93 | } 94 | -------------------------------------------------------------------------------- /src/components/user-table/user-table.css: -------------------------------------------------------------------------------- 1 | th { 2 | @apply p-2; 3 | } 4 | 5 | tbody td { 6 | @apply border border-cyan-800 px-4; 7 | } 8 | 9 | tbody tr:nth-child(even) { 10 | @apply bg-gray-50; 11 | } 12 | -------------------------------------------------------------------------------- /src/icons/close.tsx: -------------------------------------------------------------------------------- 1 | import { ComponentPropsWithoutRef } from 'react'; 2 | 3 | export function AiOutlineCloseCircle(props: ComponentPropsWithoutRef<'svg'>) { 4 | return ( 5 | 14 | 15 | 16 | 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /src/icons/delete.tsx: -------------------------------------------------------------------------------- 1 | import { ComponentPropsWithoutRef } from 'react'; 2 | 3 | export function AiOutlineDelete(props: ComponentPropsWithoutRef<'svg'>) { 4 | return ( 5 | 14 | 15 | 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /src/icons/edit.tsx: -------------------------------------------------------------------------------- 1 | import { ComponentPropsWithoutRef } from 'react'; 2 | 3 | export function AiOutlineEdit(props: ComponentPropsWithoutRef<'svg'>) { 4 | return ( 5 | 14 | 15 | 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /src/icons/exclamation.tsx: -------------------------------------------------------------------------------- 1 | import { ComponentPropsWithoutRef } from 'react'; 2 | 3 | export function BsExclamationTriangle(props: ComponentPropsWithoutRef<'svg'>) { 4 | return ( 5 | 14 | 19 | 20 | 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /src/icons/index.tsx: -------------------------------------------------------------------------------- 1 | export * from './close'; 2 | export * from './delete'; 3 | export * from './edit'; 4 | export * from './exclamation'; 5 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | :root { 6 | font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; 7 | font-synthesis: none; 8 | text-rendering: optimizeLegibility; 9 | -webkit-text-size-adjust: 100%; 10 | 11 | @apply font-normal text-base antialiased bg-gray-100; 12 | } 13 | -------------------------------------------------------------------------------- /src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import App from './app.tsx'; 4 | import './index.css'; 5 | 6 | ReactDOM.createRoot(document.getElementById('root')!).render( 7 | 8 | 9 | 10 | ); 11 | -------------------------------------------------------------------------------- /src/preview.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nguyenhieptech/react-query-example/150627eb6463cecd4b18ca903c8ec840739400e3/src/preview.PNG -------------------------------------------------------------------------------- /src/providers/app-provider.tsx: -------------------------------------------------------------------------------- 1 | import { RoutingProvider } from './routing-provider'; 2 | import { ReactQueryProvider } from './react-query-provider'; 3 | import { ToasterProvider } from './toaster-provider'; 4 | 5 | export function AppProvider() { 6 | return ( 7 | 8 | 9 | 10 | 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /src/providers/react-query-provider.tsx: -------------------------------------------------------------------------------- 1 | // https://tanstack.com/query/latest/docs/react/quick-start 2 | import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; 3 | // https://tanstack.com/query/latest/docs/react/devtools#floating-mode 4 | import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; 5 | import { PropsWithChildren } from 'react'; 6 | 7 | const queryClient = new QueryClient(); 8 | 9 | export function ReactQueryProvider({ children }: PropsWithChildren) { 10 | return ( 11 | 12 | {children} 13 | 14 | 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /src/providers/routing-provider.tsx: -------------------------------------------------------------------------------- 1 | import { AppLayout } from '@/components'; 2 | import { 3 | CreateUser, 4 | EditUser, 5 | InfiniteUsers, 6 | PaginatedUsers, 7 | Users, 8 | } from '@/views'; 9 | import { 10 | Route, 11 | RouterProvider, 12 | createBrowserRouter, 13 | createRoutesFromElements, 14 | } from 'react-router-dom'; 15 | 16 | const router = createBrowserRouter( 17 | createRoutesFromElements( 18 | }> 19 | } /> 20 | } /> 21 | } /> 22 | } /> 23 | } /> 24 | 25 | ) 26 | ); 27 | 28 | export function RoutingProvider() { 29 | return ; 30 | } 31 | -------------------------------------------------------------------------------- /src/providers/toaster-provider.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Toast, 3 | ToastClose, 4 | ToastDescription, 5 | ToastProvider, 6 | ToastTitle, 7 | ToastViewport, 8 | useToast, 9 | } from '@/components/ui'; 10 | 11 | export function ToasterProvider() { 12 | const { toasts } = useToast(); 13 | 14 | return ( 15 | 16 | {toasts.map(function ({ id, title, description, action, ...props }) { 17 | return ( 18 | 19 |
20 | {title && {title}} 21 | {description && ( 22 | {description} 23 | )} 24 |
25 | {action} 26 | 27 |
28 | ); 29 | })} 30 | 31 |
32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | export type User = Partial<{ 2 | id: number; 3 | first_name: string; 4 | last_name: string; 5 | email: string; 6 | gender: string; 7 | }>; 8 | 9 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 10 | export type TSFixMe = any; 11 | -------------------------------------------------------------------------------- /src/utils/cn.ts: -------------------------------------------------------------------------------- 1 | import { clsx, type ClassValue } from 'clsx'; 2 | import { twMerge } from 'tailwind-merge'; 3 | 4 | /** @see https://github.com/shadcn/ui/blob/main/apps/www/lib/utils.ts#L1-L6 */ 5 | export function cn(...inputs: ClassValue[]) { 6 | return twMerge(clsx(inputs)); 7 | } 8 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './cn'; 2 | -------------------------------------------------------------------------------- /src/views/create-user.tsx: -------------------------------------------------------------------------------- 1 | import { Navigate } from 'react-router-dom'; 2 | import { UserForm } from '@/components'; 3 | import { useCreateUser } from '@/api'; 4 | import { User } from '@/types'; 5 | 6 | export function CreateUser() { 7 | const createUser = useCreateUser(); 8 | 9 | const onSubmit = async (data: User) => createUser.mutate(data); 10 | 11 | // https://reactrouter.com/docs/en/v6/upgrading/v5#use-usenavigate-instead-of-usehistory 12 | if (createUser.isSuccess) { 13 | return ; 14 | } 15 | 16 | return ( 17 |
18 |

New User

19 | 20 | {createUser.isLoading &&
Loading...
} 21 | 22 | {createUser.error instanceof Error && ( 23 |
An error occurred: {createUser.error.message}
24 | )} 25 | 26 | 27 |
28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /src/views/edit-user.tsx: -------------------------------------------------------------------------------- 1 | import { Navigate } from 'react-router-dom'; 2 | import { UserForm } from '@/components'; 3 | import { useEditUser, useUser } from '@/api'; 4 | import { User } from '@/types'; 5 | 6 | export function EditUser() { 7 | const getUser = useUser(); 8 | const editUser = useEditUser(); 9 | 10 | const onSubmit = async (data: User) => editUser.mutate(data); 11 | 12 | // https://reactrouter.com/docs/en/v6/upgrading/v5#use-usenavigate-instead-of-usehistory 13 | if (editUser.isSuccess) { 14 | return ; 15 | } 16 | 17 | return ( 18 |
19 |

Edit User

20 |
21 | {getUser.isLoading &&
Loading...
} 22 | 23 | {getUser.error instanceof Error &&
{getUser.error.message}
} 24 | 25 | {getUser.data && ( 26 | 31 | )} 32 |
33 |
34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /src/views/index.tsx: -------------------------------------------------------------------------------- 1 | export * from './create-user'; 2 | export * from './edit-user'; 3 | export * from './infinite-users'; 4 | export * from './paginated-users'; 5 | export * from './users'; 6 | -------------------------------------------------------------------------------- /src/views/infinite-users.tsx: -------------------------------------------------------------------------------- 1 | import { Fragment } from 'react'; 2 | import { useInfiniteUsers } from '@/api'; 3 | import { User } from '@/types'; 4 | 5 | export function InfiniteUsers() { 6 | const pageLimit = 10; 7 | const infiniteUsers = useInfiniteUsers({ pageLimit }); 8 | let userList; 9 | 10 | if (infiniteUsers.data) { 11 | userList = infiniteUsers.data.pages.map((page, index) => ( 12 | 13 | {page.data.map((user: User) => ( 14 |
  • 15 | {user.id}. {user.first_name} {user.last_name} 16 |
  • 17 | ))} 18 |
    19 | )); 20 | } 21 | 22 | return ( 23 |
    24 |

    Infinite Users

    25 |
    26 | {infiniteUsers.error instanceof Error && ( 27 |
    An error occurred: {infiniteUsers.error.message}
    28 | )} 29 | 30 | {infiniteUsers.isFetchingNextPage &&
    Fetching Next Page...
    } 31 | 32 | {infiniteUsers.isSuccess &&
      {userList}
    } 33 |
    34 |
    35 | 44 |
    45 |
    46 | ); 47 | } 48 | -------------------------------------------------------------------------------- /src/views/paginated-users.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import { UserTable } from '@/components'; 3 | import { usePaginatedUsers } from '@/api'; 4 | 5 | export function PaginatedUsers() { 6 | const [page, setPage] = useState(1); 7 | const pageLimit = 15; 8 | const paginatedUsers = usePaginatedUsers({ 9 | page, 10 | pageLimit, 11 | }); 12 | 13 | const prevPage = () => { 14 | if (page > 1) setPage(page - 1); 15 | }; 16 | 17 | const nextPage = () => { 18 | setPage(page + 1); 19 | }; 20 | 21 | return ( 22 |
    23 |

    Paginated Users

    24 |
    25 | {paginatedUsers.error instanceof Error && ( 26 |
    {paginatedUsers.error.message}
    27 | )} 28 | 29 | {paginatedUsers.isLoading &&
    Loading...
    } 30 | 31 | {paginatedUsers.isSuccess && } 32 |
    33 |
    34 | 41 | 42 | Page: {page} 43 | 44 | 53 |
    54 |
    55 | ); 56 | } 57 | -------------------------------------------------------------------------------- /src/views/users.tsx: -------------------------------------------------------------------------------- 1 | import { UserTable } from '@/components'; 2 | import { useUsers } from '@/api'; 3 | 4 | export function Users() { 5 | const users = useUsers(); 6 | 7 | return ( 8 |
    9 |

    Basic Query Example

    10 |
    11 | {users.isLoading && ( 12 |
    Loading...
    13 | )} 14 | 15 | {users.isFetching && ( 16 |
    Fetching...
    17 | )} 18 | 19 | {users.error instanceof Error &&
    {users.error.message}
    } 20 | 21 | {users.isSuccess && ( 22 |
    23 | 24 |
    25 | )} 26 |
    27 |
    28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | export default { 3 | content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'], 4 | theme: { 5 | extend: {}, 6 | }, 7 | plugins: [], 8 | }; 9 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "noEmit": true, 15 | "jsx": "react-jsx", 16 | 17 | /* Linting */ 18 | "strict": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "noFallthroughCasesInSwitch": true, 22 | 23 | /* Absolute Imports */ 24 | "baseUrl": ".", 25 | "paths": { 26 | "@/*": ["src/*"] 27 | } 28 | }, 29 | "include": ["src"], 30 | "references": [{ "path": "./tsconfig.node.json" }] 31 | } 32 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": ["vite.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import react from '@vitejs/plugin-react'; 3 | import path from 'path'; 4 | 5 | // https://vitejs.dev/config/ 6 | export default defineConfig({ 7 | plugins: [react()], 8 | resolve: { 9 | alias: { 10 | '@': path.resolve(__dirname, './src'), 11 | }, 12 | }, 13 | server: { 14 | port: 5555, 15 | }, 16 | }); 17 | --------------------------------------------------------------------------------