diff --git a/src/App.test.tsx b/src/App.test.tsx index 2a68616..585ff6e 100644 --- a/src/App.test.tsx +++ b/src/App.test.tsx @@ -1,9 +1,9 @@ -import React from 'react'; -import { render, screen } from '@testing-library/react'; -import App from './App'; +import React from "react"; +import { render, screen } from "@testing-library/react"; +import Apps from "./App"; -test('renders learn react link', () => { - render(); +test("renders learn react link", () => { + render(); const linkElement = screen.getByText(/learn react/i); expect(linkElement).toBeInTheDocument(); }); diff --git a/src/App.tsx b/src/App.tsx index 5349b8e..f34941f 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,10 +1,10 @@ -import logo from './logo.svg'; -import './App.css'; +import "./App.css"; +import ReactUseRefHooks from "./hooks/useref/UseRefHook"; function App() { return ( -
- +
+
); } diff --git a/src/assets/success.png b/src/assets/success.png new file mode 100644 index 0000000..3e10f25 Binary files /dev/null and b/src/assets/success.png differ diff --git a/src/hooks/useref/UseRefHook.tsx b/src/hooks/useref/UseRefHook.tsx new file mode 100644 index 0000000..f9a9ceb --- /dev/null +++ b/src/hooks/useref/UseRefHook.tsx @@ -0,0 +1,49 @@ +import InfiteScrollView from "./component/InfiniteScrollView"; +import InfiteScrollViewCorrected from "./component/InfiniteScrollViewCorrected"; +import ListComponent from "./component/ListComponent"; + +export interface ReactUseStateHooksProps {} + +export default function ReactUseRefHooks(props: ReactUseStateHooksProps) { + return ( +
+ + + +
+ ); +} + +function Row1() { + return ( + <> +
+ 1. List scroll component with useRef hook +
+ + + ); +} + +function Row2() { + return ( + <> +
+ 2. Infinite List implementation using the useRef hook. +
+ + + ); +} + +function Row3() { + return ( + <> +
+ 3. Infinite list implementation using the useRef and UseReducer to + correct the loading. +
+ + + ); +} diff --git a/src/hooks/useref/component/InfiniteScrollView.tsx b/src/hooks/useref/component/InfiniteScrollView.tsx new file mode 100644 index 0000000..05b4a15 --- /dev/null +++ b/src/hooks/useref/component/InfiniteScrollView.tsx @@ -0,0 +1,65 @@ +import { Button, CircularProgress, TextField } from "@mui/material"; +import { dataList, DataType } from "./data/ListData"; +import { Ref, useCallback, useEffect, useRef, useState } from "react"; + +export interface InfiteScrollViewProps {} + +type RefType = HTMLDivElement | null; + +// Infinite scroll without useReducer it causes issue with loading spinner visibility. +export default function InfiteScrollViewCorrected( + props: InfiteScrollViewProps +) { + const [dataListCurrent, setDataListCurrent] = useState([]); + const [loading, setLoading] = useState(false); + const [hasMore, setHasMore] = useState(true); + + const lastIndexRef = useRef(0); + const lastListItemRef = useRef(null); + + // Data fetch mock code + useEffect(() => { + const observer = new IntersectionObserver((enteries) => { + if (enteries[0].isIntersecting && hasMore) { + setLoading(true); + //Code to mock the network request. + setTimeout(() => { + setDataListCurrent([...dataList.slice(0, lastIndexRef.current + 15)]); + lastIndexRef.current = lastIndexRef.current + 15; + if (lastIndexRef.current >= dataList.length - 1) { + setHasMore(false); + } + setLoading(false); + }, 2000); + } + }); + if (lastListItemRef.current) { + observer.observe(lastListItemRef.current); + } + + return () => { + if (lastListItemRef.current) observer.unobserve(lastListItemRef.current); + }; + }, [lastListItemRef]); + + return ( +
+
    + {dataListCurrent.map((data: DataType) => ( +
    + {data.name} +
    + ))} + {loading && ( +
    + +
    + )} +
    +
+
+ ); +} diff --git a/src/hooks/useref/component/InfiniteScrollViewCorrected.tsx b/src/hooks/useref/component/InfiniteScrollViewCorrected.tsx new file mode 100644 index 0000000..839cf07 --- /dev/null +++ b/src/hooks/useref/component/InfiniteScrollViewCorrected.tsx @@ -0,0 +1,103 @@ +import { CircularProgress, Icon, SvgIcon, TextField } from "@mui/material"; +import { dataList, DataType } from "./data/ListData"; +import { useEffect, useReducer, useRef, useState } from "react"; +import Success from "../../../assets/success.png"; +export interface InfiteScrollViewProps {} + +type RefType = HTMLDivElement | null; + +enum Type { + LOADING = "loading", + DATA = "data", +} + +/** + * Type definition of the action type of the action for component. + */ +interface ActionType { + type: Type; + lastIndex: number; +} + +interface State { + data: DataType[]; + loading: boolean; +} + +/** + * Reducer function to manage the states and actions. + * + * @param state Total number of the states to manage. + * @param action Action related to the state we have to manage. + * @returns + */ +function reducerFunction(state: State, action: ActionType) { + console.log(action.type); + switch (action.type) { + case Type.LOADING: + console.log(state.loading); + return { data: state.data, loading: !state.loading }; + case Type.DATA: + return { + data: [...dataList.slice(0, action.lastIndex + 15)], + loading: state.loading, + }; + } +} + +const initialState: State = { data: [], loading: false }; + +// Infinite scroll without useReducer it causes issue with loading spinner visibility. +export default function InfiteScrollView(props: InfiteScrollViewProps) { + const [state, dispatch] = useReducer(reducerFunction, initialState); + + const lastIndexRef = useRef(0); + const lastListItemRef = useRef(null); + + // Data fetch mock code + useEffect(() => { + const observer = new IntersectionObserver((enteries) => { + if (enteries[0].isIntersecting) { + dispatch({ type: Type.LOADING, lastIndex: lastIndexRef.current }); + //Code to mock the network request. + setTimeout(() => { + dispatch({ type: Type.DATA, lastIndex: lastIndexRef.current }); + dispatch({ type: Type.LOADING, lastIndex: lastIndexRef.current }); + lastIndexRef.current = lastIndexRef.current + 15; + if (lastIndexRef.current >= dataList.length - 1) { + } + }, 3000); + } + }); + if (lastListItemRef.current) { + observer.observe(lastListItemRef.current); + } + + return () => { + if (lastListItemRef.current) observer.unobserve(lastListItemRef.current); + }; + }, [lastListItemRef]); + + return ( +
+
    + {state.data.map((data: DataType) => ( +
    + {data.name} +
    + ))} +
    +
