From 8d5a1fdefc87e680c689278640ad339a36fc9e76 Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 13 Jun 2022 18:58:10 +0000 Subject: [PATCH 1/3] feat: use global state --- src/App.tsx | 5 ++- src/components/Checkbox.tsx | 13 +++--- src/components/List.tsx | 86 ++++++++++++++++++------------------ src/components/ListState.tsx | 75 +++++++++++++++++++++++++++++++ src/pages/DashboardPage.tsx | 17 +++---- src/types.ts | 44 +++++++++++++++--- 6 files changed, 176 insertions(+), 64 deletions(-) create mode 100644 src/components/ListState.tsx diff --git a/src/App.tsx b/src/App.tsx index 177137c..eb0e9d4 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -2,11 +2,14 @@ import { ThemeProvider } from "@emotion/react"; import theme from "./theme"; import {DashboardPage} from "./pages"; +import { ListProvider } from "./components/ListState"; export default function App() { return ( - + + + ); } diff --git a/src/components/Checkbox.tsx b/src/components/Checkbox.tsx index 454337f..c0431ca 100644 --- a/src/components/Checkbox.tsx +++ b/src/components/Checkbox.tsx @@ -1,4 +1,4 @@ -import { FC } from "react"; +import { FC, memo } from "react"; import styled from "@emotion/styled"; import type { CheckboxProps } from "../types"; @@ -65,12 +65,15 @@ const StyledCheckbox = styled.div(({ theme }) => ({ } })); -export const Checkbox: FC = ({ item, name }) => ( +export const Checkbox: FC = memo(({ item, handleCheckboxChange }) => ( -) \ No newline at end of file +), (p, n) => p.item.status === n.item.status) \ No newline at end of file diff --git a/src/components/List.tsx b/src/components/List.tsx index ee1a1d1..841b9fd 100644 --- a/src/components/List.tsx +++ b/src/components/List.tsx @@ -1,7 +1,8 @@ -import { useState, useEffect, FC, memo } from "react"; +import { useState, useEffect, FC, memo, ChangeEvent } from "react"; import { Checkbox, Card, CardTitle, CardBody } from "./"; import type { Item, ListProps } from "../types"; import styled from "@emotion/styled"; +import {useListReducer} from './ListState' const ListHeader = styled.div(({ theme }) => ({ fontFamily: theme.other.fontFamily.lato, @@ -14,46 +15,47 @@ const ListHeader = styled.div(({ theme }) => ({ left: "38px" })); -export const List: FC = memo( - ({ getList, handleFormChange }) => { - const [apiState, setApiState] = useState<"loading" | "success" | "failure">( - "loading" - ); - const [items, setItems] = useState([]); +export const List: FC = ({ getList }) => { + const [apiState, setApiState] = useState<"loading" | "success" | "failure">( + "loading" + ); + const [{ items }, { addItems, updateItemStatus }] = useListReducer(); - useEffect(() => { - getList() - .then((data) => { - setTimeout(() => { - setItems(data); - setApiState("success"); - }, 500); - }) - .catch((error: Error) => { - setApiState("failure"); - console.error(error); - }); - }, [getList]); + useEffect(() => { + getList() + .then((data) => { + setTimeout(() => { + addItems(data); + setApiState("success"); + }, 500); + }) + .catch((error: Error) => { + setApiState("failure"); + console.error(error); + }); + }, [getList]); - return ( - - Super Special Checkbox list - - {apiState === "loading" ?
loading...
: <>} - {apiState === "failure" ?
Something went wrong!
: <>} - {apiState === "success" ? ( -
- Info - {items.map((item, i) => ( - - ))} -
- ) : ( - <> - )} -
-
- ); - }, - (p, n) => true -); + const handleCheckboxChange = (event: ChangeEvent) => { + updateItemStatus(event.currentTarget.id as `${number}`, event.currentTarget.checked) + } + + return ( + + Super Special Checkbox list + + {apiState === "loading" ?
loading...
: <>} + {apiState === "failure" ?
Something went wrong!
: <>} + {apiState === "success" ? ( + <> + Info + {items.map((item, i) => ( + + ))} + + ) : ( + <> + )} +
+
+ ); +}; diff --git a/src/components/ListState.tsx b/src/components/ListState.tsx new file mode 100644 index 0000000..084e9c9 --- /dev/null +++ b/src/components/ListState.tsx @@ -0,0 +1,75 @@ +import React, { + createContext, + FC, + useContext, + useReducer + } from 'react' + import type { + State, + ContextType, + ActionHandlers, + Actions, + Item, + Items, + ProviderType + } from '../types' + + const initialState: State = { + items: [] + } + + const stateInitializer = ( + _initialState: State + ): State => { + return { + ..._initialState + } + } + + const Context = createContext([ + stateInitializer(initialState), + {} as ActionHandlers + ]) + + const useListReducer = (): ContextType => useContext(Context) + + const reducer = (state: State, action: Actions) => { + switch (action.type) { + case 'addItems': { + return { + ...state, + items: action.items + } + } + case 'updateItemStatus': { + return { + ...state, + items: state.items.reduce((items: Items, item: Item): Items => { + if (item.id === action.itemId) return [...items, {...item, status: action.newStatus}] + return [...items, item] + }, []) + } + } + } + } + + const ListProvider: FC = ({ children }) => { + const [state, dispatch]: [ + State, + React.Dispatch + ] = useReducer(reducer, initialState, stateInitializer) + + const actionHandlers: ActionHandlers = { + addItems: (items) => dispatch({ type: 'addItems', items }), + updateItemStatus: (itemId, newStatus) => dispatch({ type: 'updateItemStatus', itemId, newStatus }) + } + + return ( + + {children} + + ) + } + + export { useListReducer, ListProvider } + \ No newline at end of file diff --git a/src/pages/DashboardPage.tsx b/src/pages/DashboardPage.tsx index 2e42912..9d01086 100644 --- a/src/pages/DashboardPage.tsx +++ b/src/pages/DashboardPage.tsx @@ -3,33 +3,28 @@ import { Layout, List } from "../components"; import { H2 } from "../components"; import { fetchItemsA, fetchItemsB } from "../api"; import type { Item, MockA, MockB } from "../types"; +import {useListReducer} from '../components/ListState' const getNormalisedMockListA = async () => fetchItemsA().then((data: MockA[]): Item[] => - data.map((d) => ([d.title])) + data.map((d, i) => ({id: `${i}`, status: false, infos: [d.title]})) ); const getNormalisedMockListB = async () => fetchItemsB().then((data: MockB[]): Item[] => - data.map((d) => ([d.name, d.description, d.link])) + data.map((d, i) => ({id: `${i}`, status: false, infos: [d.name, d.description, d.link]})) ); export const DashboardPage = () => { - const [checkedItems, setCheckedItems] = useState([]); - - const handleFormChange = (event: ChangeEvent) => { - const inputElements = Array.from(event.target.form) as HTMLInputElement[] - const checkedInputs = inputElements.map((input: HTMLInputElement) => input.checked ? input.name : '').filter(Boolean) - setCheckedItems(checkedInputs) - } + const [{ items }] = useListReducer(); return (

Selected indexes:{" "} - {checkedItems.join(", ") || "none"} + {items.map((item: Item) => item.status ? item.id : '').filter(Boolean).join(", ") || "none"}

- +
); } diff --git a/src/types.ts b/src/types.ts index f71c435..38a1ac6 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,14 +1,48 @@ -import { ReactElement, ChangeEventHandler } from "react"; - -export type Item = Array<(() => ReactElement) | string | undefined>; +import { ReactElement, ChangeEventHandler, ChangeEvent } from "react"; export type CheckboxProps = { item: Item; - name: string; + handleCheckboxChange: ChangeEventHandler }; -export type ListProps = { getList: () => Promise, handleFormChange: ChangeEventHandler } +export type ListProps = { getList: () => Promise } export type MockA = { id: number; title: string }; export type MockB = { name: string; description: string; link?: string }; + +export type Item = { + id: `${number}`; + infos: Array<(() => ReactElement) | string | undefined>; + status: boolean; + }; + + export type Items = Item[]; + + export type State = { + items: Items; + }; + + export type Actions = + | { type: "addItems"; items: Item[] } + | { type: "updateItemStatus"; itemId: Item["id"], newStatus: Item["status"] } + + export type ActionHandlers = { + addItems: (items: Item[]) => void; + updateItemStatus: (itemId: Item["id"], newStatus: boolean) => void; + }; + + export type ContextType = [State, ActionHandlers]; + + export type ProviderType = { + children: React.ReactNode; + }; + + export type ItemProps = { + id: Item["id"] + infos: Item["infos"] + status: boolean + handleIpdateItemStatus: (event: ChangeEvent) => void + }; + + \ No newline at end of file From 04a98316a8f2dcef37f8f992584ca2dc97ab238f Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 13 Jun 2022 19:09:55 +0000 Subject: [PATCH 2/3] use name not id on input elements --- src/components/Checkbox.tsx | 2 +- src/components/List.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/Checkbox.tsx b/src/components/Checkbox.tsx index c0431ca..33ca065 100644 --- a/src/components/Checkbox.tsx +++ b/src/components/Checkbox.tsx @@ -68,7 +68,7 @@ const StyledCheckbox = styled.div(({ theme }) => ({ export const Checkbox: FC = memo(({ item, handleCheckboxChange }) => (