How to architecture React event handler - reactjs

I have a list of items, each of which is represented by a component and supports a couple of actions such as "Like", "Save", I have 2 options: one is to keep the handlers in list(Parent) component and pass the handlers to each item component or I can have the handlers in the item component, the code may look like below, I am wondering which one is better, Thanks!
Solution 1:
const ItemComp = ({item, onLike, onSave}) => {
return (
<>
<p>{item.name}</p>
<div>
<button onClick={() => onLike(item.id)}>Like</button>
<button onClick={() => onSave(item.id)}>Save</button>
</div>
</>
)
}
const ListComp = ({items}) => {
const handleLike = (id) => {
console.log(id)
// like it
}
const handleSave = (id) => {
console.log(id)
// save it
}
return (
{items.map(
(item) => {
return <ItemComp item={item} onLike={handleLike} onSave={handleSave} >
}
)}
)
}
<List items={items} />
Solution 2:
const ItemComp = ({item}) => {
// keep event handlers inside of child component
const handleLike = (id) => {
console.log(id)
// like it
}
const handleSave = (id) => {
console.log(id)
// save it
}
return (
<>
<p>{item.name}</p>
<div>
<button onClick={() => handleLike(item.id)}>Like</button>
<button onClick={() => handleSave(item.id)}>Save</button>
</div>
</>
)
}
const ListComp = ({items}) => {
return (
{items.map(
(item) => {
return <ItemComp item={item} >
}
)}
)
}
<List items={items} />

if you are going to use the component in the same context throughout the whole app, or write an explanatory document for your components, its better if the handlers are inside. Otherwise, you would have to write new handlers for each time you use the component.
Think of it as a UI Library. Pass the data, see the result :)
BUT,
if you are going to use the component as a general, container kind of component, you'll have to write the handlers outside, because the component's own handlers wouldn't know how to handle the different kind of data if you introduce new contexts for it.

Related

How to render component onClick in React after passing props?

How do you update the LatestTweetsComponent with the data returned from a fetch call that happens in handleRequest? tweets is updating correctly onClick however the LatestTweetsComponent does not render or update. I may be approaching this incorrectly.
const LatestTweets = () => {
const [tweets, setTweets] = useState(null)
const handleRequest = async () => {
// this hits an api to get tweets and sets the tweets
// is this where the LatestTweetsComponent render is called?
}
return (
<div>
<button onClick={() => handleRequest()}>View Latest Tweets</button>
<LatestTweetsComponent tweets={tweets} />
</div>
)
}
const LatestTweetsComponent = props => {
return (
<div>
{props.map((tweet, index) => {
return <p key={index}>{tweet}</p>
})}
</div>
)
}
export default LatestTweets
i think this is because you are trying to map over "props", while you should be mapping over "props.tweets"
try this :
const LatestTweetsComponent = props => {
return (
<div>
{props.tweets.map((tweet, index) => {
return <p key={index}>{tweet}</p>
})}
</div>
)
}
Use that handleFetch in useEffect. So, whenever your state gets change useeffect will rerender that change.
And use tweets as argument in useEffect

How to avoid calling `useState` in a map method?

I have a snippet within a React component similar to this:
<div>
{items.map((item) => {
const [isExpanded, setIsExpanded] = useState(false)
return (
// ...
)
})}
</div>
I am calling useState in a map method, but I read in the documentation that hooks must be top level. I am wondering how could I refactor this code to avoid doing it?
option 1
In your case i would suggest creating a new component for the item.
<div>
{ items.map((item) => <Item key={} />) }
</div>
...
const Item = () => {
const [isExpanded, setIsExpanded] = useState(false)
return (
// ...
)}
}
option 2
const [expandedIds, setExpandedIds] = useState([])
...
<div>
{items.map((item) => {
const expanded = checkIsExpended(item)
return (
// ...
)
})}
</div>

Component not re-rendering when useState hook updates

