diff --git a/public/pokemon-battleground.webp b/public/pokemon-battleground.webp new file mode 100644 index 0000000..87ca346 Binary files /dev/null and b/public/pokemon-battleground.webp differ diff --git a/public/pokemon-logo.png b/public/pokemon-logo.png new file mode 100644 index 0000000..ed90ba2 Binary files /dev/null and b/public/pokemon-logo.png differ diff --git a/src/course/02- lessons/01-Bronze/StateColocationVsStateLifting/exercise/components/Form.tsx b/src/course/02- lessons/01-Bronze/StateColocationVsStateLifting/exercise/components/Form.tsx new file mode 100644 index 0000000..ef12031 --- /dev/null +++ b/src/course/02- lessons/01-Bronze/StateColocationVsStateLifting/exercise/components/Form.tsx @@ -0,0 +1,111 @@ +import { Skeleton } from '@shared/components/Skeleton/Skeleton.component'; +import { + TPokemonTypesApiResponse, + usePokedex +} from '@shared/hooks/usePokedex'; +import classNames from 'classnames'; +import { FormEvent } from 'react'; + +// πŸ§‘πŸ»β€πŸ’» 1.f: we need to pass a prop called onPokemonTypesUpdate which will take a string[] setup the interface and suppky the parameter to the Form component. + +export const Form = () => { + // πŸ§‘πŸ»β€πŸ’» 1.c: Setup a useState state colocation variable called selectedPokemonTypes, setSelectedPokemonTypes which will have a default of [] + + // ✍🏻 This is already done for you. Feel free to have a look how it works in shared/hooks/usePokedex + const { data, isLoading } = usePokedex({ + path: 'types', + queryParams: 'pageSize=8' + }); + + const handleSubmit = (event: FormEvent) => { + event.preventDefault(); + + // πŸ§‘πŸ»β€πŸ’» 1.g: now we want to validate whether the selectedPokemonTypes have 4 items in the array before we call onPokemonTypesUpdate(selectedPokemonTypes). + + // Once completed, head over to Screen.tsx as the Form component will be complaining about a missing prop. + }; + + const onPokemonTypeSelection = (type: string) => { + // πŸ’£ We can get rid of this line once we start using the type param. + console.log(type); + // πŸ§‘πŸ»β€πŸ’» 1.e: we need to check IF the selectedPokemonTypes already has the selectedType + // because we need to toggle it on and off. If it is selected, we just setSelectedPokemonTypes with the filtered out type + // if it's not in there then we set the type [...selectedPokemonTypes, type]; + }; + + return ( +
+

+ Select you favorite pokemon types (max 4) +