+
+ {state.loading ? ( + + ) : ( + + )} +
+
+ ); +} diff --git a/src/hooks/useref/component/ListComponent.tsx b/src/hooks/useref/component/ListComponent.tsx new file mode 100644 index 0000000..6e094d2 --- /dev/null +++ b/src/hooks/useref/component/ListComponent.tsx @@ -0,0 +1,73 @@ +import { Button, TextField } from "@mui/material"; +import { dataList, DataType } from "./data/ListData"; +import { useRef, useState } from "react"; +import { text } from "stream/consumers"; + +export interface IListComponentProps {} + +type RefType = HTMLUListElement | null; + +export default function ListComponent(props: IListComponentProps) { + const [index, setIndex] = useState(0); + + function scrollToIndex(index: number) { + const listNode: RefType = listRef.current; + const childNode = listNode!.querySelectorAll("ul > div")[index]; + childNode.scrollIntoView({ + behavior: "smooth", + block: "end", + inline: "center", + }); + } + + const listRef = useRef(null); + + const handleFirstButtonClick = () => { + scrollToIndex(0); + }; + + const handleScrollToIndexButtonClick = () => { + scrollToIndex(index); + }; + + const handleSecondButtonClick = () => { + scrollToIndex(dataList.length - 1); + }; + + return ( +
+
+ + + { + setIndex(e.target.value); + }} + label="Index" + /> + + +
+
    + {dataList.map((data: DataType) => ( +
    + {data.name} +
    + ))} +
+
+ ); +} diff --git a/src/hooks/useref/component/data/ListData.tsx b/src/hooks/useref/component/data/ListData.tsx new file mode 100644 index 0000000..62d0409 --- /dev/null +++ b/src/hooks/useref/component/data/ListData.tsx @@ -0,0 +1,73 @@ +/** + * Type definition for the data string. + */ +export interface DataType { + id: number; + name: string; +} + +/** + * Data list to show the infinite list thing. + */ +export const dataList: DataType[] = [ + { id: 1, name: "Services1" }, + { id: 2, name: "Engineering" }, + { id: 3, name: "Services2" }, + { id: 4, name: "Training" }, + { id: 5, name: "Support" }, + { id: 6, name: "Research and Development" }, + { id: 7, name: "Training" }, + { id: 8, name: "Human Resources" }, + { id: 9, name: "Services2" }, + { id: 10, name: "Legal1" }, + { id: 11, name: "Sales" }, + { id: 12, name: "Legal2" }, + { id: 13, name: "Accounting" }, + { id: 14, name: "Business Development" }, + { id: 15, name: "Accounting" }, + { id: 16, name: "Services3" }, + { id: 17, name: "Training" }, + { id: 18, name: "Research and Development" }, + { id: 19, name: "Human Resources" }, + { id: 20, name: "Legal3" }, + { id: 21, name: "Research and Development" }, + { id: 22, name: "Human Resources" }, + { id: 23, name: "Services4" }, + { id: 24, name: "Research and Development" }, + { id: 25, name: "Research and Development" }, + { id: 26, name: "Accounting" }, + { id: 27, name: "Product Management" }, + { id: 28, name: "Human Resources" }, + { id: 29, name: "Legal4" }, + { id: 30, name: "Legal5" }, + { id: 31, name: "Services5" }, + { id: 32, name: "Engineering1" }, + { id: 33, name: "Services6" }, + { id: 34, name: "Training" }, + { id: 35, name: "Support" }, + { id: 36, name: "Research and Development" }, + { id: 37, name: "Training" }, + { id: 38, name: "Human Resources" }, + { id: 39, name: "Services7" }, + { id: 40, name: "Legal6" }, + { id: 41, name: "Sales" }, + { id: 42, name: "Legal7" }, + { id: 43, name: "Accounting" }, + { id: 44, name: "Business Development" }, + { id: 45, name: "Accounting" }, + { id: 46, name: "Services7" }, + { id: 47, name: "Training" }, + { id: 48, name: "Research and Development" }, + { id: 49, name: "Human Resources" }, + { id: 50, name: "Legal8" }, + { id: 51, name: "Research and Development" }, + { id: 52, name: "Human Resources" }, + { id: 53, name: "Services8" }, + { id: 54, name: "Research and Development" }, + { id: 55, name: "Research and Development" }, + { id: 56, name: "Accounting" }, + { id: 57, name: "Product Management" }, + { id: 58, name: "Human Resources" }, + { id: 59, name: "Legal9" }, + { id: 60, name: "Legal10" }, +]; diff --git a/src/index.tsx b/src/index.tsx index 032464f..bfb6316 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,15 +1,15 @@ -import React from 'react'; -import ReactDOM from 'react-dom/client'; -import './index.css'; -import App from './App'; -import reportWebVitals from './reportWebVitals'; +import React from "react"; +import ReactDOM from "react-dom/client"; +import "./index.css"; +import Apps from "./App"; +import reportWebVitals from "./reportWebVitals"; const root = ReactDOM.createRoot( - document.getElementById('root') as HTMLElement + document.getElementById("root") as HTMLElement ); root.render( - + );