export default function SearchPage() {
const [searchString, setSearchString] = React.useState("");
const [apiCall, setApiCall] = React.useState<() => Promise<Collection>>();
const {isIdle, isLoading, isError, error, data} = useApi(apiCall);
const api = useContext(ApiContext);
useEffect(()=>console.log("APICall changed to", apiCall), [apiCall]);
const doSearch = (event: React.FormEvent) => {
event.preventDefault();
setApiCall(() => () => api.search(searchString));
};
const doNext = () => {
var next = api.next;
if (next) {
setApiCall(()=>(() => next)());
}
window.scrollTo(0, 0);
}
const doPrev = () => {
if (api.prev) {
setApiCall(() => api.prev);
}
window.scrollTo(0, 0);
}
return (
<>
<form className={"searchBoxContainer"} onSubmit={doSearch}>
<TextField
label={"Search"}
variant={"filled"}
value={searchString}
onChange={handleChange}
className={"searchBox"}
InputProps={{
endAdornment: (
<IconButton onClick={() => setSearchString("")}>
<ClearIcon/>
</IconButton>
)
}}
/>
<Button type={"submit"} variant={"contained"} className={"searchButton"}>Go</Button>
</form>
{
(isIdle) ? (
<span/>
) : isLoading ? (
<span>Loading...</span>
) : isError ? (
<span>Error: {error}</span>
) : (
<Paper className={"searchResultsContainer"}>
<Box className={"navButtonContainer"}>
<Button variant={"contained"}
disabled={!api.prev}
onClick={doPrev}
className={"navButton"}>
{"< Prev"}
</Button>
<Button variant={"contained"}
disabled={!api.next}
onClick={doNext}
className={"navButton"}>
{"Next >"}
</Button>
</Box>
<Box className={"searchResults"}>
{
data && data.items().all().map(item => (
<span className={"thumbnailWrapper"}>
<img className={"thumbnail"}
src={item.link("preview")?.href}
alt={(Array.from(item.allData())[0].object as SearchResponseDataModel).title}/>
</span>
))
}
</Box>
<Box className={"navButtonContainer"}>
<Button variant={"contained"}
disabled={!api.prev}
onClick={doPrev}
className={"navButton"}>
{"< Prev"}
</Button>
<Button variant={"contained"}
disabled={!api.next}
onClick={doNext}
className={"navButton"}>
{"Next >"}
</Button>
</Box>
</Paper>
)
}
</>
)
}
For various reasons, I've got a function stored in my state (it's for use with the react-query library). I'm seeing very odd behaviour when I try and update it, though. When any of doSearch, doNext, or doPrev are called, it successfully updates the state - the useEffect hook is firing properly and I can see the message in console - but it's not triggering a re-render until the window loses and regains focus.
Most of the other people I've seen with this problem have been storing an array in their state, and updating the array rather than creating a new one - so the hooks don't treat it as a new object, and the re-render doesn't happen. I'm not using an array, though, I'm using a function, and passing it different function objects. I'm absolutely stumped and have no idea what's going on.
EDIT: It seems it might not be the rendering failing to fire, but the query hook not noticing that its input has changed? I've edited the code above to show the whole function, and my custom hook is below.
function useApi(func?: () => Promise<Collection>) {
return useQuery(
["doApiCall", func],
func || (async () => await undefined),
{
enabled: !!func,
keepPreviousData: true
}
)
}
You can’t put a function into the queryKey. Keys need to be serializable. See: https://react-query.tanstack.com/guides/query-keys#array-keys

Unable to update props in child component

