I am trying to make a custom select box component with parent and shild components, with autocomplete and also fetching from api. The problem is that i am trying to fire onchange function from parent to child to select an item from the select box but it is not working, can someone tell me where is the problem?
export function SelectComponent() {
const [results, setResults] = useState([]);
const [selectedValue, setSelectedValue] = useState<ComboBoxOption>();
const handleOnChange = (e: any) => {
if (!e.target.value.trim()) return setResults([]);
const filteredValue = results.filter((item: any) =>
item.value.toString().toLowerCase().startsWith(item.toLowerCase())
);
setResults(filteredValue);
};
useEffect(() => {
const fetchData = async () => {
const response = await axios.get(...);
setResults(response.data);
};
fetchData();
}, []);
return (
<div>
<SelectField
options={results}
value={selectedValue?.value}
onChange={handleOnChange}
onSelect={item => setSelectedValue(item)}
/>
</div>
);
}
export function SelectField({
...
}: SelectFieldProps) {
const [isOpen, setIsOpen] = useState(false);
const [isActive, setIsActive] = useState(false);
const [defaultValue, setDefaultValue] = useState("");
const handleOnChange: React.ChangeEventHandler<HTMLInputElement> = event => {
setIsOpen(true);
setDefaultValue(event.target.value);
onChange && onChange(event);
};
return (
<div>
<input
placeholder={placeholder}
value={defaultValue}
onChange={handleOnChange}
/>
<button onClick={() => {setIsOpen(!isOpen);}}></button>
<ul>
{options.map((option: any, index: any) => {
return (
<li
key={index}
onClick={() => {setIsOpen(false);}
>
<span>{option.value}</span>
</li>
);
})}
</ul>
)
</div>
);
}
It looks like the problem may be in the handleOnChange function in the SelectComponent. The function is trying to filter the results state based on the value of the input element, but it should be filtering based on the e.target.value instead. Also, it's using item.toLowerCase() which doesn't make sense, instead it should use e.target.value.toLowerCase():
const handleOnChange = (e: any) => {
if (!e.target.value.trim()) return setResults([]);
const filteredValue = results.filter((item: any) =>
item.value.toString().toLowerCase().startsWith(e.target.value.toLowerCase())
);
setResults(filteredValue);
};
Also, in the SelectField component, it seems that you are not calling the onSelect prop when an option is selected. You should call the onSelect prop and pass the selected option as a parameter when an option is clicked, like so:
<li
key={index}
onClick={() => {
setIsOpen(false);
onSelect(option);
}}
>
<span>{option.value}</span>
</li>
I would also recommend using onBlur instead of onClick for the input field, this way it can be closed when the user clicks outside of the component.
Related
I'm currently trying to implement some kind of modal (I'm aware that there is a bunch of libraries for that). The real code is much more complex because of a bunch of animation stuff, but it boils down to this (also see this Stackblitz):
const Modal: React.FunctionComponent<{ visible?: boolean }> = ({
visible,
}) => {
const [isVisible, setIsVisible] = React.useState(visible);
React.useEffect(() => setIsVisible(visible), [visible]);
if (!isVisible) {
return null;
}
return (
<div>
I'm visible <button onClick={() => setIsVisible(false)}>Close</button>
</div>
);
};
const App: React.FunctionComponent = () => {
const [showModal, setShowModal] = React.useState(false);
return (
<div>
<button onClick={() => setShowModal(true)}>Show modal</button>
<Modal visible={showModal} />
</div>
);
}
The first time the parent component sets the visible property it works without a problem. But when I close the "modal" and want to set the property again it does not show up again, because the property from the point of view of the "modal" didn't actually change.
Is there a way to always rerender a FunctionComponent when a property gets touched even if the value didn't change?
Have you try this:
const Modal: React.FunctionComponent<{ visible?: boolean }> = ({
visible,
setIsVisible
}) => {
if (!isVisible) {
return null;
}
return (
<div>
I'm visible <button onClick={() => setIsVisible(false)}>Close</button>
</div>
);
};
const App: React.FunctionComponent = () => {
const [showModal, setShowModal] = React.useState(false);
return (
<div>
<button onClick={() => setShowModal(true)}>Show modal</button>
<Modal visible={showModal} setIsVisible={setShowModal} />
</div>
);
}
It will then re-render also your parent component, because they share the same state
you're trying changing the value in the child element, this does not get reflected in the parent
My suggestion is that to close the modal from parent itself
which reduces the code complexity and there is only single source of data here
export const Modal: React.FunctionComponent<{ visible?: boolean , onClose }> = ({
visible,onClose
}) => {
const [isVisible, setIsVisible] = React.useState(visible);
React.useEffect(() => setIsVisible(visible), [visible]);
if (!isVisible) {
return null;
}
return (
<div>
I'm visible <button onClick={() => onClose()}>Close</button>
</div>
);
};
<Modal visible={showModal} onClose={()=>setShowModal(false)} />
working example https://stackblitz.com/edit/react-ts-heiqak?file=Modal.tsx,App.tsx,index.html
I made a JSON file for the upcoming NFL season. In this component I have a working fetch method that gets my data, and I've named the variable "squads". Now I want to press a button to filter out the selected team's schedule and display it in a modal. I've hard coded my button in this example. My modal component works fine, and I have {props.children} in the modal's body to accept my data.
In the code below you'll see that I'm trying to assign the filtered team to the selectedTeam variable using useState. The error message I'm getting just says my variables are undefined.
import React, { useState, useEffect } from "react";
import Modal from "./Components/Modal";
export default function App() {
const [show, setShow] = useState(false);
const [title, setTitle] = useState("");
const [squads, setSquads] = useState([]);
const [modalTitleBackground, setModalTitleBackground] = useState("");
const [image, setImage] = useState("");
const [selectedTeam, setSelectedTeam] = useState([]);
const url = "../nfl2021.json";
const fetchData = async () => {
try {
const response = await fetch(url);
const data = await response.json();
setSquads(data.teams);
} catch (error) {
console.log(error);
}
};
useEffect(() => {
fetchData();
}, []);
// const filterTeam = (team) => {
// const theTeam = squads.filter((squad) => squad.name === team);
// setModalTitleBackground(theTeam[0].topBG);
// // setTitle(theTeam[0].name);
// setNickname(theTeam[0].nickname);
// setImage(`./images/${theTeam[0].img}`);
// setShow(true);
// };
const filterTeam = (team) => {
setSelectedTeam(squads.filter((squad) => squad.name === team));
console.log(selectedTeam);
setTitle(selectedTeam[0].name);
setModalTitleBackground(selectedTeam[0].topBG);
setImage(`./images/${selectedTeam[0].img}`);
setShow(true);
};
return (
<div className="App">
<button onClick={() => filterTeam("New England Patriots")}>
Show Modal
</button>
<button onClick={() => filterTeam("Green Bay Packers")}>
Show Modal 2
</button>
<button onClick={() => filterTeam("Cincinnati Bengals")}>
Show Modal 3
</button>
<Modal
image={image}
title={title}
backgroundColor={modalTitleBackground}
onClose={() => setShow(false)}
show={show}
>
<p>
This is the modal body using props.children in the modal component.
</p>
<p>The {title} 2021 schedule.</p>
{selectedTeam[0].schedule.map((schedule, index) => {
return (
<p>
Week {index + 1}: The {selectedTeam[0].nickname} play the{" "}
{selectedTeam[0].schedule[index].opponent}.
</p>
);
})}
</Modal>
</div>
);
}
1- In react, the state is set asynchronously. selectedTeam is not set until next render.
2- You can use find instead of filter and get rid of array access.
const [selectedTeam, setSelectedTeam] = useState({schedule: []});
...
const filterTeam = (team) => {
let temp = squads.find((squad) => squad.name === team);
setSelectedTeam(temp);
console.log(temp);
setTitle(temp.name);
setModalTitleBackground(temp.topBG);
setImage(`./images/${temp.img}`);
setShow(true);
};
...
{selectedTeam.schedule.map((match, index) => {
return (
<p>
Week {index + 1}: The {selectedTeam.nickname} play the {match.opponent}.
</p>
);
})}
Hello I have a clickhandler that I send to a child component and use it on onclick, but for some reason, my click handler event on my parent component is not running
parent jsx:
type ClickHandler = (tag: ITag) => (e: MouseEvent) => void
const MenuTags: React.FC<{hover: boolean}> = observer(({hover}) => {
const {layoutStore} = useRootStore()
const [tags, setTags] = useState<ITag[]>(Tags)
const showHideDropItem: ShowHideDropItem = (tag) => {
console.log(tag)
setTags((items) =>
items.map((item) => ({
...item,
Active: item.Name === tag.Name ? tag.Active !== true : false,
})),
)
}
const clickHandler: ClickHandler = (tag) => (e) => {
console.log('a')
e.preventDefault()
showHideDropItem(tag)
}
return (
<MenuList
open={layoutStore.sideBar || layoutStore.onHoverSideState}
hover={hover}
>
{tags.map((item) => (
<div key={JSON.stringify(item.Name)}>
{item.Title ? <div className="title_tagList">{item.Title}</div> : ''}
<TagList
open={layoutStore.sideBar || layoutStore.onHoverSideState}
tag={item}
clickHandler={clickHandler}
/>
</div>
))}
</MenuList>
)
})
my children jsx:
const TagList: React.FC<ITagList> = observer(({tag, clickHandler, open}) => {
const tagHandleClick = (e: any) => {
e.preventDefault()
if (tag.Active !== undefined) clickHandler(tag)
}
return (
<ListItem open={open} isDropDown={!!tag.DropdownItems} active={tag.Active}>
<div className="tag-container">
<NavLink
className="tag-wrapper"
to={tag.Link}
onClick={tagHandleClick}
>
<tag.Icon className="svg-main" size={22} />
<span className="tag-name">{tag.Name}</span>
</NavLink>
</div>
</ListItem>
)
})
when clicking on my event it enters my handler of the child component, but the handler does not call my parent component's handler
Your clickHandler is a function that returns a function. It might be easier to see if you temporarily rewrite it like this:
const clickHandler: ClickHandler = (tag) => {
return (e) => {
console.log("a")
e.preventDefault()
showHideDropItem(tag)
}
}
Instead of returning a function you could just do the logic of the inner function directly instead.
const clickHandler: ClickHandler = (tag) => {
console.log('a')
showHideDropItem(tag)
}
I'm trying to dynamically add button to my component when selecting a value in a select. Adding the button to my page works fine, but the state of the component inside the callback function of the button stays at its default value.
I have the following implementation:
const Component = () => {
const [value, setValue] = useState('');
const [buttons, setButtons] = useState([]);
const onChangeValue = (e) => {
setValue(e.target.value);
setButtons([...buttons,
<button
onClick={() => console.log(value)} // displays '' which is the default state
>
Try me
</button>
]);
}
return (
<div>
<select onChange={onChangeValue}>
<option value={1}>Option #1</option>
<option value={2}>Option #2</option>
<option value={3}>Option #3</option>
</select>
{buttons}
</div>
);
}
I would guess that the state of my component is not "connected" to the buttons, but I'm not sure how I can do it.
I think the onClick event is trying to console log the wrong variable. You should put event.target.value instead of just value.
You can create custom components and pass the dynamic value from useState as a prop:
const Component = () => {
const [value, setValue] = useState('');
const [buttons, setButtons] = useState([]);
const onChangeValue = (e) => {
setValue(e.target.value);
setButtons([...buttons,
(props) => <button
onClick={() => console.log(props.value)}
>
Try me
</button>
]);
}
return (
<div>
<select onChange={onChangeValue}>
<option value={1}>Option #1</option>
<option value={2}>Option #2</option>
<option value={3}>Option #3</option>
</select>
{buttons.map((B, i) => <B key={i} value={value}/>)}
</div>
);
}
The issue in your code is because of useState since useState is asynchronous. So at the time component is creating so the value is "empty" as its not updated because of asynchronous. The better way is to kind of closure which remember its state. You can simply do this by saving variable state:
Here is the code:
const Component = () => {
const [value, setValue] = useState("");
const [buttons, setButtons] = useState([]);
const handleClick = (val) => console.log(val);
const onChangeValue = (e) => {
e.persist();
const val = e.target.value;
setValue(e.target.value);
if (buttons.length < 3) {
//avoid unnecessary addition
setButtons([
...buttons,
<button
key={value}
onClick={() => handleClick(val)} // displays '' which is the default state
>
Try me
</button>
]);
}
}
Here is the demo: https://codesandbox.io/s/sharp-oskar-qnm51?file=/src/App.js:65-633
I have to create component which fetch data with pagination and filters.
Filters are passed by props and if they changed, component should reset data and fetch it from page 0.
I have this:
const PaginationComponent = ({minPrice, maxPrice}) => {
const[page, setPage] = useState(null);
const[items, setItems] = useState([]);
const fetchMore = useCallback(() => {
setPage(prevState => prevState + 1);
}, []);
useEffect(() => {
if (page === null) {
setPage(0);
setItems([]);
} else {
get(page, minPrice, maxPrice)
.then(response => setItems(response));
}
}, [page, minPrice, maxPrice]);
useEffect(() => {
setPage(null);
},[minPrice, maxPrice]);
};
.. and there is a problem, because first useEffect depends on props, because I use them to filtering data and in second one I want to reset component. And as a result after changing props both useEffects run.
I don't have more ideas how to do it correctly.
In general the key here is to move page state up to the parent component and change the page to 0 whenever you change your filters. You can do it either with useState, or with useReducer.
The reason why it works with useState (i.e. there's only one rerender) is because React batches state changes in event handlers, if it didn't, you'd still end up with two API calls. CodeSandbox
const PaginationComponent = ({ page, minPrice, maxPrice, setPage }) => {
const [items, setItems] = useState([]);
useEffect(() => {
get(page, minPrice, maxPrice).then(response => setItems(response));
}, [page, minPrice, maxPrice]);
return (
<div>
{items.map(item => (
<div key={item.id}>
{item.id}, {item.name}, ${item.price}
</div>
))}
<div>Page: {page}</div>
<button onClick={() => setPage(v => v - 1)}>back</button>
<button onClick={() => setPage(v => v + 1)}>next</button>
</div>
);
};
const App = () => {
const [page, setPage] = useState(0);
const [minPrice, setMinPrice] = useState(25);
const [maxPrice, setMaxPrice] = useState(50);
return (
<div>
<div>
<label>Min price:</label>
<input
value={minPrice}
onChange={event => {
const { value } = event.target;
setMinPrice(parseInt(value, 10));
setPage(0);
}}
/>
</div>
<div>
<label>Max price:</label>
<input
value={maxPrice}
onChange={event => {
const { value } = event.target;
setMaxPrice(parseInt(value, 10));
setPage(0);
}}
/>
</div>
<PaginationComponent minPrice={minPrice} maxPrice={maxPrice} page={page} setPage={setPage} />
</div>
);
};
export default App;
The other solution is to use useReducer, which is more transparent, but also, as usual with reducers, a bit heavy on the boilerplate. This example behaves a bit differently, because there is a "set filters" button that makes the change to the state that is passed to the pagination component, a bit more "real life" scenario IMO. CodeSandbox
const PaginationComponent = ({ tableConfig, setPage }) => {
const [items, setItems] = useState([]);
useEffect(() => {
const { page, minPrice, maxPrice } = tableConfig;
get(page, minPrice, maxPrice).then(response => setItems(response));
}, [tableConfig]);
return (
<div>
{items.map(item => (
<div key={item.id}>
{item.id}, {item.name}, ${item.price}
</div>
))}
<div>Page: {tableConfig.page}</div>
<button onClick={() => setPage(v => v - 1)}>back</button>
<button onClick={() => setPage(v => v + 1)}>next</button>
</div>
);
};
const tableStateReducer = (state, action) => {
if (action.type === "setPage") {
return { ...state, page: action.page };
}
if (action.type === "setFilters") {
return { page: 0, minPrice: action.minPrice, maxPrice: action.maxPrice };
}
return state;
};
const App = () => {
const [tableState, dispatch] = useReducer(tableStateReducer, {
page: 0,
minPrice: 25,
maxPrice: 50
});
const [minPrice, setMinPrice] = useState(25);
const [maxPrice, setMaxPrice] = useState(50);
const setPage = useCallback(
page => {
if (typeof page === "function") {
dispatch({ type: "setPage", page: page(tableState.page) });
} else {
dispatch({ type: "setPage", page });
}
},
[tableState]
);
return (
<div>
<div>
<label>Min price:</label>
<input
value={minPrice}
onChange={event => {
const { value } = event.target;
setMinPrice(parseInt(value, 10));
}}
/>
</div>
<div>
<label>Max price:</label>
<input
value={maxPrice}
onChange={event => {
const { value } = event.target;
setMaxPrice(parseInt(value, 10));
}}
/>
</div>
<button
onClick={() => {
dispatch({ type: "setFilters", minPrice, maxPrice });
}}
>
Set filters
</button>
<PaginationComponent tableConfig={tableState} setPage={setPage} />
</div>
);
};
export default App;
You can use following
const fetchData = () => {
get(page, minPrice, maxPrice)
.then(response => setItems(response));
}
// Whenever page updated fetch new data
useEffect(() => {
fetchData();
}, [page]);
// Whenever filter updated reseting page
useEffect(() => {
const prevPage = page;
setPage(0);
if(prevPage === 0 ) {
fetchData();
}
},[minPrice, maxPrice]);