This is a game that I am building and in this game I have created some levels. But when the game ends for the first time the second time my countdown timer is not getting decremented.
this is my App.js component:
import "./App.css";
import React, { useState, useEffect, useCallback } from "react";
import SingleCard from "./components/singleCard/SingleCard";
import Timer from "./components/timer/Timer";
import Modal from "./components/modal/Modal";
import soundOn from "./images/soundon.png";
import soundOff from "./images/soundoff.png";
import { Helmet } from "react-helmet";
const cardImages = [
{ src: "/img/img-1.png", matched: false },
{ src: "/img/img-2.png", matched: false },
{ src: "/img/img-3.png", matched: false },
{ src: "/img/img-4.png", matched: false },
{ src: "/img/img-5.png", matched: false },
{ src: "/img/img-6.png", matched: false },
];
function App({ background, wrongAns, correctAns, deadSound, winSound }) {
const [cards, setCards] = useState([]);
const [turns, setTurns] = useState(0);
const [choiceOne, setChoiceOne] = useState(null);
const [choiceTwo, setChoiceTwo] = useState(null);
const [disabled, setDisabled] = useState(false);
const [isgameEnd, setGameEnd] = useState(false);
const [timerStart, setTimerStart] = useState(false);
const [playSound, setPlaySound] = useState(false);
const [count, setCount] = useState(0);
const [IsPlaying, setIsPlaying] = useState(true);
const [isModalOpen, setModalIsOpen] = useState(false);
const [restartTimer, setRestartTimer] = useState(false);
const [isMute, setMute] = useState(false);
const [loading, setLoading] = useState(true);
function handleMute(state = false) {
background.muted = state;
wrongAns.muted = state;
correctAns.muted = state;
deadSound.muted = state;
winSound.muted = state;
setMute(state);
}
let timer;
// function that will decide the condition for opening the modal
const toggleModal = () => {
setModalIsOpen(true);
};
// function that will execute when we click a button in the modal
const handlePlaySound = () => {
setPlaySound(false);
};
// function that will execute when game is set to background in android
function AudioBgOnPause() {
if (playSound === true) {
background.pause();
setIsPlaying(false);
}
}
// functiona that will execute when game is again resumed
function AudioBgOnResume() {
if (IsPlaying === false) {
setIsPlaying(true);
}
}
// creating there global reference so that we can call these functions in the index file
window.AudioBgOnPause = AudioBgOnPause;
window.AudioBgOnResume = AudioBgOnResume;
// check if playSound is off or on
if (playSound === false) {
background.pause();
} else if (playSound === true && IsPlaying === true) {
background.play();
}
// Play Again
const playAgain = () => {
// setCards([]);
shuffleCards();
setTurns(0);
setChoiceOne(null);
setChoiceTwo(null);
setDisabled(false);
setGameEnd(false);
setTimerStart(false);
setPlaySound(false);
setCount(0);
setIsPlaying(true);
setModalIsOpen(false);
setRestartTimer(true);
setMute(false);
};
const restartGame = () => {
playAgain();
};
// check if isGameEnd is true i.e. the game is ended
// losing condition
useEffect(() => {
if (turns < 6 && isgameEnd === true) {
setDisabled(true);
setTimerStart(false);
clearInterval(timer);
if (playSound === true) {
deadSound.play();
}
setPlaySound(false);
setTimeout(() => {
toggleModal();
}, 2000);
}
}, [turns, isgameEnd]);
// winning situation
useEffect(() => {
if (
(turns === 6 && isgameEnd === false) ||
(turns === 6 && isgameEnd === true)
) {
// clearInterval(timer);
// setDisabled(true);
setRestartTimer(true);
setTimerStart(false);
if (playSound === true) {
winSound.play();
}
setPlaySound(playSound);
shuffleCards();
// setTimeout(() => {
// toggleModal();
// }, 2000);
}
}, [turns, isgameEnd]);
// shuffle Cards
const shuffleCards = () => {
const shuffleCards = [...cardImages, ...cardImages]
.sort(() => Math.random() - 0.5)
.map((card) => ({ ...card, id: Math.random() }));
setCards(shuffleCards);
setTurns(0);
};
// console.log("cards array", cards);
// handle a choice
const handleChoice = (card) => {
setTimerStart(true);
// background.play();
background.loop = true;
// checking if the counter is one only then set sound to true when the card is flipped for first time
count === 1 ? setPlaySound(true) : setPlaySound(playSound);
// after that increment the counter so that the upper condition should not hold anymore
setCount(count + 1);
choiceOne ? setChoiceTwo(card) : setChoiceOne(card);
};
// compare 2 selected cards
useEffect(() => {
if (choiceOne && choiceTwo) {
setDisabled(true);
if (choiceOne.src === choiceTwo.src) {
setCards((prevCards) => {
return prevCards.map((card) => {
if (card.src === choiceOne.src) {
return { ...card, matched: true };
} else {
return card;
}
});
});
if (playSound === true) {
correctAns.play();
}
setTurns((prevTurns) => prevTurns + 1);
resetTurn();
} else {
if (playSound === true) {
wrongAns.play();
}
setTimeout(() => resetTurn(), 500);
}
}
}, [choiceOne, choiceTwo]);
// start a new game automatically
// set counter to one when the component first mounts so that sound starts to play on first click only
useEffect(() => {
shuffleCards();
setCount(count + 1);
}, []);
// reset choices
const resetTurn = () => {
setChoiceOne(null);
setChoiceTwo(null);
setDisabled(false);
};
// console.log("restart App", restartTimer);
// timer callback
const onGameEnd = useCallback(() => {
setGameEnd(!isgameEnd);
}, [isgameEnd]);
useEffect(() => {
setTimeout(() => {
setLoading(false);
}, 4000);
}, []);
return (
<>
<Helmet>
<meta charSet="utf-8" />
<title>Match Maker</title>
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"
/>
</Helmet>
<div className="App">
{loading && (
<div className="loader-container">
<div className="spinner"></div>
</div>
)}
<>
{/* <img
className="logo"
src="https://cheetay.pk/static/images/newLandingPage/logo.svg"
alt="card back"
/> */}
<div
style={
loading ? { visibility: "hidden" } : { visibility: "inherit" }
}
>
<div className="soundBtn">
{!isMute === true ? (
<div
className="soundIcon"
style={{
cursor: "pointer",
khtmlUserSelect: "none",
MozUserSelect: "none",
OUserSelect: "none",
userSelect: "none",
}}
onClick={() => handleMute(!isMute)}
>
<img src={soundOn} alt="soundOff" />
</div>
) : (
<div
className="soundIcon"
style={{
cursor: "pointer",
khtmlUserSelect: "none",
MozUserSelect: "none",
OUserSelect: "none",
userSelect: "none",
}}
onClick={() => handleMute(!isMute)}
>
<img src={soundOff} alt="soundOff" />
</div>
)}
</div>
<div className="card-grid">
{cards.map((card) => (
<SingleCard
key={card.id}
card={card}
handleChoice={handleChoice}
flipped={
card === choiceOne || card === choiceTwo || card.matched
}
disabled={disabled}
isModalOpen={isModalOpen}
/>
))}
</div>
<div className="TimerAndTurnsInfo">
<Timer
timerStart={timerStart}
timer={timer}
onGameEnd={onGameEnd}
restartTimer={restartTimer}
/>
<p>matched {turns}</p>
</div>
</div>
</>
</div>
{isModalOpen && (
<Modal handlePlaySound={handlePlaySound} restartGame={restartGame} />
)}
</>
);
}
export default App;
and this is my timer component:
import React, { useEffect, useState } from "react";
import "./Timer.css";
const Child = ({ timerStart, timer, onGameEnd, restartTimer }) => {
const [seconds, setSeconds] = useState(40);
// let time = 40;
useEffect(() => {
if (restartTimer === true) {
setSeconds(40);
}
}, [seconds, restartTimer]);
// console.log("restart Timer", restartTimer);
useEffect(() => {
if (timerStart === true) {
timer = setInterval(() => {
if (seconds > 0) {
setSeconds(seconds - 1);
}
if (seconds === 0) {
onGameEnd(true);
clearInterval(timer);
}
}, 1000);
}
return () => clearInterval(timer);
});
return (
<p className="time">
Time{" "}
<span className="span1">
<span>{seconds}s</span>
</span>
</p>
);
};
const Timer = React.memo(({ timerStart, timer, onGameEnd, restartTimer }) => (
<Child
timerStart={timerStart}
timer={timer}
onGameEnd={onGameEnd}
restartTimer={restartTimer}
/>
));
export default Timer;
my timer gets re initialized when restartTimer state is set to true.
I think your problem is here
useEffect(() => {
if (restartTimer === true) {
setSeconds(40);
}
}, [seconds, restartTimer]);
whenever the seconds reduce and restartTimer === true (can't find where you set it to false after useState) you reset it to 40.
try removing seconds from the dependency
Related
How can I make other filter button disappear when picked 1 (or multiple) value in same filter block.
Here is my code base:
const FilterBlock = props => {
const {
filterApi,
filterState,
filterFrontendInput,
group,
items,
name,
onApply,
initialOpen
} = props;
const { formatMessage } = useIntl();
const talonProps = useFilterBlock({
filterState,
items,
initialOpen
});
const { handleClick, isExpanded } = talonProps;
const classStyle = useStyle(defaultClasses, props.classes);
const ref = useRef(null);
useEffect(() => {
const handleClickOutside = event => {
if (ref.current && !ref.current.contains(event.target)) {
isExpanded && handleClick();
}
};
document.addEventListener('click', handleClickOutside, true);
return () => {
document.removeEventListener('click', handleClickOutside, true);
};
}, [isExpanded]);
const list = isExpanded ? (
<Form>
<FilterList
filterApi={filterApi}
filterState={filterState}
name={name}
filterFrontendInput={filterFrontendInput}
group={group}
items={items}
onApply={onApply}
/>
</Form>
) : null;
return (
<div
data-cy="FilterBlock-root"
aria-label={itemAriaLabel}
ref={ref}
>
<Menu.Button
data-cy="FilterBlock-triggerButton"
type="button"
onClick={handleClick}
aria-label={toggleItemOptionsAriaLabel}
>
<div>
<span>
{name}
</span>
<svg
width="8"
height="5"
viewBox="0 0 8 5"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M6.97291 0.193232C7.20854"
fill="currentColor"
/>
</svg>
</div>
</Menu.Button>
<div>
<div>
{list}
</div>
</div>
</div>
);
};
I am trying to achieve when I chose 1 value or more than 1 value inside filter block the other block will disappear but right now I achieved that when I chose 1 value the other filter block disappear but when I chose another value in the same block the other filter block appear again. Anyone have idea how can I work on this?
I am using React and Redux for this project
Thank you for helping me on this!!!!
Update:
Added parent component for FilterBlock.ks:
const FilterSidebar = props => {
const { filters, filterCountToOpen } = props;
const [selectedGroup, setSelectedGroup] = useState(null);
const talonProps = useFilterSidebar({ filters });
const {
filterApi,
filterItems,
filterNames,
filterFrontendInput,
filterState,
handleApply,
handleReset
} = talonProps;
const filterRef = useRef();
const classStyle = useStyle(defaultClasses, props.classes);
const handleApplyFilter = useCallback(
(...args) => {
const filterElement = filterRef.current;
if (
filterElement &&
typeof filterElement.getBoundingClientRect === 'function'
) {
const filterTop = filterElement.getBoundingClientRect().top;
const windowScrollY =
window.scrollY + filterTop - SCROLL_OFFSET;
window.scrollTo(0, windowScrollY);
}
handleApply(...args);
},
[handleApply, filterRef]
);
const result = Array.from(filterItems)
.filter(
([group, items]) =>
selectedGroup === null ||
selectedGroup === filterNames.get(group)
)
.map(([group, items], iteration) => {
const blockState = filterState.get(group);
const groupName = filterNames.get(group);
const frontendInput = filterFrontendInput.get(group);
return (
<FilterBlock
key={group}
filterApi={filterApi}
filterState={blockState}
filterFrontendInput={frontendInput}
group={group}
items={items}
name={groupName}
onApply={(...args) => {
console.log('args: ', ...args);
setSelectedGroup(prev =>
prev !== null ? null : groupName
);
return handleApplyFilter(...args);
}}
initialOpen={iteration < filterCountToOpen}
iteration={iteration}
/>
);
});
return (
<div className="container px-4 mx-auto">
<Menu
as="div"
className="my-16 justify-center flex flex-wrap py-5 border-y border-black border-opacity-5"
>
{result}
</Menu>
</div>
);
};
Updated added useFilterSideBar.js:
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useQuery } from '#apollo/client';
import { useHistory, useLocation } from 'react-router-dom';
import { useAppContext } from '#magento/peregrine/lib/context/app';
import mergeOperations from '../../util/shallowMerge';
import { useFilterState } from '../FilterModal';
import {
getSearchFromState,
getStateFromSearch,
sortFiltersArray,
stripHtml
} from '../FilterModal/helpers';
import DEFAULT_OPERATIONS from '../FilterModal/filterModal.gql';
const DRAWER_NAME = 'filter';
export const useFilterSidebar = props => {
const { filters } = props;
const operations = mergeOperations(DEFAULT_OPERATIONS, props.operations);
const { getFilterInputsQuery } = operations;
const [isApplying, setIsApplying] = useState(false);
const [{ drawer }, { toggleDrawer, closeDrawer }] = useAppContext();
const [filterState, filterApi] = useFilterState();
const prevDrawer = useRef(null);
const isOpen = drawer === DRAWER_NAME;
const history = useHistory();
const { pathname, search } = useLocation();
const { data: introspectionData } = useQuery(getFilterInputsQuery);
const attributeCodes = useMemo(
() => filters.map(({ attribute_code }) => attribute_code),
[filters]
);
// Create a set of disabled filters.
const DISABLED_FILTERS = useMemo(() => {
const disabled = new Set();
// Disable category filtering when not on a search page.
if (pathname !== '/search.html') {
disabled.add('category_id');
disabled.add('category_uid');
}
return disabled;
}, [pathname]);
// Get "allowed" filters by intersection of filter attribute codes and
// schema input field types. This restricts the displayed filters to those
// that the api will understand.
const possibleFilters = useMemo(() => {
const nextFilters = new Set();
const inputFields = introspectionData
? introspectionData.__type.inputFields
: [];
// perform mapping and filtering in the same cycle
for (const { name } of inputFields) {
const isValid = attributeCodes.includes(name);
const isEnabled = !DISABLED_FILTERS.has(name);
if (isValid && isEnabled) {
nextFilters.add(name);
}
}
return nextFilters;
}, [DISABLED_FILTERS, attributeCodes, introspectionData]);
const isBooleanFilter = options => {
const optionsString = JSON.stringify(options);
return (
options.length <= 2 &&
(optionsString.includes(
JSON.stringify({
__typename: 'AggregationOption',
label: '0',
value: '0'
})
) ||
optionsString.includes(
JSON.stringify({
__typename: 'AggregationOption',
label: '1',
value: '1'
})
))
);
};
// iterate over filters once to set up all the collections we need
const [
filterNames,
filterKeys,
filterItems,
filterFrontendInput
] = useMemo(() => {
const names = new Map();
const keys = new Set();
const frontendInput = new Map();
const itemsByGroup = new Map();
const sortedFilters = sortFiltersArray([...filters]);
for (const filter of sortedFilters) {
const { options, label: name, attribute_code: group } = filter;
// If this aggregation is not a possible filter, just back out.
if (possibleFilters.has(group)) {
const items = [];
// add filter name
names.set(group, name);
// add filter key permutations
keys.add(`${group}[filter]`);
// TODO: Get all frontend input type from gql if other filter input types are needed
// See https://github.com/magento-commerce/magento2-pwa/pull/26
if (isBooleanFilter(options)) {
frontendInput.set(group, 'boolean');
// add items
items.push({
title: 'No',
value: '0',
label: name + ':' + 'No'
});
items.push({
title: 'Yes',
value: '1',
label: name + ':' + 'Yes'
});
} else {
// Add frontend input type
frontendInput.set(group, null);
// add items
for (const { label, value } of options) {
items.push({ title: stripHtml(label), value });
}
}
itemsByGroup.set(group, items);
}
}
return [names, keys, itemsByGroup, frontendInput];
}, [filters, possibleFilters]);
// on apply, write filter state to location
useEffect(() => {
if (isApplying) {
const nextSearch = getSearchFromState(
search,
filterKeys,
filterState
);
// write filter state to history
history.push({ pathname, search: nextSearch });
// mark the operation as complete
setIsApplying(false);
}
}, [filterKeys, filterState, history, isApplying, pathname, search]);
const handleOpen = useCallback(() => {
toggleDrawer(DRAWER_NAME);
}, [toggleDrawer]);
const handleClose = useCallback(() => {
closeDrawer();
}, [closeDrawer]);
const handleApply = useCallback(() => {
setIsApplying(true);
handleClose();
}, [handleClose]);
const handleReset = useCallback(() => {
filterApi.clear();
setIsApplying(true);
}, [filterApi, setIsApplying]);
const handleKeyDownActions = useCallback(
event => {
// do not handle keyboard actions when the modal is closed
if (!isOpen) {
return;
}
switch (event.keyCode) {
// when "Esc" key fired -> close the modal
case 27:
handleClose();
break;
}
},
[isOpen, handleClose]
);
useEffect(() => {
const justOpened =
prevDrawer.current === null && drawer === DRAWER_NAME;
const justClosed =
prevDrawer.current === DRAWER_NAME && drawer === null;
// on drawer toggle, read filter state from location
if (justOpened || justClosed) {
const nextState = getStateFromSearch(
search,
filterKeys,
filterItems
);
filterApi.setItems(nextState);
}
// on drawer close, update the modal visibility state
if (justClosed) {
handleClose();
}
prevDrawer.current = drawer;
}, [drawer, filterApi, filterItems, filterKeys, search, handleClose]);
useEffect(() => {
const nextState = getStateFromSearch(search, filterKeys, filterItems);
filterApi.setItems(nextState);
}, [filterApi, filterItems, filterKeys, search]);
return {
filterApi,
filterItems,
filterKeys,
filterNames,
filterFrontendInput,
filterState,
handleApply,
handleClose,
handleKeyDownActions,
handleOpen,
handleReset,
isApplying,
isOpen
};
};
Update FilterList component:
const FilterList = props => {
const {
filterApi,
filterState,
filterFrontendInput,
name,
group,
itemCountToShow,
items,
onApply,
toggleItemOptionsAriaLabel
} = props;
const classes = useStyle(defaultClasses, props.classes);
const talonProps = useFilterList({ filterState, items, itemCountToShow });
const { isListExpanded, handleListToggle } = talonProps;
const { formatMessage } = useIntl();
// memoize item creation
// search value is not referenced, so this array is stable
const itemElements = useMemo(() => {
if (filterFrontendInput === 'boolean') {
const key = `item-${group}`;
return (
<li
key={key}
className={classes.item}
data-cy="FilterList-item"
>
<FilterItemRadioGroup
filterApi={filterApi}
filterState={filterState}
group={group}
name={name}
items={items}
onApply={onApply}
labels={labels}
/>
</li>
);
}
return items.map((item, index) => {
const { title, value } = item;
const key = `item-${group}-${value}`;
if (!isListExpanded && index >= itemCountToShow) {
return null;
}
// create an element for each item
const element = (
<li
key={key}
className={classes.item}
data-cy="FilterList-item"
>
<FilterItem
filterApi={filterApi}
filterState={filterState}
group={group}
item={item}
onApply={onApply}
/>
</li>
);
// associate each element with its normalized title
// titles are not unique, so use the element as the key
labels.set(element, title.toUpperCase());
return element;
});
}, [
classes,
filterApi,
filterState,
filterFrontendInput,
name,
group,
items,
isListExpanded,
itemCountToShow,
onApply
]);
const showMoreLessItem = useMemo(() => {
if (items.length <= itemCountToShow) {
return null;
}
const label = isListExpanded
? formatMessage({
id: 'filterList.showLess',
defaultMessage: 'Show Less'
})
: formatMessage({
id: 'filterList.showMore',
defaultMessage: 'Show More'
});
return (
<li className={classes.showMoreLessItem}>
<button
onClick={handleListToggle}
className="text-sm hover_text-indigo-500 transition-colors duration-sm"
data-cy="FilterList-showMoreLessButton"
>
{label}
</button>
</li>
);
}, [
isListExpanded,
handleListToggle,
items,
itemCountToShow,
formatMessage,
classes
]);
return (
<Fragment>
<ul className={classes.items}>
{itemElements}
{showMoreLessItem}
</ul>
</Fragment>
);
};
FilterList.defaultProps = {
onApply: null,
itemCountToShow: 5
};
Update FilterRadioGroup:
const FilterItemRadioGroup = props => {
const { filterApi, filterState, group, items, onApply, labels } = props;
const radioItems = useMemo(() => {
return items.map(item => {
const code = `item-${group}-${item.value}`;
return (
<FilterItemRadio
key={code}
filterApi={filterApi}
filterState={filterState}
group={group}
item={item}
onApply={onApply}
labels={labels}
/>
);
});
}, [filterApi, filterState, group, items, labels, onApply]);
const fieldValue = useMemo(() => {
if (filterState) {
for (const item of items) {
if (filterState.has(item)) {
return item.value;
}
}
}
return null;
}, [filterState, items]);
const field = `item-${group}`;
const fieldApi = useFieldApi(field);
const fieldState = useFieldState(field);
useEffect(() => {
if (field && fieldValue === null) {
fieldApi.reset();
} else if (field && fieldValue !== fieldState.value) {
fieldApi.setValue(fieldValue);
}
}, [field, fieldApi, fieldState.value, fieldValue]);
return (
<RadioGroup field={field} data-cy="FilterDefault-radioGroup">
{radioItems}
</RadioGroup>
);
};
FilterItemRadioGroup.defaultProps = {
onApply: null
};
The code seems overly complicated for what it does.
Could you perhaps make the filter block a state object like this:
filterGroup = {
title: "",
hasSelectedItems: boolean,
filterItems: []
}
Then it would be a simple matter of checking hasSelectedItems to conditionally render only one filterGroup in the UI.
I apologize I don't have the time to go in depth and try out your code, but I would put the filterBlock rendering inside the return statement and do something like this:
return(
{ (!!filterValue && (filterGroup === selectedGroup))
&&
<FilterGroup />
}
);
I find the best results come from using my state values as conditions inside my return statement.
That way, I don't have to manage complex logic in the .map() functions or with useEffect.
It looks like FilterSideBar is where you build the the array of FilterBlocks.
According to your question, I think you want to display all filter blocks if NO filters are selected, but once a filter is selected, you want the other FilterBlocks to not render, correct?
So, in your FilterSidebar module, I'd modify your "result" variable to include a check to see if anything has already been selected.
If something has been selected, then only render the FilterBlock that corresponds to that selected list item.
const result = Array.from(filterItems)
.filter(
([group, items]) =>
selectedGroup === null ||
selectedGroup === filterNames.get(group)
)
.map(([group, items], iteration) => {
const blockState = filterState.get(group);
const groupName = filterNames.get(group);
const frontendInput = filterFrontendInput.get(group);
return (
// This is the conditional rendering
(!!blockState.filterGroupSelected && blockState.filterGroupSelected === group) ?
<FilterBlock
key={group}
...
/>
: null
);
});
Then you'll have to add an item to your state object (unless you already have it) to track the group that was selected (e.g. "Printer Type"), and only allow that one to render.
If no group has a selected filter item, they should all render.
Also, you'll have to be sure to clear the selectedGroup whenever there are no selected items.
If this does not work, it may be that your state changes are not triggering a re-render. In which case it can be as simple as adding a reference to your state object in your component's return method like this:
{blockState.filterGroupSelected && " " }
It's a hack but it works, and keeps you from adding calls to useEffect.
Here is my codesandbox link. https://codesandbox.io/s/bold-lederberg-d2h508?file=/src/Components/Products/Products.js In this filtering when i click highest price its showing correcly and when i again click all filters how can i set back to its original state. please provide any solution for this.
import React, { useState } from "react";
import Products from "./Components/Products/Products";
import SearchInput from "./Components/SearchInput/SearchInput";
import data from "./utils/data";
const App = () => {
const [searchValue, setSearchValue] = useState("");
const [productsInfo, setProductsInfo] = useState([]);
const handleChange = (e) => {
setSearchValue(e.target.value);
};
const selectedChangeFilter = (e) => {
const { value } = e.target;
if (value === "sporting goods") {
const sportingGoods = data.filter(
(product) => product.category === "Sporting Goods"
);
setProductsInfo(sportingGoods);
}
if (value === "electronics") {
const electronicsGoods = data.filter(
(product) => product.category === "Electronics"
);
setProductsInfo(electronicsGoods);
}
if (value === "lowest price") {
const lowestPriceGoods = data.sort((el1, el2) =>
el1.price.localeCompare(el2.price, undefined, { numeric: true })
);
setProductsInfo([...lowestPriceGoods]);
}
if (value === "highest price") {
const highestPriceGoods = data.sort((el1, el2) =>
el2.price.localeCompare(el1.price, undefined, { numeric: true })
);
setProductsInfo([...highestPriceGoods]);
}
if (value === "all") {
setProductsInfo(data);
}
};
const searchProducts = (products) => {
if (searchValue.toLowerCase().trim() === "") {
setProductsInfo(products);
} else {
const seekedItem = productsInfo.filter(
(product) =>
product.name.toLowerCase().trim().includes(searchValue) ||
product.category.toLowerCase().trim().includes(searchValue)
);
setProductsInfo(seekedItem);
}
};
return (
<div>
<SearchInput
handleChange={handleChange}
searchValue={searchValue}
selectedChangeFilter={selectedChangeFilter}
/>
<Products
data={data}
searchValue={searchValue}
productsInfo={productsInfo}
searchProducts={searchProducts}
/>
</div>
);
};
export default App;
I try this but it doesn't work
if (value === "all") {
const copiedData = [...data]
setProductsInfo(copiedData);
}
Best regards
I wrote a component that uses Player from #vimeo/player but now I am unsure how to write tests that are not focused on implementation details. I am using testing-library.
Specifically, I am looking for a way to test state changes that stem from these events (loaded, bufferstart, bufferend, play, pause):
useEffect(() => {
async function setupPlayer() {
if (playerRef.current) {
playerRef.current.on('loaded', () => setIsLoading(false));
playerRef.current.on('bufferstart', () => setIsBuffering(true));
playerRef.current.on('bufferend', () => setIsBuffering(false));
playerRef.current.on('play', () => setIsPlaying(true));
playerRef.current.on('pause', () => setIsPlaying(false));
}
}
if (playerRef.current) {
playerRef.current = new Player(
playerRef.current,
{ url: `https://player.vimeo.com/video/${video}`,
loop: true,
title: false,
portrait: false,
controls: false,
byline: false,
muted: true,
responsive: true },
);
setupPlayer();
}
return () => playerRef.current && playerRef.current.destroy();
}, [playerRef]);
My goal is to test for example that a loading/buffering has been rendered until loaded/bufferend events are fired.
/* eslint-disable max-len */
import React, { useEffect, useRef, useState } from 'react';
import Button from 'button';
import { IconScan, IconPause, IconPlay } from 'iconography';
import Loader from 'loader';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import Player from '#vimeo/player';
const VIDEO_PLAYER_INDEX = 2;
const VideoPlayer = ({ wrapperEl,
video,
style,
wrapperClassNames,
renderedInOverlay,
onSetOverlay,
isMobile }) => {
const playerRef = useRef();
const [isLoading, setIsLoading] = useState(true);
const [isPlaying, setIsPlaying] = useState(false);
const [isHovering, setIsHovering] = useState(true);
const [isBuffering, setIsBuffering] = useState(false);
const [width, setWidth] = useState(0);
const [height, setHeight] = useState(0);
useEffect(() => {
async function setupPlayer() {
if (playerRef.current) {
playerRef.current.on('loaded', (e) => {
setIsLoading(false);
console.log(e);
});
playerRef.current.on('bufferstart', () => setIsBuffering(true));
playerRef.current.on('bufferend', () => setIsBuffering(false));
playerRef.current.on('play', () => setIsPlaying(true));
playerRef.current.on('pause', () => setIsPlaying(false));
}
}
if (playerRef.current) {
playerRef.current = new Player(
playerRef.current,
{ url: `https://player.vimeo.com/video/${video}`,
loop: true,
title: false,
portrait: false,
controls: false,
byline: false,
muted: true,
responsive: true },
);
setupPlayer();
}
return () => playerRef.current && playerRef.current.destroy();
}, [playerRef]);
useEffect(() => {
if (isMobile && isPlaying && !isBuffering) {
setIsHovering(false);
}
}, [isPlaying, isMobile, isBuffering]);
useEffect(() => {
if (playerRef.current) {
Promise.all([playerRef.current.getVideoWidth(), playerRef.current.getVideoHeight()]).then((dimensions) => {
setWidth(dimensions[0]);
setHeight(dimensions[1]);
});
}
}, [isMobile, playerRef]);
const handlePlayClick = () => {
if (isPlaying) {
playerRef.current.pause();
} else if (!isPlaying && !isBuffering) {
playerRef.current.play();
}
};
const renderIframe = () => {
if (!video) return null;
const showControls = isHovering || !isPlaying || isBuffering;
const showPlay = isPlaying && !isBuffering;
const aspectRatio = (width / height) * 100;
const wrapperBtnStyle = renderedInOverlay ? { style: { maxWidth: `${aspectRatio}vh` } } : {};
return (
<div className={ classNames('video-player', { 'video-player--is-loading': isLoading }) }>
<Button
className="video-player__wrapper-btn"
variant="naked"
onClick={ () => handlePlayClick() }
onMouseEnter={ () => setIsHovering(true) }
onMouseLeave={ () => setIsHovering(false) }
{ ...wrapperBtnStyle }>
<div
ref={ playerRef }
className="video-player__player"
data-testid="video-player"
/>
{ showControls &&
<div className="video-player__controls-container">
<div className="video-player__controls-btn" >
{showPlay ?
<IconPause size="lg" title="Pause" className="video-player__control-icon" />
:
<IconPlay size="lg" title="Play" className="video-player__control-icon" />
}
</div>
{isBuffering && (
<div className="video-player__buffer-container">
<div className="video-player__buffer-indicator" />
</div>)}
</div>
}
</Button>
{ !renderedInOverlay &&
<div className="video-player__fullscreen-btn-container">
<Button
className="video-player__fullscreen-btn"
variant="naked"
onClick={ (e) => onSetOverlay(e, false, VIDEO_PLAYER_INDEX) }
onTouchStart={ (e) => onSetOverlay(e, false, VIDEO_PLAYER_INDEX) }>
<IconScan size="lg" title="Fullscreen" />
</Button>
</div>
}
<Loader className="video-player__loader" height={ 30 } />
</div>
);
};
return (
wrapperEl ?
React.createElement(
wrapperEl,
{
className: classNames(wrapperClassNames),
style,
},
renderIframe(),
)
: renderIframe()
);
};
VideoPlayer.defaultProps = {
video: '',
wrapperEl: '',
wrapperClassNames: '',
style: {},
renderedInOverlay: false,
isMobile: false,
};
VideoPlayer.propTypes = {
video: PropTypes.string.isRequired,
wrapperEl: PropTypes.string,
wrapperClassNames: PropTypes.string,
style: PropTypes.shape({}),
renderedInOverlay: PropTypes.bool,
isMobile: PropTypes.bool,
};
export {
VideoPlayer,
VIDEO_PLAYER_INDEX,
};
Is someone able to give me a pointer how I can achieve that? Thanks!
I am trying to figure out how to implement useReducer in my stopwatch app that I made a while ago that uses setState. I am having lots of trouble implementing it, and I feel like my confusion centers around how the reducer function actually works.
The major problem that I am having is that I am not sure how the reducer function actually works, as I am having trouble implementing the logic for the timer buttons. And I am assuming that I should be putting the logic in the switch statement.
Here is my actual code:
import React, { useReducer } from "react"
function init(initialState) {
return { time: initialState }
}
let initialState = {
timerOn: true,
timerStart: 0,
timerTime: 0
};
function reducer(state, action) {
switch (action.type) {
case 'start timer':
return startTimer()
case 'reset timer':
return zerotimer(initialState)
case 'resume timer':
return
default:
return state
}
}
function zerotimer(initialState) {
return { initialState };
}
function startTimer(initialState) {
this.timer = setInterval(() => {
this.setState({
timerTime: Date.now() - this.state.timerStart
});
}, 10);
}
function Timer({ initialState }) {
const [state, dispatch] = useReducer(reducer, initialState)
let centiseconds = (Math.floor(initialState / 10) % 100);
let seconds = (Math.floor(initialState / 1000) % 60);
let minutes = (Math.floor(initialState / 60000) % 60);
let hours = (Math.floor(initialState / 3600000));
return (
<div>
<div>
<div>
<h2>Time to run!</h2>
</div>
<div>Stopwatch</div>
<div>
{hours} : {minutes} : {seconds} : {centiseconds}
</div>
{state.timerOn === false && state.timerTime === 0 && (
<button onClick={() => dispatch({ type: 'start timer' })}>Start</button>
)}
{state.timerOn === true && (
<button onClick={this.stopTimer}>Stop</button>
)}
{state.timerOn === false && state.timerTime > 0 && (
<button onClick={this.startTimer}>Resume</button>
)}
{state.timerOn === false && state.timerTime > 0 && (
<button onClick={() => dispatch({ type: 'reset timer' })}>Reset</button>
)}
</div>
</div>
)
}
export default Timer;
Here's a minimal implementation using useState first, then below I'll cover the useReducer case.
I dropped the hours, and removed a few other things to make it simpler.
Using useState :
// import React from "react";
import React, { useState, useRef, useEffect, useReducer } from "react";
import ReactDOM from "react-dom";
function Timer(props) {
const [centiseconds, setCentiseconds] = useState(0);
const [seconds, setSeconds] = useState(0);
const [minutes, setMinutes] = useState(0);
const [timerOn, setTimerOn] = useState(false);
useInterval(() => {
if (!timerOn) return;
setCentiseconds((centiseconds) =>
centiseconds === 99 ? 0 : centiseconds + 1
);
}, 10);
useInterval(() => {
if (!timerOn) return;
setSeconds((seconds) => (seconds === 59 ? 0 : seconds + 1));
}, 1000);
useInterval(() => {
if (!timerOn) return;
setMinutes((minutes) => (minutes === 59 ? 0 : minutes + 1));
}, 60000);
return (
<>
<div>
{String(minutes).padStart(2, "0")} : {String(seconds).padStart(2, "0")}{" "}
: {String(centiseconds).padStart(2, "0")}
</div>
{timerOn === false && (
<button onClick={() => setTimerOn(true)}>Start</button>
)}
{timerOn === true && (
<button onClick={() => setTimerOn(false)}>Stop</button>
)}
{
<button
onClick={() => {
setTimerOn(false);
setCentiseconds(0);
setSeconds(0);
setMinutes(0);
}}
>
Reset
</button>
}
</>
);
}
// source: https://overreacted.io/making-setinterval-declarative-with-react-hooks/
function useInterval(callback, delay) {
const savedCallback = useRef();
// Remember the latest callback.
useEffect(() => {
savedCallback.current = callback;
}, [callback]);
// Set up the interval.
useEffect(() => {
function tick() {
savedCallback.current();
}
if (delay !== null) {
let id = setInterval(tick, delay);
return () => clearInterval(id);
}
}, [delay]);
}
ReactDOM.render(
<React.StrictMode>
<Timer />
</React.StrictMode>,
document.getElementById("root")
);
From here, it's pretty straightforward to convert it into useReducer.
What you need to do is replace the setState calls with actions, and handle the actions in the reducer.
It's honestly an overkill for this case, and it's too much boilerplate code, but here it is anyway.
Using useReducer:
import React, { useRef, useEffect, useReducer } from "react";
import ReactDOM from "react-dom";
const initialState = {
centiseconds: 0,
seconds: 0,
minutes: 0,
timerOn: false,
};
const reducer = (state, action) => {
switch (action.type) {
case "START":
return {
...state,
timerOn: true,
};
case "STOP":
return {
...state,
timerOn: false,
};
case "RESET":
return initialState;
case "SET_CENTISECONDS":
return {
...state,
centiseconds: action.payload.centiseconds,
};
case "SET_SECONDS":
return {
...state,
seconds: action.payload.seconds,
};
case "SET_MINUTES":
return {
...state,
minutes: action.payload.minutes,
};
default:
return initialState;
}
};
function Timer(props) {
const [state, dispatch] = useReducer(reducer, initialState);
const { centiseconds, seconds, minutes, timerOn } = state;
useInterval(() => {
if (!timerOn) return;
dispatch({
type: "SET_CENTISECONDS",
payload: {
centiseconds: centiseconds === 99 ? 0 : centiseconds + 1,
},
});
}, 10);
useInterval(() => {
if (!timerOn) return;
dispatch({
type: "SET_SECONDS",
payload: {
seconds: seconds === 59 ? 0 : seconds + 1,
},
});
}, 1000);
useInterval(() => {
if (!timerOn) return;
dispatch({
type: "SET_MINUTES",
payload: {
minutes: minutes === 59 ? 0 : minutes + 1,
},
});
}, 60000);
return (
<>
<div>
{String(minutes).padStart(2, "0")} : {String(seconds).padStart(2, "0")}{" "}
: {String(centiseconds).padStart(2, "0")}
</div>
{timerOn === false && (
<button onClick={() => dispatch({ type: "START" })}>Start</button>
)}
{timerOn === true && (
<button onClick={() => dispatch({ type: "STOP" })}>Stop</button>
)}
{<button onClick={() => dispatch({ type: "RESET" })}>Reset</button>}
</>
);
}
// source: https://overreacted.io/making-setinterval-declarative-with-react-hooks/
function useInterval(callback, delay) {
const savedCallback = useRef();
// Remember the latest callback.
useEffect(() => {
savedCallback.current = callback;
}, [callback]);
// Set up the interval.
useEffect(() => {
function tick() {
savedCallback.current();
}
if (delay !== null) {
let id = setInterval(tick, delay);
return () => clearInterval(id);
}
}, [delay]);
}
ReactDOM.render(
<React.StrictMode>
<Timer />
</React.StrictMode>,
document.getElementById("root")
);
This is not the only way to accomplish this. You can use useReducer however you want. You can pass the new state you want to be merged instead of passing an action, similar to setState in react classes. (check this article).
Here I'm working on AutoComplete and Auto fill of react.
I'm trying to convert it to react hooks as I have written all of my code is in hooks only.
I've to some level converted it to hooks based as per my understanding. But I'm not able to completely convert it.
Original code
import React, { Component } from "react";
class App extends Component {
constructor(props) {
super(props);
this.state = {
item: {
code: "",
name: "",
unit: "",
rate: ""
},
cursor: 0,
searchItems: []
};
this.autocomplete = this.autocomplete.bind(this);
this.handleKeyup = this.handleKeyup.bind(this);
this.handleKeydown = this.handleKeydown.bind(this);
this.handleListKeydown = this.handleListKeydown.bind(this);
this.selectItem = this.selectItem.bind(this);
this.handleChange = this.handleChange.bind(this);
}
autocomplete(evt) {
let text = evt.target.value;
fetch(`https://invoiceitems.herokuapp.com/items?name_like=${text}&_limit=6`)
.then((res) => res.json())
.then((data) => {
this.setState({ searchItems: data });
});
}
handleKeyup(evt) {
if (evt.keyCode === 27) {
this.setState({ searchItems: [] });
return false;
}
}
handleKeydown(evt) {
const { cursor, searchItems } = this.state;
// arrow up/down button should select next/previous list element
if (evt.keyCode === 38 && cursor > 0) {
this.setState((prevState) => ({
cursor: prevState.cursor - 1
}));
} else if (evt.keyCode === 40 && cursor < searchItems.length - 1) {
this.setState((prevState) => ({
cursor: prevState.cursor + 1
}));
}
if (evt.keyCode === 13) {
let currentItem = searchItems[cursor];
if (currentItem !== undefined) {
const { name, code, rate, unit } = currentItem;
this.setState({ item: { name, code, rate, unit }, searchItems: [] });
}
}
if (evt.keyCode === 8) {
this.setState({ item: { name: "", code: "", rate: "", unit: "" } });
}
}
selectItem(id) {
const { searchItems } = this.state;
let selectedItem = searchItems.find((item) => item.code === id);
const { code, name, unit, rate } = selectedItem;
this.setState({ item: { code, name, unit, rate } });
this.setState({ searchItems: [] });
}
handleListKeydown(evt) {
console.log(evt.keyCode);
}
handleChange(evt) {
this.setState({ item: { [evt.target.name]: evt.target.value } });
}
render() {
const { searchItems, cursor, item, handleChange } = this.state;
const { code, name, unit, rate } = item;
return (
<div className="container mt-3">
<h1 className="h2 text-center">Autocomplete Example</h1>
<div className="form-group">
<label htmlFor="autocomplete">Item Name </label>
<input
type="text"
id="autocomplete"
onChange={this.autocomplete}
onKeyUp={this.handleKeyup}
onKeyDown={this.handleKeydown}
value={name}
className="custom-input form-control"
/>
{searchItems.length > 0 && (
<ul className="list-group">
{searchItems.map((item, idx) => (
<li
className={
cursor === idx
? "active list-group-item"
: "list-group-item"
}
key={idx}
onClick={() => this.selectItem(item.code)}
onKeyDown={(evt) => this.handleListKeydown(evt, item.code)}
>
{item.name}
</li>
))}
</ul>
)}
</div>
</div>
);
}
}
export default App;
Link to original code: https://codepen.io/regexp/details/RwPNaLe
Using hooks
Here is the code that I tried to convert to hooks.
import React, { useState } from "react";
export default function FunctionName(props) {
const [item, setItem] = useState({
vendorNameData: invoiceDetail[0].invoiceData.vendor,
vendorAccountData: invoiceDetail[0].invoiceData.vendaAccount,
vendorAddressData: invoiceDetail[0].invoiceData.vendorAddress
});
const [cursor, setCursor] = useState(0);
const [searchItems, SetSearchItems] = useState([]);
function AutoComplete(evt) {
let text = evt.target.value;
console.log(text);
fetch(`https://invoiceitems.herokuapp.com/items?name_like=${text}&_limit=6`)
.then((res) => res.json())
.then((data) => {
SetSearchItems(data);
});
}
function HandleKeyUp(evt) {
if (evt.keyCode === 27) {
SetSearchItems([]);
return false;
}
}
function HandleKeyDown(evt) {
// const [cursor, setCursor] = useState();
// const [searchItems, SetSearchItems] = useState()
if (evt.keyCode === 38 && cursor > 0) {
setCursor((cursor) => ({ cursor: cursor + 1 }));
} else if (evt.keyCode === 40 && cursor < searchItems.length - 1) {
setCursor((cursor) => ({ cursor: cursor + 1 }));
}
if (evt.keyCode === 13) {
let currentItem = searchItems[cursor];
if (currentItem !== undefined) {
const {
vendorNameData,
vendorAccountData,
vendorAddressData
} = currentItem;
setItem({ vendorNameData, vendorAccountData, vendorAddressData });
SetSearchItems([]);
}
}
if (evt.keyCode === 8) {
setItem({
vendorNameData: "",
vendorAccountData: "",
vendorAddressData: ""
});
}
}
function SelectItem(id) {
const [searchItems, SetSearchItems] = useState();
let selectedItem = searchItems.find((item) => item.code === id);
const {
vendorNameData,
vendorAccountData,
vendorAddressData
} = selectedItem;
setItem({ vendorNameData, vendorAccountData, vendorAddressData });
SetSearchItems([]);
}
function HandleListKeyDown(evt) {
console.log(evt.keyCode);
}
function HandleChange(evt) {
setItem({ item: { [evt.target.name]: evt.target.value } });
}
}
It would be really helpful to point me out where I'm lagging. I've tried my best but this is what I could come up with.
Any help would really be appreciated.
I've 'traduced' and cleaned up your original component, resulting as follows: (please see notes below)
import React, {useState, useCallback} from 'react';
function Input() {
const [item, setItem] = useState({
code: '',
name: '',
unit: '',
rate: ''
});
const [cursor, setCursor] = useState(0);
const [searchItems, setSearchItems] = useState([]);
const autocomplete = useCallback((evt) => {
const text = evt.target.value;
fetch(
`https://invoiceitems.herokuapp.com/items?name_like=${text}&_limit=6`
)
.then((res) => res.json())
.then((data) => {
setSearchItems(data);
});
}, []);
const handleKeyup = useCallback((evt) => {
if (evt.keyCode === 27) {
setSearchItems([]);
return false;
}
return true;
}, []);
const handleKeydown = useCallback(
(evt) => {
// arrow up/down button should select next/previous list element
if (evt.keyCode === 38 && cursor > 0) {
setCursor((prevCursor) => prevCursor - 1);
} else if (evt.keyCode === 40 && cursor < searchItems.length - 1) {
setCursor((prevCursor) => prevCursor + 1);
}
if (evt.keyCode === 13) {
let currentItem = searchItems[cursor];
if (currentItem !== undefined) {
const {code, name, unit, rate} = currentItem;
setItem({code, name, unit, rate});
setSearchItems([]);
}
}
if (evt.keyCode === 8) {
setItem({code: '', name: '', unit: '', rate: ''});
}
},
[cursor, searchItems]
);
const selectItem = useCallback(
(id) => {
let selectedItem = searchItems.find((item) => item.code === id);
const {code, name, unit, rate} = selectedItem;
setItem({code, name, unit, rate});
setSearchItems([]);
},
[searchItems]
);
const handleListKeydown = useCallback((evt) => {
console.log(evt.keyCode);
}, []);
return (
<div className={'container mt-3'}>
<h1 className={'h2 text-center'}>{'Autocomplete Example'}</h1>
<div className={'form-group'}>
<label htmlFor={'autocomplete'}>{'Item Name'}</label>
<input
type={'text'}
id={'autocomplete'}
onChange={autocomplete}
onKeyUp={handleKeyup}
onKeyDown={handleKeydown}
value={item.name}
className={'custom-input form-control'}
/>
{searchItems.length > 0 && (
<ul className={'list-group'}>
{searchItems.map((item, idx) => (
<li
className={
cursor === idx
? 'active list-group-item'
: 'list-group-item'
}
key={idx}
onClick={() => selectItem(item.code)}
onKeyDown={handleListKeydown}>
{item.name}
</li>
))}
</ul>
)}
</div>
</div>
);
}
export {Input};
useState replaces Class Components State management. It is good practice to split your state into smaller pieces as possible, because new values will completely replace old ones (there is no merging like Class Components this.setState does). Using cursor as an example, do const [cursor, setCursor] = useState(0); to initialize your state (in this case initial state is 0). Then use cursor to use the value and call setCursor(newValue) to update it. Each time you update your state you will most likely trigger a re-rendering.
useCallback allows you to only re-declare a function when its dependencies have changed, thus improving performance. Function dependencies are specified in the second argument array. On each render, React will compare the new values with the old ones and will always return the same function when dependencies have not changed.
The return statement replaces your previous render method.
Follows a working snippet. Please note that OP original code behavior has not been changed.
const {useState, useCallback, StrictMode} = React;
function Input() {
const [item, setItem] = useState({
code: '',
name: '',
unit: '',
rate: ''
});
const [cursor, setCursor] = useState(0);
const [searchItems, setSearchItems] = useState([]);
const autocomplete = useCallback((evt) => {
const text = evt.target.value;
fetch(
`https://invoiceitems.herokuapp.com/items?name_like=${text}&_limit=6`
)
.then((res) => res.json())
.then((data) => {
setSearchItems(data);
});
}, []);
const handleKeyup = useCallback((evt) => {
if (evt.keyCode === 27) {
setSearchItems([]);
return false;
}
return true;
}, []);
const handleKeydown = useCallback(
(evt) => {
// arrow up/down button should select next/previous list element
if (evt.keyCode === 38 && cursor > 0) {
setCursor((prevCursor) => prevCursor - 1);
} else if (evt.keyCode === 40 && cursor < searchItems.length - 1) {
setCursor((prevCursor) => prevCursor + 1);
}
if (evt.keyCode === 13) {
let currentItem = searchItems[cursor];
if (currentItem !== undefined) {
const {code, name, unit, rate} = currentItem;
setItem({code, name, unit, rate});
setSearchItems([]);
}
}
if (evt.keyCode === 8) {
setItem({code: '', name: '', unit: '', rate: ''});
}
},
[cursor, searchItems]
);
const selectItem = useCallback(
(id) => {
let selectedItem = searchItems.find((item) => item.code === id);
const {code, name, unit, rate} = selectedItem;
setItem({code, name, unit, rate});
setSearchItems([]);
},
[searchItems]
);
const handleListKeydown = useCallback((evt) => {
console.log(evt.keyCode);
}, []);
return (
<div className={'container mt-3'}>
<h1 className={'h2 text-center'}>{'Autocomplete Example'}</h1>
<div className={'form-group'}>
<label htmlFor={'autocomplete'}>{'Item Name'}</label>
<input
type={'text'}
id={'autocomplete'}
onChange={autocomplete}
onKeyUp={handleKeyup}
onKeyDown={handleKeydown}
value={item.name}
className={'custom-input form-control'}
/>
{searchItems.length > 0 && (
<ul className={'list-group'}>
{searchItems.map((item, idx) => (
<li
className={
cursor === idx
? 'active list-group-item'
: 'list-group-item'
}
key={idx}
onClick={() => selectItem(item.code)}
onKeyDown={handleListKeydown}>
{item.name}
</li>
))}
</ul>
)}
</div>
</div>
);
}
ReactDOM.render(
<StrictMode>
<Input />
</StrictMode>,
document.getElementById('root')
);
.active {
color: #ff0000;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.0/umd/react-dom.production.min.js"></script>
<div id='root' />