├── .eslintrc.cjs
├── .gitignore
├── README.md
├── index.html
├── package-lock.json
├── package.json
├── public
└── vite.svg
├── setupTests.js
├── src
├── App.jsx
├── __mocks__
│ └── msw
│ │ ├── handlers.js
│ │ └── server.js
├── __tests__
│ ├── App.test.jsx
│ ├── AppWithRouter.test.jsx
│ ├── PostContainer.test.jsx
│ ├── TestInputField.test.jsx
│ ├── UsernameDisplay.test.jsx
│ ├── __snapshots__
│ │ └── PostContainer.test.jsx.snap
│ └── utils
│ │ └── helpers.jsx
├── components
│ ├── LoginForm.jsx
│ ├── PostContainer.jsx
│ ├── PostContent.jsx
│ ├── PostContentButtons.jsx
│ ├── RegisterForm.jsx
│ ├── TestInputField.jsx
│ ├── UserDetails.jsx
│ └── UsernameDisplay.jsx
├── globals.css
├── main.jsx
├── pages
│ ├── BlogPostsPage.jsx
│ └── UsersPage.jsx
└── utils
│ ├── constants.jsx
│ ├── contexts
│ └── UserContext.js
│ └── hooks
│ ├── useDocumentClick.js
│ └── useFetchUser.js
└── vite.config.js
/.eslintrc.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | env: { browser: true, es2020: true },
4 | extends: [
5 | 'eslint:recommended',
6 | 'plugin:react/recommended',
7 | 'plugin:react/jsx-runtime',
8 | 'plugin:react-hooks/recommended',
9 | ],
10 | ignorePatterns: ['dist', '.eslintrc.cjs'],
11 | parserOptions: { ecmaVersion: 'latest', sourceType: 'module' },
12 | settings: { react: { version: '18.2' } },
13 | plugins: ['react-refresh'],
14 | rules: {
15 | 'react/jsx-no-target-blank': 'off',
16 | 'react-refresh/only-export-components': [
17 | 'warn',
18 | { allowConstantExport: true },
19 | ],
20 | },
21 | }
22 |
--------------------------------------------------------------------------------
/.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 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # React + Vite
2 |
3 | This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
4 |
5 | Currently, two official plugins are available:
6 |
7 | - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
8 | - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
9 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Vite + React
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-tutorial",
3 | "private": true,
4 | "version": "0.0.0",
5 | "type": "module",
6 | "scripts": {
7 | "dev": "vite",
8 | "build": "vite build",
9 | "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0",
10 | "preview": "vite preview",
11 | "test": "vitest"
12 | },
13 | "dependencies": {
14 | "prop-types": "^15.8.1",
15 | "react": "^18.2.0",
16 | "react-dom": "^18.2.0",
17 | "react-router-dom": "^6.22.3"
18 | },
19 | "devDependencies": {
20 | "@testing-library/jest-dom": "^6.4.2",
21 | "@testing-library/react": "^14.2.2",
22 | "@testing-library/user-event": "^14.5.2",
23 | "@types/react": "^18.2.56",
24 | "@types/react-dom": "^18.2.19",
25 | "@vitejs/plugin-react": "^4.2.1",
26 | "eslint": "^8.56.0",
27 | "eslint-plugin-react": "^7.33.2",
28 | "eslint-plugin-react-hooks": "^4.6.0",
29 | "eslint-plugin-react-refresh": "^0.4.5",
30 | "jest": "^29.7.0",
31 | "jsdom": "^24.0.0",
32 | "msw": "^2.2.13",
33 | "sass": "^1.71.1",
34 | "vite": "^5.1.4",
35 | "vitest": "^1.4.0"
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/public/vite.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/setupTests.js:
--------------------------------------------------------------------------------
1 | import { afterEach, beforeAll, afterAll } from "vitest";
2 | import { cleanup, configure } from "@testing-library/react";
3 | import { server } from "./src/__mocks__/msw/server";
4 | import "@testing-library/jest-dom/vitest";
5 |
6 | configure({ asyncUtilTimeout: 5000 });
7 |
8 | beforeAll(() => {
9 | server.listen();
10 | });
11 |
12 | afterEach(() => {
13 | cleanup();
14 | server.resetHandlers();
15 | });
16 |
17 | afterAll(() => {
18 | server.close();
19 | });
20 |
--------------------------------------------------------------------------------
/src/App.jsx:
--------------------------------------------------------------------------------
1 | import { Suspense, useEffect, useState } from "react";
2 | import { PostContainer } from "./components/PostContainer";
3 | import { UserDetails } from "./components/UserDetails";
4 | import { UserContext } from "./utils/contexts/UserContext";
5 | import { useFetchUser } from "./utils/hooks/useFetchUser";
6 | import { Outlet, Link, useNavigate } from "react-router-dom";
7 | import { TestInputField } from "./components/TestInputField";
8 |
9 | export default function App({ usersData }) {
10 | const { user, loading, error } = useFetchUser(2);
11 | const [userData, setUserData] = useState();
12 | // const navigate = useNavigate();
13 |
14 | const [users, setUsers] = useState(usersData);
15 |
16 | useEffect(() => {
17 | if (!loading && !error && user) {
18 | setUserData(user);
19 | }
20 | }, [loading, error, user]);
21 |
22 | return (
23 | Loading...}>
24 |
25 | {users.map((user) => (
26 |
27 | ))}
28 |
41 | {/*
42 |
43 | {
47 | if (e.target.value.length > 10) {
48 | navigate("/blog-posts", {
49 | state: {
50 | posts: [
51 | {
52 | id: 1,
53 | title: "hello world",
54 | content: "welcome to my first post",
55 | },
56 | ],
57 | },
58 | });
59 | }
60 | }}
61 | />
62 |
*/}
63 |
64 |
65 |
66 |
67 |
68 |
69 | );
70 | }
71 |
--------------------------------------------------------------------------------
/src/__mocks__/msw/handlers.js:
--------------------------------------------------------------------------------
1 | import { http } from "msw";
2 |
3 | export const handlers = [
4 | http.get("https://jsonplaceholder.typicode.com/users/*", ({ params }) => {
5 | return Response.json({
6 | id: params.id,
7 | username: "josh",
8 | name: "josh",
9 | email: "josh@josh.com",
10 | });
11 | }),
12 | ];
13 |
--------------------------------------------------------------------------------
/src/__mocks__/msw/server.js:
--------------------------------------------------------------------------------
1 | import { setupServer } from 'msw/node';
2 | import { handlers } from './handlers';
3 |
4 | export const server = setupServer(...handlers)
--------------------------------------------------------------------------------
/src/__tests__/App.test.jsx:
--------------------------------------------------------------------------------
1 | import { expect, it, describe } from "vitest";
2 | import { render, screen, waitFor } from "@testing-library/react";
3 | import userEvent from "@testing-library/user-event";
4 | import { within } from "@testing-library/dom";
5 | import App from "../App";
6 | import { server } from "../__mocks__/msw/server";
7 | import { http } from "msw";
8 |
9 | describe("when there is only 1 user", () => {
10 | describe("Edit Button is Clicked", () => {
11 | it("should render save button", async () => {
12 | render(
13 |
22 | );
23 | const editButton = screen.getByRole("button", { name: "Edit" });
24 | await userEvent.click(editButton);
25 | const saveButton = screen.getByRole("button", { name: "Save" });
26 | expect(saveButton).toBeInTheDocument();
27 | });
28 | it("should display username & email input fields", async () => {
29 | render(
30 |
39 | );
40 | const editButton = screen.getByRole("button", { name: "Edit" });
41 | await userEvent.click(editButton);
42 | expect(screen.getByLabelText("Username:")).toBeInTheDocument();
43 | expect(screen.getByLabelText("Email:")).toBeInTheDocument();
44 | });
45 | });
46 | });
47 |
48 | describe("when there are 2 users", () => {
49 | it("should have two users", () => {
50 | render(
51 |
65 | );
66 |
67 | expect(screen.getByText("ansonthedev")).toBeInTheDocument();
68 | expect(screen.getByText("michael")).toBeInTheDocument();
69 | });
70 |
71 | it("should click edit button for 1st user and display save button", async () => {
72 | render(
73 |
87 | );
88 | const userDetails = screen.getByTestId("user-details-1");
89 | expect(within(userDetails).queryByText("michael")).toBeNull();
90 | const editBtn = within(userDetails).getByRole("button", { name: "Edit" });
91 | await userEvent.click(editBtn);
92 | expect(
93 | within(userDetails).getByRole("button", { name: "Save" })
94 | ).toBeInTheDocument();
95 | });
96 |
97 | it("should edit 2nd username and save", async () => {
98 | render(
99 |
113 | );
114 | const userDetails = screen.getByTestId("user-details-2");
115 | await userEvent.click(
116 | within(userDetails).getByRole("button", { name: "Edit" })
117 | );
118 | await userEvent.type(
119 | within(userDetails).getByLabelText("Username:"),
120 | "123"
121 | );
122 | await userEvent.click(
123 | within(userDetails).getByRole("button", { name: "Save" })
124 | );
125 | expect(within(userDetails).queryByLabelText("Username:")).toBeNull();
126 | expect(within(userDetails).getByText("michael123")).toBeInTheDocument();
127 | });
128 | });
129 |
130 | describe("rendering context data", () => {
131 | it("should render correct Email", async () => {
132 | server.use(
133 | http.get(
134 | "https://jsonplaceholder.typicode.com/users/*",
135 | async ({ params }) => {
136 | return Response.json({
137 | id: params.id,
138 | username: "joshua",
139 | name: "joshua",
140 | email: "joshua@yahoo.com",
141 | });
142 | }
143 | )
144 | );
145 |
146 | render();
147 | await waitFor(async () =>
148 | expect(
149 | await screen.findByText("Email: joshua@yahoo.com")
150 | ).toBeInTheDocument()
151 | );
152 | });
153 | });
154 | describe("updating UserContext", () => {
155 | it("should update display name", async () => {
156 | render();
157 | await userEvent.type(
158 | screen.getByLabelText("Update Name:"),
159 | "Jonathan The Dev"
160 | );
161 | await userEvent.click(
162 | screen.getByRole("button", { name: "Save Display Name" })
163 | );
164 | expect(
165 | screen.getByText("Display Name: Jonathan The Dev")
166 | ).toBeInTheDocument();
167 | });
168 | });
169 |
--------------------------------------------------------------------------------
/src/__tests__/AppWithRouter.test.jsx:
--------------------------------------------------------------------------------
1 | import { renderWithRouter } from "./utils/helpers";
2 | import { expect, it } from "vitest";
3 | import { screen } from "@testing-library/react";
4 | import userEvent from "@testing-library/user-event";
5 |
6 | it("should click on Users link and navigate to users route", async () => {
7 | renderWithRouter({ initialEntries: ["/"] });
8 | await userEvent.click(screen.getByRole("link", { name: "Users" }));
9 |
10 | expect(screen.getByText("Welcome to Users Dashboard")).toBeInTheDocument();
11 | });
12 |
13 | it("should navigate to /blog-posts and back to /", async () => {
14 | renderWithRouter({ initialEntries: ["/"] });
15 | await userEvent.click(screen.getByRole("link", { name: "Blogs" }));
16 | expect(screen.getByText("Welcome to BlogPosts Page")).toBeInTheDocument();
17 | await userEvent.click(screen.getByRole("link", { name: "Home" }));
18 | expect(screen.queryByText("Welcome to BlogPosts Page")).toBeNull();
19 | });
20 |
--------------------------------------------------------------------------------
/src/__tests__/PostContainer.test.jsx:
--------------------------------------------------------------------------------
1 | import { expect, it, describe } from "vitest";
2 | import { render, screen } from "@testing-library/react";
3 | import { PostContainer } from "../components/PostContainer";
4 | import { UserContext } from "../utils/contexts/UserContext";
5 |
6 | describe("render context values", () => {
7 | const mockUserContextData = {
8 | id: 1001,
9 | username: "johnny",
10 | email: "johnny@gmail.com",
11 | name: "Johnny",
12 | setUserData: () => {},
13 | };
14 |
15 | it("should match snapshot", () => {
16 | const { container } = render(
17 |
18 |
19 |
20 | );
21 | expect(container).toMatchSnapshot();
22 | });
23 |
24 | it("should display correct username", () => {
25 | render(
26 |
27 |
28 |
29 | );
30 |
31 | expect(screen.getByText("Username: johnny")).toBeInTheDocument();
32 | });
33 | });
34 |
--------------------------------------------------------------------------------
/src/__tests__/TestInputField.test.jsx:
--------------------------------------------------------------------------------
1 | import { expect, it, describe } from "vitest";
2 | import { render, screen } from "@testing-library/react";
3 | import { TestInputField } from "../components/TestInputField";
4 |
5 | it("should find input by placeholder value", () => {
6 | render();
7 | expect(screen.getByPlaceholderText(/enter data/)).toBeInTheDocument();
8 | });
9 |
10 | it("should find input by display value", () => {
11 | render();
12 | expect(screen.getByDisplayValue("hello")).toBeInTheDocument();
13 | });
14 |
--------------------------------------------------------------------------------
/src/__tests__/UsernameDisplay.test.jsx:
--------------------------------------------------------------------------------
1 | import { describe, it, test, expect } from "vitest";
2 | import { render, screen } from "@testing-library/react";
3 | import { UsernameDisplay } from "../components/UsernameDisplay";
4 |
5 | describe("UsernameDisplay", () => {
6 | it("should render username", async () => {
7 | render();
8 | expect(
9 | await screen.findByText("ansonthedev", {}, { timeout: 2000 })
10 | ).toBeInTheDocument();
11 | });
12 | });
13 |
--------------------------------------------------------------------------------
/src/__tests__/__snapshots__/PostContainer.test.jsx.snap:
--------------------------------------------------------------------------------
1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2 |
3 | exports[`render context values > should match snapshot 1`] = `
4 |
5 |
6 |
7 |
8 | PostContainer
9 |
10 |
11 |
12 | Display Name:
13 | Johnny
14 |
15 |
16 | ID:
17 | 1001
18 |
19 |
20 | Email:
21 | johnny@gmail.com
22 |
23 |
24 | Username:
25 | johnny
26 |
27 |
28 |
29 |
30 | PostContent
31 |
32 |
33 |
34 |
35 |
36 | PostContentButtons
37 |
38 |
39 | 1001
40 |
41 |
46 |
50 |
53 |
54 | johnny@gmail.com
55 |
56 |
57 |
58 | `;
59 |
--------------------------------------------------------------------------------
/src/__tests__/utils/helpers.jsx:
--------------------------------------------------------------------------------
1 | import { RouterProvider, createMemoryRouter } from "react-router-dom";
2 | import { routes } from "../../utils/constants";
3 | import { render } from "@testing-library/react";
4 |
5 | export const renderWithRouter = ({ initialEntries = [], initialIndex = 0 }) => {
6 | const router = createMemoryRouter(routes, {
7 | initialEntries,
8 | initialIndex,
9 | });
10 |
11 | return render();
12 | };
13 |
--------------------------------------------------------------------------------
/src/components/LoginForm.jsx:
--------------------------------------------------------------------------------
1 | import { useContext, useEffect } from "react";
2 | import { useDocumentClick } from "../utils/hooks/useDocumentClick";
3 | import { UserDetails } from "./UserDetails";
4 | import { AppContext } from "../utils/contexts/AppContext";
5 | import { RegisterForm } from "./RegisterForm";
6 |
7 | export function LoginForm() {
8 | const appContext = useContext(AppContext);
9 |
10 | console.log(appContext);
11 |
12 | useEffect(() => {
13 | const resizeEventHandler = (e) => {
14 | console.log("Window/ViewPort Resized!");
15 | };
16 |
17 | window.addEventListener("resize", resizeEventHandler);
18 |
19 | return () => {
20 | console.log("Unmounting LoginForm");
21 | console.log("Removing Resize Event Listener");
22 | window.removeEventListener("resize", resizeEventHandler);
23 | };
24 | }, []);
25 |
26 | useDocumentClick();
27 |
28 | return (
29 |
68 | );
69 | }
70 |
--------------------------------------------------------------------------------
/src/components/PostContainer.jsx:
--------------------------------------------------------------------------------
1 | import { useContext } from "react";
2 | import { PostContent } from "./PostContent";
3 | import { UserContext } from "../utils/contexts/UserContext";
4 |
5 | export function PostContainer() {
6 | const userContextData = useContext(UserContext);
7 |
8 | return (
9 |
10 |
11 |
PostContainer
12 |
13 |
Display Name: {userContextData.name}
14 |
ID: {userContextData.id}
15 |
Email: {userContextData.email}
16 |
Username: {userContextData.username}
17 |
18 |
19 | );
20 | }
21 |
--------------------------------------------------------------------------------
/src/components/PostContent.jsx:
--------------------------------------------------------------------------------
1 | import { useContext } from "react";
2 | import { PostContentButtons } from "./PostContentButtons";
3 | import { UserContext } from "../utils/contexts/UserContext";
4 |
5 | export function PostContent() {
6 | const userContextData = useContext(UserContext);
7 |
8 | return (
9 |
10 |
11 |
PostContent
12 |
13 |
14 | {userContextData.email}
15 |
16 | );
17 | }
18 |
--------------------------------------------------------------------------------
/src/components/PostContentButtons.jsx:
--------------------------------------------------------------------------------
1 | import { useContext, useState } from "react";
2 | import { UserContext } from "../utils/contexts/UserContext";
3 |
4 | export function PostContentButtons() {
5 | const { id, setUserData } = useContext(UserContext);
6 | const [value, setValue] = useState("");
7 |
8 | return (
9 |
10 |
11 |
PostContentButtons
12 |
13 | {id}
14 |
15 |
16 |
setValue(e.target.value)}
20 | />
21 |
31 |
32 | );
33 | }
34 |
--------------------------------------------------------------------------------
/src/components/RegisterForm.jsx:
--------------------------------------------------------------------------------
1 | import { useContext, useState } from "react";
2 | import { AppContext } from "../utils/contexts/AppContext";
3 |
4 | export function RegisterForm() {
5 | const [formFields, setFormFields] = useState({
6 | username: "",
7 | password: "",
8 | displayName: "",
9 | });
10 |
11 | const appContext = useContext(AppContext);
12 |
13 | return (
14 |
65 | );
66 | }
67 |
--------------------------------------------------------------------------------
/src/components/TestInputField.jsx:
--------------------------------------------------------------------------------
1 | export function TestInputField() {
2 | return (
3 | {}} />
4 | );
5 | }
6 |
--------------------------------------------------------------------------------
/src/components/UserDetails.jsx:
--------------------------------------------------------------------------------
1 | import PropTypes from "prop-types";
2 | import { useState } from "react";
3 |
4 | export function UserDetails({ user, setUsers }) {
5 | const [isEditing, setIsEditing] = useState(false);
6 | const [username, setUsername] = useState(user.username);
7 | const [email, setEmail] = useState(user.email);
8 |
9 | return (
10 |
86 | );
87 | }
88 |
89 | UserDetails.propTypes = {
90 | user: PropTypes.shape({
91 | id: PropTypes.number.isRequired,
92 | username: PropTypes.string.isRequired,
93 | email: PropTypes.string.isRequired,
94 | }).isRequired,
95 | setUsers: PropTypes.func.isRequired,
96 | };
97 |
--------------------------------------------------------------------------------
/src/components/UsernameDisplay.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 |
3 | export function UsernameDisplay({ username }) {
4 | const [timerPassed, setTimerPassed] = useState(false);
5 |
6 | useEffect(() => {
7 | setTimeout(() => {
8 | setTimerPassed(true);
9 | }, 1500);
10 | }, []);
11 |
12 | return (
13 |
14 | {timerPassed && username}
15 |
16 | );
17 | }
18 |
--------------------------------------------------------------------------------
/src/globals.css:
--------------------------------------------------------------------------------
1 | body {
2 | background-color: #1e1e1e;
3 | color: #e3e3e3;
4 | font-family: sans-serif;
5 | }
6 |
--------------------------------------------------------------------------------
/src/main.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom/client";
3 | import { createBrowserRouter, RouterProvider } from "react-router-dom";
4 | import "./globals.css";
5 | import { routes } from "./utils/constants";
6 |
7 | export const router = createBrowserRouter(routes);
8 |
9 | ReactDOM.createRoot(document.getElementById("root")).render(
10 |
11 |
12 |
13 | );
14 |
--------------------------------------------------------------------------------
/src/pages/BlogPostsPage.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 | import { useLocation } from "react-router-dom";
3 |
4 | export function BlogPostsPage() {
5 | const [posts, setPosts] = useState([]);
6 | const { state } = useLocation();
7 |
8 | useEffect(() => {
9 | if (state && state.posts) {
10 | setPosts(state.posts);
11 | }
12 | }, [state]);
13 |
14 | return (
15 |
16 |
Welcome to BlogPosts Page
17 | {posts.map((post) => (
18 |
19 |
{post.title}
20 |
23 |
24 | ))}
25 |
26 | );
27 | }
28 |
--------------------------------------------------------------------------------
/src/pages/UsersPage.jsx:
--------------------------------------------------------------------------------
1 | export function UsersPage() {
2 | return (
3 |
4 |
Welcome to Users Dashboard
5 |
6 | );
7 | }
8 |
--------------------------------------------------------------------------------
/src/utils/constants.jsx:
--------------------------------------------------------------------------------
1 | import App from "../App";
2 | import { BlogPostsPage } from "../pages/BlogPostsPage";
3 | import { UsersPage } from "../pages/UsersPage";
4 |
5 | export const routes = [
6 | {
7 | path: "/",
8 | element: (
9 |
23 | ),
24 | children: [
25 | {
26 | path: "/blog-posts",
27 | element: ,
28 | },
29 | ],
30 | },
31 | {
32 | path: "users",
33 | element: ,
34 | },
35 | ];
36 |
--------------------------------------------------------------------------------
/src/utils/contexts/UserContext.js:
--------------------------------------------------------------------------------
1 | import { createContext } from "react";
2 |
3 | export const UserContext = createContext({
4 | id: 0,
5 | username: "",
6 | email: "",
7 | name: "",
8 | setUserData: () => {},
9 | });
10 |
--------------------------------------------------------------------------------
/src/utils/hooks/useDocumentClick.js:
--------------------------------------------------------------------------------
1 | import { useEffect } from "react";
2 |
3 | export function useDocumentClick() {
4 | useEffect(() => {
5 | console.log("useDocumentClick");
6 |
7 | const handleDocumentClick = (e) => {
8 | console.log("Clicked Document");
9 | };
10 | document.addEventListener("click", handleDocumentClick);
11 |
12 | return () => {
13 | document.removeEventListener("click", handleDocumentClick);
14 | };
15 | }, []);
16 | }
17 |
--------------------------------------------------------------------------------
/src/utils/hooks/useFetchUser.js:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 |
3 | const userApiUrl = "https://jsonplaceholder.typicode.com/users";
4 |
5 | export function useFetchUser(userId) {
6 | const [userData, setUserData] = useState({});
7 | const [loading, setLoading] = useState(false);
8 | const [error, setError] = useState();
9 |
10 | useEffect(() => {
11 | const controller = new AbortController();
12 | setLoading(true);
13 | fetch(`${userApiUrl}/${userId}`, { signal: controller.signal })
14 | .then((response) => response.json())
15 | .then((data) => {
16 | setUserData(data);
17 | setError(undefined);
18 | })
19 | .catch((err) => {
20 | setError(err);
21 | })
22 | .finally(() => {
23 | setTimeout(() => {
24 | setLoading(false);
25 | }, 2000);
26 | });
27 | return () => {
28 | controller.abort();
29 | setTimeout(() => {
30 | setLoading(false);
31 | }, 2000);
32 | };
33 | }, [userId]);
34 |
35 | return { user: userData, loading, error };
36 | }
37 |
--------------------------------------------------------------------------------
/vite.config.js:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "vite";
2 | import react from "@vitejs/plugin-react";
3 |
4 | // https://vitejs.dev/config/
5 | export default defineConfig({
6 | plugins: [react()],
7 | test: {
8 | environment: "jsdom",
9 | setupFiles: "./setupTests.js",
10 | testTimeout: 10000,
11 | },
12 | });
13 |
--------------------------------------------------------------------------------