This is my parent Component having state ( value and item ). I am trying to pass value state as a props to child component. The code executed in render method is Performing toggle when i click on button. But when i call the list function inside componentDidMount, Toggle is not working but click event is performed.
import React, { Component } from 'react'
import Card from './Components/Card/Card'
export class App extends Component {
state = {
values : new Array(4).fill(false),
item : [],
}
toggleHandler = (index) => {
console.log("CLICKED");
let stateObject = this.state.values;
stateObject.splice(index,1,!this.state.values[index]);
this.setState({ values: stateObject });
}
list = () => {
const listItem = this.state.values.map((data, index) => {
return <Card key = {index}
show = {this.state.values[index]}
toggleHandler = {() => this.toggleHandler(index)} />
})
this.setState({ item : listItem });
}
componentDidMount(){
// if this is not executed as the JSX is render method is executed everything is working fine. as props are getting update in child component.
this.list();
}
render() {
return (
<div>
{/* {this.state.values.map((data, index) => {
return <Card key = {index}
show = {this.state.values[index]}
toggleHandler = {() => this.toggleHandler(index)} />
})
} */}
{this.state.item}
</div>
)
}
}
export default App
This is my child Component where the state is passed as props
import React from 'react'
const Card = (props) => {
return (
<div>
<section>
<h1>Name : John Doe</h1>
<h3>Age : 20 </h3>
</section>
{props.show ?
<section>Skills : good at nothing</section> : null
}
<button onClick={props.toggleHandler} >Toggle</button>
</div>
)
}
export default Card
I know the componentDidMount is executed only once. but how to make it work except writing the JSX directly inside render method
make a copy of the state instead of mutating it directly. By using [...this.state.values] or this.state.values.slice()
toggleHandler = (index) => {
console.log("CLICKED");
let stateObject = [...this.state.values]
stateObject = stateObject.filter((_, i) => i !== index);
this.setState({ values: stateObject });
}
Also in your render method, this.state.item is an array so you need to loop it
{this.state.item.map(Element => <Element />}
Also directly in your Render method you can just do
{this.state.values.map((data, index) => {
return <Card key = {index}
show = {this.state.values[index]}
toggleHandler = {() => this.toggleHandler(index)} />
})}
In your card component try using
<button onClick={() => props.toggleHandler()}} >Toggle</button>
Value should be mapped inside render() of the class component in order to work
like this:
render() {
const { values } = this.state;
return (
<div>
{values.map((data, index) => {
return (
<Card
key={index}
show={values[index]}
toggleHandler={() => this.toggleHandler(index)}
/>
);
})}
</div>
);
}
check sandbox for demo
https://codesandbox.io/s/stupefied-spence-67p4f?file=/src/App.js

How to get item index appropriately using React 16.8 Hook?

I am practicing a todo list project using React 16 Hooks. However, I found it's hard to get the index using inside map() function like this:
Parent Todo Component:
const Todo = () => {
const dispatch = useDispatch();
const { list } = useSelector(state => state.todoReducer.toJS());
const [value, setValue] = useState('');
function handleOnchange (e) {
setValue(e.target.value)
}
function handleAddItem() {
actionCreators.addItem(dispatch, value);
setValue('');
}
function handleRemoveItem(index) {
// if use handleChecked(index) will trigger handleItemChecked and printed all //indexes everytime
actionCreators.removeItem(dispatch, value);
}
function handleItemChecked(index) {
console.log(index)
}
return (
<>
<input type="text" value={value} onChange={handleOnchange} />
<button onClick={handleAddItem}>+</button>
<List items={list} handleClick={handleRemoveItem} isCompeted={false} handleChecked={handleItemChecked}/>
</>
)
}
Child Component:
const List = ({items, handleClick, isCompleted, handleChecked}) => {
return (
<ListWrapper>
{items && items.length > 0 && items.map((item, index) => {
return (
<ListWrapper.item key={`${item}${new Date()}${Math.random()}`}>
{/* if like this: onClick={handleChecked(index)} will cause the issue */}
{/* <li>{item}<input type="checkbox" onClick={handleChecked(index)}/></li> */}
<li>{item}<input type="checkbox" name={index} onClick={e => handleChecked(e.target.name)}/></li>
<button onClick={handleClick}>-</button>
</ListWrapper.item>
);
})}
</ListWrapper>
)
}
I found if in the child component: List, if I need to get the index of item, I have to assign name={index} . If using handleChecked(index) directly, will cause rendering many times issue in its parent component(Todo). Is any better way to handle this case? Thank you so much in advanced!
as commented by jonrsharpe:
<button onClick={handleClick}>-</button>
here's 2 known methods to fix it:
<button onClick={() => handleClick(index)}>-</button>
or
<button onClick={handleClick.bind(this, index)}>-</button>
read about that: https://reactjs.org/docs/handling-events.html
hope helped :)

Resources