i have a method showInfo that is accessed by two components Child1 and Child2.
initially i had the showInfo method within Child1 component like below
const Child1 = ({history}: RouteComponentProps) => {
type Item = {
id: string,
name: string,
value: string,
};
const showInfo = (item: item) => {
const id = item.id;
const value = item.value;
const handleRouteChange = () => {
const path = value === 'value1' ? `/value1/?item=${itemId}` : `/value2/?item=${itemId}`;
history.push(path);
}
return (
<Button onClick={handleRouteChange}> Info </Button>
);
}
return (
<SomeComponent
onDone = {({ item }) => {
notify({
actions: showInfo(item)
})
}}
/>
);
}
the above code works. but now i have another component child2 that needs to use the same method showInfo.
the component child2 is like below
const Child2 = () => {
return (
<Component
onDone = {({ item }) => {
notify({
actions: showInfo(item)
})
}}
/>
);
}
Instead of writing the same method showInfo in Child2 component i thought of having it in different file from where child1 and child2 components can share the method showInfo.
below is the file with name utils.tsx that has showInfo method
export const showInfo = (item: item) => {
const id = item.id;
const value = item.value;
const handleRouteChange = () => {
const path = value === 'value1' ? `/value1/?item=${itemId}` :
`/value2/?item=${itemId}`;
history.push(path); //error here push is not identified as no
//history passed
}
return (
<Button onClick={handleRouteChange}> Info </Button>
);
}
return (
<SomeComponent
onDone = {({ item }) => {
notify({
actions: showInfo(item)
})
}}
/>
);
}
With the above, i get the error where i use history.push in showInfo method. push not defined.
this is because history is not defined in this file utils.tsx
now the question is how can i pass history from child1 or child2 components. or what is the other way that i can access history in this case.
could someone help me with this. thanks.
EDIT:
notify in child1 and child2 is coming from useNotifications which is like below
const useNotifications = () => {
const [activeNotifications, setActiveNotifications] = React.useContext(
NotificationsContext
);
const notify = React.useCallback(
(notifications) => {
const flatNotifications = flatten([notifications]);
setActiveNotifications(activeNotifications => [
...activeNotifications,
...flatNotifications.map(notification => ({
id: notification.id,
...notification,
});
},
[setActiveNotifications]
)
return notify;
}
While you could potentially create a custom hook, your function returns JSX. There's a discussion about returning JSX within custom hooks here. IMO, this is not so much a util or custom hook scenario, as it is a reusable component. Which is fine!
export const ShowInfo = (item: item) => { // capitalize
const history = useHistory(); // use useHistory
// ... the rest of your code
}
Now in Child1 and Child2:
return (
<SomeComponent
onDone = {({ item }) => {
notify({
actions: <ShowHistory item={item} />
})
}}
/>
);
You may have to adjust some code in terms of your onDone property and notify function, but this pattern gives you the reusable behavior you're looking for.
Related
I have a component named Products, and it has this function declared in it:
const filterBySearch = (value: string) => {
setAllProducts((prevProducts) => {
const filtered = sourceProducts.filter((product) =>
product.name.toLowerCase().includes(value.toLowerCase())
);
return filtered;
});
};
I also have 2 other components, Navbar, and App, the Navbar contains a search input field, I want things to work in a way that whenever the value of that input inside the Navbar changes, the FilterBySearch function is called with the input value as its argument.
The problem is that Navbar is neither a child of Products nor a parent, but they're both children of the App component.
How do I pass the FilterBySearch from Products to App then from App to Navbar ?
Rather than passing the function you can set up the filterBySearch function inside the App component with the products state and call the function inside the Navbar component inside the change event listener of the input element.
Next, pass the allProducts state to the Products components
const App = () => {
const [allProducts, setAllProducts] = useState([]);
const filterBySearch = (value: string) => {
setAllProducts((prevProducts) => {
const filtered = sourceProducts.filter((product) =>
product.name.toLowerCase().includes(value.toLowerCase())
);
return filtered;
});
};
return (
<Navbar filterBySearch={filterBySearch} />
<Products allProducts={products} />
)
}
const Navbar = ({filterBySearch}) => {
return <input onChange={(e) => filterBySearch(e.target.value)} />
}
const Products = ({allProducts}) => {
//...
}
Or
You can define the function inside the Navbar component and pass the setAllProducts function as a prop to the Navbar component
const App = () => {
const [allProducts, setAllProducts] = useState([]);
return (
<Navbar setAllProducts={setAllProducts} />
<Products allProducts={products} />
)
}
const Navbar = ({setAllProducts}) => {
const filterBySearch = (value: string) => {
setAllProducts((prevProducts) => {
const filtered = sourceProducts.filter((product) =>
product.name.toLowerCase().includes(value.toLowerCase())
);
return filtered;
});
};
return <input onChange={(e) => filterBySearch(e.target.value)} />
}
const Products = ({allProducts}) => {
//...
}
I have 2 components that pass to the child the prop data, but only one parent pass the prop numPage and the prop setNumPage,
When I try to use the optional sign in the interface it tells me that React.Dispatch<React.SetStateAction> cannot be undefined. I found a solution that is inadequate, use any, can you give me another solution?
First parent
const Home = () => {
const [searchParams] = useSearchParams()
const [numPages, setNumPages] = useState<number>(1)
const url:string = searchParams.get("search") ? `${SEARCH_URL}${searchParams.get("search")}${PAGES}${numPages}` : `${POPULAR_RESULTS}${numPages}`;
const {data, loading} = useFetch<movieApi>(url);
if(loading) return <Sppiner/>
return (
<div>
<Items
data={data}
numPages={numPages}
setNumPages={setNumPages}
/>
</div>
);
};
Second parent
const GenresPage = () => {
const { data, loading } = useFetch<movieApi>(POPULAR_RESULTS);
if(loading) return <Sppiner/>
return (
<div>
<Items data={data} />
</div>
);
};
export default GenresPage;
Child
interface DataProps {
data: movieApi | null;
numPages:number;
setNumPages:React.Dispatch<React.SetStateAction<number>>;
}
const Items = ({ data,numPages,setNumPages }:DataProps) => {}
To the child if I put any or DataProps it works, but I don't wanna do that.
You can change setNumPages inside DataProps to:
setNumPages: (value: number) => void
I'm experiencing this warning with different components in my application but I will take just the following one as example.
I have got the following component. It has a map function that renders multiple components from an array:
const Clipboards = () => {
const { user, addClipboard } = useAuth();
const clipboards = user?.clipboards || [];
return (
<>
{clipboards.map(clipboard =>
<Clipboard clipboard={clipboard}/>
)}
<IconButton onClick={() => addClipboard()} color={'secondary'}>
<PlusIcon/>
</IconButton>
</>
)
};
export default Clipboards;
with Clipboard being as follows. As you can see, I use key prop in the wrapping div:
const Clipboard = ({clipboard, setSelectedClipboard}) => {
const {addClipboardRubric} = useAuth();
const {enqueueSnackbar} = useSnackbar();
const selectClip = () => {
setSelectedClipboard({id: clipboard.id, name: clipboard.name})
};
const [{isActive}, drop] = useDrop(() => ({
accept: 'rubric',
collect: (monitor) => ({
isActive: monitor.canDrop() && monitor.isOver(),
}),
drop(item, monitor) {
handleDrop(item)
},
}));
const handleDrop = async (rubric) => {
try {
await addClipboardRubric({
clipboardId: clipboard.id,
rubric: rubric
})
enqueueSnackbar('Rubric added', {
variant: 'success'
});
} catch (e) {
enqueueSnackbar('Rubric already added', {
variant: 'error'
});
}
}
console.log(`clip-${clipboard.id}`)
return (
<div ref={drop} key={clipboard.id}>
<IconButton
id={`clip-${clipboard.id}`}
onClick={selectClip}
color={'secondary'}
>
<Badge badgeContent={clipboard?.rubrics?.length || 0} color={"secondary"}>
<ClipboardIcon/>
</Badge>
</IconButton>
</div>
)
}
export default connect(
({search: {repertories = {}}}) => ({repertories}),
(dispatch) => ({
setSelectedClipboard: (payload) => dispatch(SET_SELECTED_CLIPBOARD(payload)),
})
)(Clipboard);
As you can see. I'm adding the key and it is unique.
What would be the problem then?
You have to add a unique key to every item returned from the map function. For example, you can use the index as a unique key which is not recommended but just to give you an example. In your code, you are adding a key inside the component. You need to add the key to the component itself. Check the below code.
const Clipboards = () => {
const { user, addClipboard } = useAuth();
const clipboards = user?.clipboards || [];
return (
<>
{clipboards.map((clipboard,index) =>
<Clipboard clipboard={clipboard} key={index}/>
)}
<IconButton onClick={() => addClipboard()} color={'secondary'}>
<PlusIcon/>
</IconButton>
</>
)
};
export default Clipboards;
You haven't added a key to the Clipboard component which is returned by the map.
Keys help React identify which items have changed, are added, or are removed. Keys should be given to the elements inside the array to give the elements a stable identity.
Source
Try something like this (if Clipboard as an id property):
{clipboards.map(clipboard =>
<Clipboard key={clipboard.id} clipboard={clipboard}/>
)}
Read this before using the index as a key
I want to toggle each of the item that I clicked on but Its keeps toggling all the Items. Using the useContext api
import React, { useState, useEffect } from "react";
const MyContext = React.createContext({
addToFavorites: () => {},
likeHandler: () => {},
fetchRequest: () => {},
});
export const MyContextProvider = (props) => {
const [favorites, setfavorites] = useState([]);
const [toggle, setToggle] = useState(false);
const [items, setItems] = useState([]);
const fetchRequest = async () => {
const api_Key = "oLfD9P45t23L5bwYmF2sib88WW5yZ8Xd7mkmhGSy";
const response = await fetch(
`https://api.nasa.gov/mars-photos/api/v1/rovers/curiosity/photos?
sol=50&api_key=${api_Key}`
);
const data = await response.json();
const allItems = data.photos.map((item) => {
return {
id: item.id,
title: item.camera.full_name,
img: item.img_src,
date: item.rover.launch_date,
like: false,
};
});
setItems(allItems);
};
const likeHandler = (item) => {
const found = items.find((x) => x.id === item.id);
setToggle((found.like = !found.like));
console.log(found); //this logs the particular item that is clicked on
};
return (
<MyContext.Provider
value={{
likeHandler,
fetchRequest,
toggleLike: toggle,
data: items,
}}
>
{props.children}
</MyContext.Provider>
);
};
export default MyContext;
I also have a NasaCard component where I call the likeHandler function and the toggle state, onClick of the FavoriteIcon from my context.And I pass in the toggle state to a liked props in my styled component to set the color of the favorite Icon
import {
Container,
Image,
Name,
InnerContainer,
Titlecontainer,
Date,
FavouriteContainer,
FavouriteIcon,
ImageContainer,
SocialContainer,
} from "./index";
import MyContext from "../../Context/store";
import React, { useState, useEffect, useContext } from "react";
const NasaCards = (props) => {
const { likeHandler, toggleLike } = useContext(MyContext);
return (
<Container>
<InnerContainer>
<ImageContainer>
<Image src={props.Image} alt="" />
</ImageContainer>
<Titlecontainer>
<Name>{props.title}</Name>
<Date>{props.date}</Date>
<FavouriteContainer>
<FavouriteIcon
liked={toggleLike}
onClick={() => {
likeHandler({
id: props.id,
title: props.title,
Image: props.Image,
});
}}
/>
</InnerContainer>
</Container>
);
};
export default NasaCards;
I think you're making this a little more complicated than it needs to be. For starters you are using a single boolean toggle state for all the context consumers, and then I think you're mixing your like property on the items state array with the toggle state.
The items array objects have a like property, so you can simply toggle that in the context, and then also use that property when mapping that array.
MyContextProvider - Map the items state to a new array, updating the like property of the matching item.
const likeHandler = (item) => {
setItems(items => items.map(
el => el.id === item.id
? { ...el, like: !el.like }
: el
));
console.log(item); // this logs the particular item that is clicked on
};
NasaCards - Use item.like property for the liked prop on FavouriteIcon and pass the entire props object to the likeHandler callback.
const NasaCards = (props) => {
const { likeHandler } = useContext(MyContext);
return (
<Container>
<InnerContainer>
<ImageContainer>
<Image src={props.Image} alt="" />
</ImageContainer>
<Titlecontainer>
<Name>{props.title}</Name>
<Date>{props.date}</Date>
<FavouriteContainer>
<FavouriteIcon
liked={props.like} // <-- use like property
onClick={() => {
likeHandler(props); // <-- props has id property
}}
/>
</FavouriteContainer>
</Titlecontainer?
</InnerContainer>
</Container>
);
};
This is how my Categories react functional component looks like. For easier testing, I split up the handleClick and the react component itself - but that shouldn't be an issue for this question.
How do I pass the component string value from the map() to the handleClick()? handleClick already passes some operator parameter, which gets me struggling with this simple issue...
export const useCategories = () => {
const handleClick = (operator) => {
updateCategory({
variables: {
id: '123',
operator,
category // <-- this value is missing
}
})
}
return {
icon: {
onClick: handleClick('$pull') // <-- Here I add some operator value
}
}
}
export const Categories = () => {
const { icon } = useCategories()
return (
<div>
{categories.map((category) => <Icon onClick={icon.onClick} />)} {/* <-- how to pass category value to handleClick...? */}
</div>
)
}
In order to "add" a variable, you can use currying.
Hence, return a function that receives the new argument and use it internally:
export const useCategories = () => {
return (category) => {
const handleClick = (operator) => {
updateCategory({
variables: {
id: '123',
operator,
category // now this is the argument of the wrapper function
}
})
};
return () => handleClick('$pull');
};
}
And you can use it like this:
export const Categories = () => {
const getOnClick = useCategories();
return (
<div>
{categories.map((category) => {
const onClick = getOnClick(category);
return <Icon onClick={onClick} />;
})}
</div>
)
}