+
+ {isLoading && ( +
+ + + + + + + + + + + + +
+ )} +
+ {data && + data.length && + data.map((pokemonType) => { + const isSelected = + // πŸ§‘πŸ»β€πŸ’» 1.d: replace the empty array with the selectedPokemonTypes state variable. + // πŸ’£ We can remove these ignores when we apply the code + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + [].includes(pokemonType); + const hasSelectedAllOptions = [].length === 4; + + return ( + + ); + })} +
+ + +
+
+ ); +}; diff --git a/src/course/02- lessons/01-Bronze/StateColocationVsStateLifting/exercise/components/PokemonOptions.tsx b/src/course/02- lessons/01-Bronze/StateColocationVsStateLifting/exercise/components/PokemonOptions.tsx new file mode 100644 index 0000000..e2c27c5 --- /dev/null +++ b/src/course/02- lessons/01-Bronze/StateColocationVsStateLifting/exercise/components/PokemonOptions.tsx @@ -0,0 +1,129 @@ +import { Skeleton } from '@shared/components/Skeleton/Skeleton.component'; +import { + TPokemonCardsApiResponse, + usePokedex +} from '@shared/hooks/usePokedex'; +import classNames from 'classnames'; +import { FormEvent } from 'react'; + +interface IPokemonOptions { + type: string; + // πŸ§‘πŸ»β€πŸ’» 2.h: Add two new props called onPokemonSelection which takes a string[] and string as params and another + // variable called defaultSelectedPokemon which is an optional string[]. +} + +export const PokemonOptions = ({ type }: IPokemonOptions) => { + // πŸ§‘πŸ»β€πŸ’» 2.d: Create a selectedPokemon useState variable. Default value to be [] + + // πŸ§‘πŸ»β€πŸ’» 2.i: Replace the default of selectedPokemon from [] to the defaultSelectedPokemon. This will remember which cards were selected when the component re-renders. + + // ✍🏻 This is already done for you. Feel free to have a look how it works in shared/hooks/usePokedex + const { data, isLoading, isError } = usePokedex< + TPokemonCardsApiResponse[] + >({ + path: 'cards', + queryParams: `pageSize=4&q=types:${type}&supertype:pokemon`, + skip: type === undefined + }); + + const handleSubmit = (event: FormEvent) => { + event.preventDefault(); + + // πŸ§‘πŸ»β€πŸ’» 2.j: call onPokemonSelection(selectedPokemon, type); + }; + + const togglePokemonSelection = (pokemonId: string) => { + // πŸ’£ We can get rid of this line once we start using the type param. + console.log(pokemonId); + // πŸ§‘πŸ»β€πŸ’» 2.g: We need to now update the state for when a pokemon card is selected or not. + // IF selectedPokemon includes the pokemonId then we need to de-select that pokemon card. + // ELSE we add the pokemon id to the array of pokemon cards [...selectedPokemon, pokemonId] and then setSelectedPokemon(newValues) (make this a variable as we will need it for later.) + // You should now start to see the pokemon being selected and de-selected. But the next thing we need to do is update the state within the screen. Search for 2.h + // πŸ§‘πŸ»β€πŸ’» 2.k: Inside the ELSE, check if the newlySelectedPokemon has the length of 2. IF it does, call onPokemonSelection(newlySelectedPokemon, type);. Head over to the Screen.tsx component to finish it off. + }; + + return ( +
+

+ {type} Pokemon +

+
+
+ {isLoading && ( +
+ + + + +
+ )} +
+ {data && + data.length > 0 && + data.map((pokemonCard) => { + // πŸ§‘πŸ»β€πŸ’» 2.e: Replace the empty arrays with the selectedPokemon variable we created. + const isSelected = [].find( + (pokemonId) => pokemonId === pokemonCard.id + ); + const allPokemonSelected = [].length === 2; + + return ( + + ); + })} +
+ + +
+
+
+ ); +}; diff --git a/src/course/02- lessons/01-Bronze/StateColocationVsStateLifting/exercise/components/Screen.tsx b/src/course/02- lessons/01-Bronze/StateColocationVsStateLifting/exercise/components/Screen.tsx new file mode 100644 index 0000000..62d6c72 --- /dev/null +++ b/src/course/02- lessons/01-Bronze/StateColocationVsStateLifting/exercise/components/Screen.tsx @@ -0,0 +1,84 @@ +/** + * Exercise: 🧼 State Colocation vs πŸͺœ State Lifting + * + * πŸ€” Observations of this file + * So the lead developer wanted us to manage the state for the selected pokemon types & the selected pokemon at this level so those variables can be used here and within the children. Each variable will be an array of strings which represent the type or the id of the selected pokemon. + * + * We need to tackle this in stages... + * + * Stage one (follow 1.* steps) - Creating the form that returns the pokemon types and saving those selected types to the lifted state variable + * Stage two (following 2.* steps) - Using those types, we will render the pokemon options component + * + */ + +export const Screen = () => { + // πŸ§‘πŸ»β€πŸ’» 1.a: Create a useState variable called selectedPokemonTypes, setSelectedPokemonTypes. Default to be an empty array. + + // πŸ§‘πŸ»β€πŸ’» 2.a: Create a useState> variable called selectedPokemon, setSelectedPokemon. Default to be an object {}. + + // πŸ§‘πŸ»β€πŸ’» 1.h: Create a function called onPokemonTypesUpdate which will take a types: string[] param. Pass that into the Form component as a prop. The function will just need setSelectedPokemonTypes for now. + // πŸŽ‰ STAGE ONE COMPLETED you should now be able to see the types display, select them and then the state in the screen gets updated. + + // πŸ§‘πŸ»β€πŸ’» 2.m: You will now start to see the happy path all working fine, however when you change to different pokemon types and receive a new set of pokemon you now get some messy state where selectedPokemon is more than 8. To fix this, write the following code inside 1.h function (before the setSelectedPokemonTypes) + /* + const newlyUpdatedPokemon = { ...selectedPokemon }; + + selectedPokemonTypes + .filter((type) => { + return !types.find((selectedType) => selectedType === type); + }) + .forEach((type) => { + delete newlyUpdatedPokemon[type]; + }); + + setSelectedPokemon(newlyUpdatedPokemon); + */ + // STAGE TWO completed. You have now built the screen. BUT 🐞 there is a bug where the PokemonOptions re-renders the types that did not need to update when you change your types after one try. The reason is the "key" prop using index. The api has no identifier per type. If you enjoyed this exercise have a look into fixing it and make a pr. + + // πŸ§‘πŸ»β€πŸ’» 2.l: Create a function called onPokemonSelection which will take a pokemon: string[], type: string + // Then create a newlySelectedPokemon variable which will be a copy of the current selectedPokemon {...selectedPokemon} + // assign the newlySelectedPokemon[type] to equal the pokemon variable. + // setSelectedPokemon(newlySelectedPokemon); + + return ( +
+ hello +
+
+
+

+ Pokemon Battle Picker + + Battle Picker + +

+
+
+ {/* πŸ§‘πŸ»β€πŸ’» 1.b: Render pokemon types form from ./components/Form and then head over to the form component */} +
+
+ {/* πŸ§‘πŸ»β€πŸ’» 2.b: Loop through the selectedPokemonTypes and pass down the type property to the PokemonOptions (./components/PokemonOptions) component. You will also need a key to be on the component. I used `${pokemonType}-${index}` */} +
+ + {/* πŸ§‘πŸ»β€πŸ’» 2.c: We need to check if the KEYS in the selectedPokemon object equal 4 and the selectedPokemonTypes length is 4 before rendering the code snippet below. Head over to PokemonOptions when completed. */} + {/* + + */} +
+
+ ); +}; diff --git a/src/course/02- lessons/01-Bronze/StateColocationVsStateLifting/exercise/exercise.stories.tsx b/src/course/02- lessons/01-Bronze/StateColocationVsStateLifting/exercise/exercise.stories.tsx new file mode 100644 index 0000000..b808a12 --- /dev/null +++ b/src/course/02- lessons/01-Bronze/StateColocationVsStateLifting/exercise/exercise.stories.tsx @@ -0,0 +1,24 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import { Exercise } from './exercise'; + +const meta: Meta = { + title: + 'Lessons/πŸ₯‰ Bronze/🧼 State Colocation vs πŸͺœ State Lifting/02-Exercise', + component: Exercise, + parameters: { + layout: 'fullscreen' + } +}; + +export default meta; +type Story = StoryObj; + +/* + * See https://storybook.js.org/docs/writing-stories/play-function#working-with-the-canvas + * to learn more about using the canvasElement to query the DOM + */ +export const Default: Story = { + play: async () => {}, + args: {} +}; diff --git a/src/course/02- lessons/01-Bronze/StateColocationVsStateLifting/exercise/exercise.tsx b/src/course/02- lessons/01-Bronze/StateColocationVsStateLifting/exercise/exercise.tsx new file mode 100644 index 0000000..ee28457 --- /dev/null +++ b/src/course/02- lessons/01-Bronze/StateColocationVsStateLifting/exercise/exercise.tsx @@ -0,0 +1,4 @@ +import { Screen } from './components/Screen'; + +// Head over to screen to get started. +export const Exercise = () => ; diff --git a/src/course/02- lessons/01-Bronze/StateColocationVsStateLifting/final/components/Form.tsx b/src/course/02- lessons/01-Bronze/StateColocationVsStateLifting/final/components/Form.tsx new file mode 100644 index 0000000..e52e78f --- /dev/null +++ b/src/course/02- lessons/01-Bronze/StateColocationVsStateLifting/final/components/Form.tsx @@ -0,0 +1,115 @@ +import { Skeleton } from '@shared/components/Skeleton/Skeleton.component'; +import { + TPokemonTypesApiResponse, + usePokedex +} from '@shared/hooks/usePokedex'; +import classNames from 'classnames'; +import { FormEvent, useState } from 'react'; + +interface IForm { + onPokemonTypesUpdate: (types: string[]) => void; +} + +export const Form = ({ onPokemonTypesUpdate }: IForm) => { + const [selectedPokemonTypes, setSelectedPokemonTypes] = useState< + string[] + >([]); + + const { data, isLoading } = usePokedex({ + path: 'types', + queryParams: 'pageSize=8' + }); + + const handleSubmit = (event: FormEvent) => { + event.preventDefault(); + + if (selectedPokemonTypes.length === 4) { + onPokemonTypesUpdate(selectedPokemonTypes); + } + }; + + const onPokemonTypeSelection = (type: string) => { + if (selectedPokemonTypes.includes(type)) { + setSelectedPokemonTypes( + selectedPokemonTypes.filter( + (pokemonType) => pokemonType !== type + ) + ); + } else { + setSelectedPokemonTypes([...selectedPokemonTypes, type]); + } + }; + + return ( +
+

+ Select you favorite pokemon types (max 4) +

+
+ {isLoading && ( +
+ + + + + + + + + + + + +
+ )} +
+ {data && + data.length && + data.map((pokemonType) => { + const isSelected = + selectedPokemonTypes.includes(pokemonType); + const hasSelectedAllOptions = + selectedPokemonTypes.length === 4; + + return ( + + ); + })} +
+ + +
+
+ ); +}; diff --git a/src/course/02- lessons/01-Bronze/StateColocationVsStateLifting/final/components/PokemonOptions.tsx b/src/course/02- lessons/01-Bronze/StateColocationVsStateLifting/final/components/PokemonOptions.tsx new file mode 100644 index 0000000..cb2c8e5 --- /dev/null +++ b/src/course/02- lessons/01-Bronze/StateColocationVsStateLifting/final/components/PokemonOptions.tsx @@ -0,0 +1,141 @@ +import { Skeleton } from '@shared/components/Skeleton/Skeleton.component'; +import { + TPokemonCardsApiResponse, + usePokedex +} from '@shared/hooks/usePokedex'; +import classNames from 'classnames'; +import { FormEvent, useState } from 'react'; + +interface IPokemonOptions { + type: string; + onPokemonSelection: (pokemonIds: string[], type: string) => void; + defaultSelectedPokemon?: string[]; +} + +export const PokemonOptions = ({ + type, + onPokemonSelection, + defaultSelectedPokemon = [] +}: IPokemonOptions) => { + // 🧼 State Colocation: we only want to have 2 cards in each component selected and use that for validation. + const [selectedPokemon, setSelectedPokemon] = useState( + // πŸͺœ State Lifting: We inherit the lifted state to maintain the selected options when we change pokemon types. + defaultSelectedPokemon + ); + + // 🧼 State Colocation: We want to only call this api for the type provided and not all the types selected. + const { data, isLoading, isError } = usePokedex< + TPokemonCardsApiResponse[] + >({ + path: 'cards', + queryParams: `pageSize=4&q=types:${type}&supertype:pokemon`, + skip: type === undefined + }); + + const handleSubmit = (event: FormEvent) => { + event.preventDefault(); + + // πŸͺœ State Lifting passing a function in as a prop to update the state above. + onPokemonSelection(selectedPokemon, type); + }; + + const togglePokemonSelection = (pokemonId: string) => { + if (selectedPokemon.includes(pokemonId)) { + setSelectedPokemon( + selectedPokemon.filter( + (selectedPokemonId) => selectedPokemonId !== pokemonId + ) + ); + } else { + const newlySelectedPokemon = [...selectedPokemon, pokemonId]; + // 🧼 State Colocation: Updating the visual state of the selected pokemon. + setSelectedPokemon(newlySelectedPokemon); + + if (newlySelectedPokemon.length === 2) { + // πŸͺœ State Lifting passing a function in as a prop to update the state above. + onPokemonSelection(newlySelectedPokemon, type); + } + } + }; + + return ( +
+

+ {type} Pokemon +

+
+
+ {isLoading && ( +
+ + + + +
+ )} +
+ {data && + data.length > 0 && + data.map((pokemonCard) => { + const isSelected = selectedPokemon.find( + (pokemonId) => pokemonId === pokemonCard.id + ); + const allPokemonSelected = + selectedPokemon.length === 2; + + return ( + + ); + })} +
+ + +
+
+
+ ); +}; diff --git a/src/course/02- lessons/01-Bronze/StateColocationVsStateLifting/final/components/Screen.tsx b/src/course/02- lessons/01-Bronze/StateColocationVsStateLifting/final/components/Screen.tsx new file mode 100644 index 0000000..045650a --- /dev/null +++ b/src/course/02- lessons/01-Bronze/StateColocationVsStateLifting/final/components/Screen.tsx @@ -0,0 +1,88 @@ +import { useState } from 'react'; +import { Form } from './Form'; +import { PokemonOptions } from './PokemonOptions'; + +export const Screen = () => { + // πŸͺœ State Lifting: Setting up shared state variables so the components in this scope can read and use them + const [selectedPokemonTypes, setSelectedPokemonTypes] = useState< + string[] + >([]); + + // πŸͺœ State Lifting: Setting up shared state variables so the components in this scope can read and use them + const [selectedPokemon, setSelectedPokemon] = useState< + Record + >({}); + + // πŸͺœ State Lifting: Setting up a function which will update the state instead of bleeding this complexity into the UI component. + const onPokemonTypesUpdate = (types: string[]) => { + const newlyUpdatedPokemon = { ...selectedPokemon }; + + selectedPokemonTypes + .filter((type) => { + return !types.find((selectedType) => selectedType === type); + }) + .forEach((type) => { + delete newlyUpdatedPokemon[type]; + }); + + setSelectedPokemon(newlyUpdatedPokemon); + + setSelectedPokemonTypes(types); + }; + + // πŸͺœ State Lifting: Setting up a function which will update the state instead of bleeding this complexity into the UI component. + const onPokemonSelection = (pokemon: string[], type: string) => { + const newPokemon = { ...selectedPokemon }; + newPokemon[type] = pokemon; + setSelectedPokemon(newPokemon); + }; + + return ( +
+ hello +
+
+
+

+ Pokemon Battle Picker + + Battle Picker + +

+
+
+
+
+
+ {selectedPokemonTypes.map((pokemonType, index) => ( + + ))} +
+ + {Object.keys(selectedPokemon).length === 4 && + selectedPokemonTypes.length === 4 && ( + + )} +
+
+ ); +}; diff --git a/src/course/02- lessons/01-Bronze/StateColocationVsStateLifting/final/final.stories.tsx b/src/course/02- lessons/01-Bronze/StateColocationVsStateLifting/final/final.stories.tsx new file mode 100644 index 0000000..0b0f87b --- /dev/null +++ b/src/course/02- lessons/01-Bronze/StateColocationVsStateLifting/final/final.stories.tsx @@ -0,0 +1,24 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import { Final } from './final'; + +const meta: Meta = { + title: + 'Lessons/πŸ₯‰ Bronze/🧼 State Colocation vs πŸͺœ State Lifting/03-Final', + component: Final, + parameters: { + layout: 'fullscreen' + } +}; + +export default meta; +type Story = StoryObj; + +/* + * See https://storybook.js.org/docs/writing-stories/play-function#working-with-the-canvas + * to learn more about using the canvasElement to query the DOM + */ +export const Default: Story = { + play: async () => {}, + args: {} +}; diff --git a/src/course/02- lessons/01-Bronze/StateColocationVsStateLifting/final/final.tsx b/src/course/02- lessons/01-Bronze/StateColocationVsStateLifting/final/final.tsx new file mode 100644 index 0000000..0bcdfa9 --- /dev/null +++ b/src/course/02- lessons/01-Bronze/StateColocationVsStateLifting/final/final.tsx @@ -0,0 +1,3 @@ +import { Screen } from './components/Screen'; + +export const Final = () => ; diff --git a/src/course/02- lessons/01-Bronze/StateColocationVsStateLifting/lesson.mdx b/src/course/02- lessons/01-Bronze/StateColocationVsStateLifting/lesson.mdx new file mode 100644 index 0000000..79ff880 --- /dev/null +++ b/src/course/02- lessons/01-Bronze/StateColocationVsStateLifting/lesson.mdx @@ -0,0 +1,123 @@ +import { Meta } from '@storybook/blocks'; + + + +--- + +# 🧼 State Colocation vs πŸͺœ State Lifting + +Understanding the difference between **state colocation** and **state lifting** is crucial for managing state effectively in React apps. Although they both deal with the _placement_ of state, they serve **opposite purposes**. + +--- + +## 🧼 State Colocation + +### What is it? + +Keep state **as close as possible** to where it’s needed. + +```jsx +function SearchInput() { + const [query, setQuery] = useState(''); // colocated inside the component that needs it + + return ( + setQuery(e.target.value)} /> + ); +} +``` + +### What It Means: + +Only the component that directly uses the state should own it. This minimizes prop drilling and makes the component more self-contained. + +### When to Use + +- Only one component uses the state +- No need to share the state with siblings or parents + +### Benefits + +- Easier to maintain and test +- Reduces unnecessary props +- Keeps state logically scoped + +--- + +## πŸͺœ State Lifting + +### What is it? + +Move state **up** the component tree to share it across multiple components. + +```jsx +function Parent() { + const [count, setCount] = useState(0); // lifted up here + + return ( + <> + setCount(count + 1)} + /> + + + ); +} +``` + +### What It Means + +If two or more components need access to or control over the same piece of state, lift it to their **closest common ancestor**. + +### When to Use + +- Two or more components need to read from or update the same state +- You need synchronized behavior across sibling components + +### Benefits + +- Keeps state in sync +- Prevents duplication or divergence of values +- Encourages better separation of concerns + +--- + +## 🧠 Summary Table + +| Pattern | Goal | Where State Lives | When to Use | +| -------------------- | ------------------------------------------ | ----------------------- | --------------------------------------- | +| **State Colocation** | Keep state near the component that uses it | Inside the component | When only one component needs the state | +| **State Lifting** | Share state across components | Common parent component | When multiple components need the state | + +--- + +## 🧠 Rule of Thumb + +- Use **colocation** by default. +- Use **lifting** when you need to **share or sync state** between components. + +--- + +## Exercise + +### Scenario + +You are creating a part of the new Pokemon Battle game and the screen you are working on is the "Pick your team" screen. We need to be able to choose up to four pokemon types and then choose 2 randomly picked pokemon from each type before we move onto the battle screen. The screen will work like this: + +1. You select four options from the form and select get Pokemon. +2. You will then be presented a grid which will display four different Pokemon per type. +3. You select two pokemon from each type to be your chosen pokemon for the battle +4. You click Ready to battle. + +### What we are going to do? + +The lead developer on the team has structured how the screen should work: + +- **components/Screen** - manages the state selectedPokemonTypes & selectedPokemonForBattle and handles the updating of those variables. +- **components/Form** - fetches all the pokemon types and displays a form where the player can select up to four types and then click get pokemon to update the screens state. +- **components/PokemonOptions** - fetches the pokemon for the type it has been given, handles the selection of only 2 in each group and then updates the screens state. +- **shared/hooks/usePokedex** - There is a reuseable react hook that we can already use in the form/pokemon options to display the data that we want to display. The pokedex has two apis known as "types" and "cards" api. + +## Issues + +Any issues or improvements to the course please raise [here](https://github.com/code-mattclaffey/react-design-patterns/issues/new). diff --git a/src/shared/hooks/usePokedex.ts b/src/shared/hooks/usePokedex.ts new file mode 100644 index 0000000..5b2062a --- /dev/null +++ b/src/shared/hooks/usePokedex.ts @@ -0,0 +1,84 @@ +// define the api types like a generic + +import { useEffect, useReducer } from 'react'; + +export type TPokemonCardsApiResponse = { + id: string; + name: string; + images: { + small: string; + }; +}; + +export type TPokemonTypesApiResponse = string; + +type TTypesApi = { + path: 'types'; + skip?: boolean; + queryParams?: string; +}; + +type TCardsApi = { + path: 'cards'; + queryParams?: string; + skip?: boolean; +}; + +interface IUsePokedexState { + data?: TResponse; + isError?: boolean; + isLoading?: boolean; +} + +const usePokedexReducer = ( + state: IUsePokedexState, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + action: { type: string; payload?: any } +): IUsePokedexState => { + switch (action.type) { + case 'SUCCESS': + return { ...state, isLoading: false, data: action.payload }; + case 'LOADING': + return { ...state, isLoading: true }; + case 'ERROR': + return { ...state, isLoading: false }; + default: + return state; + } +}; + +export const usePokedex = ({ + path, + queryParams = '', + skip = false +}: TCardsApi | TTypesApi): IUsePokedexState => { + const [state, dispatch] = useReducer(usePokedexReducer, { + isError: false, + isLoading: false, + data: undefined + }); + + useEffect(() => { + if (skip) return; + + const getData = async () => { + try { + dispatch({ type: 'LOADING' }); + const response = await fetch( + `https://api.pokemontcg.io/v2/${path}?${queryParams}` + ); + const json = await response.json(); + + dispatch({ type: 'SUCCESS', payload: json.data }); + } catch (e) { + dispatch({ type: 'ERROR' }); + + console.error('Error'); + } + }; + + getData(); + }, [skip, path, queryParams]); + + return state; +};