useEffect not triggered by prop dependency - react-hooks

So I have this component set up, which has an onClick handler to like or unlike a certain recipe. I am using the useEffect hook to make sure that the icon is changed accordingly based on the favoriteId prop. When the onClick and the associated queries are executed however, the useEffect hook is not triggered at all, how come?
const RecipeCard = ({ name, image, id, favoriteId }) => {
const { user } = useContext(AuthenticatedUserContext);
const [isFavorite, setIsFavorite] = useState(false);
const onLikePress = async () => {
if (favoriteId) {
await deleteDoc(doc(db, "favorites", favoriteId));
favoriteId = null;
} else {
const res = await addDoc(collection(db, "favorites"), {
userId: user.uid,
recipeId: id,
});
favoriteId = res.id;
}
};
useEffect(() => {
console.log("hit");
favoriteId ? setIsFavorite(true) : setIsFavorite(false);
}, [favoriteId]);
return (
<TouchableWithoutFeedback
onPress={onPress}
style={{ flex: 1, padding: 10 }}
>
<View>
<AntDesign
onPress={() => {
if (!user) {
setShowNoAccountModal(true);
} else {
onLikePress();
}
}}
name={isFavorite ? "like1" : "like2"}
color="black"
size={30}
/>
</View>
</TouchableWithoutFeedback>
);
};
export default RecipeCard;
Parent component:
export const HomeScreen = () => {
const [recipes, setRecipes] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
getRecipes();
}, []);
const getRecipes = async () => {
const querySnapshot = await getDocs(collection(db, "receipes"));
const fetchedRecipes = [];
for (const d of querySnapshot.docs) {
const citiesRef = collection(db, "favorites");
const q = query(
citiesRef,
where("userId", "==", user.uid),
where("recipeId", "==", d.id)
);
const querySnapshot = await getDocs(q);
const isFavorite = false;
if (querySnapshot.empty) {
favoriteId = null;
} else {
favoriteId = querySnapshot.docs[0].id;
}
const recipe = {
...d.data(),
id: d.id,
favoriteId,
};
fetchedRecipes.push(recipe);
}
setRecipes(fetchedRecipes);
setLoading(false);
};
return (
<View style={styles.container}>
{/* <Button title="Sign Out" onPress={handleLogout} /> */}
<Text style={{ fontSize: 24, fontWeight: "bold", paddingBottom: 10 }}>
Recepten
</Text>
{recipes && recipes.length > 0 && (
<FlatList
data={recipes}
renderItem={({ item }) => (
<RecipeCard
name={item.title}
id={item.id}
image={item.thumbnail}
favoriteId={favoriteId}
/>
)}
keyExtractor={(item) => item.id}
horizontal
/>
)}
</View>
);
};

You are mutating your favoriteId variable, but not using setState, so it is not done properly and react is unaware your variable might have changed.
To fix this, you will need to pass a function to change your favoriteId prop inside of your component's parent:
// in parent:
const [favoriteId, setFavoriteId] = useState() // this code should be here already
return (
// your code here
<RecipeCard changeFavoriteId={(newId) => setFavoriteId(newId)} />
// just add this changeFavoriteId prop, the old props should be here still though
// rest of your code
)
// in RecipeCard.js
const RecipeCard = ({ name, image, id, favoriteId, changeFavoriteId }) => {
// your code here
const onLikePress = async () => {
if (favoriteId) {
await deleteDoc(doc(db, "favorites", favoriteId));
favoriteId = null;
} else {
const res = await addDoc(collection(db, "favorites"), {
userId: user.uid,
recipeId: id,
});
changeFavoriteId(res.id) // this bit here changed, now you are using setState
}
};
// the rest of your code
By using setState function that you obtained from parent useState, your component will trigger a rerender after the value is changed.

Related

Cannot be used as a JSX component. Its return type 'Promise<Element>' is not a valid JSX element

I have an React Component Editor. I am trying to initialize the state using an async function. But I am unable to .
How we can do that in React.
const Editor = () => {
const { id } = useParams();
const [schemas, updateSchemas] = useAtom(bfsAtom);
const schema = id && _.get(schemas, id, {});
type InitialStateType = {
properties: KeyedProperty[];
validations: ValidationDataProperty[];
};
const getInitialState = async (): Promise<InitialStateType> => {
return {
properties: createPropertiesFromSchema(schema),
validations: initializeConditions(schema),
};
};
const initialState = await getInitialState();
const mainReducer = (
{ properties, validations }: InitialStateType,
action: Action
) => ({
properties: propertyReducer(properties, action),
validations: validationReducer(validations, action),
});
const [state, dispatch] = useReducer(mainReducer, initialState);
return (
<PropertyContext.Provider value={{ state, dispatch }}>
<SchemaEditor schema={schema} />
</PropertyContext.Provider>
);
};
TL;DR moving the async from top level to inside the component did the trick
Is not directly linked with your question but one thing that I was doing wrong is the following:
I had a component that looked like:
const MyComponent = async () => {
...
const apiResponse = await apiFunction();
return <div>
{apiResponse.success && (
<p>It was a success!</p>
)}
</div>
}
BUT you shouldn't use async in your components at top level. So to fix it I did this:
const MyComponent = () => {
...
const apiResponse = async () => {
return await apiFunction();
}
return <div>
{apiResponse().success && (
<p>It was a success!</p>
)}
</div>
}

How can I use the function in Parent component when a button is clicked in child component react hooks?

I have a function that I want to run in the parent component whenever a particular button in the child component is clicked. I am using react hooks for state management.
The Button clicked is the last one in the child component, and the function I am trying to call from the parent component is onClickHandling.
Parent component:
const SearchPage = () => {
const [searchText, setSearchTerm] = useState('');
const [image, setImage] = useState([]);
const [isLoaded, setIsLoaded] = useState(false);
const [isNext, setIsNext] = useState(false);
const [nextPageIndex, setNextPageIndex] = useState(1);
const [isHidden, setIsHidden] = useState(true);
const onInputChange = (e) => {
setSearchTerm(e.target.value);
};
const getImages = () => {
fetchImages(nextPageIndex, searchText)
.then((data) => {
setImage(data.data.results);
setIsLoaded(false);
});
};
const onSubmitHandler = (e) => {
setImage([]);
e.preventDefault();
setNextPageIndex(1);
getImages();
setIsLoaded(true);
setIsHidden(false);
};
const onClickHandling = () => {
setIsNext(true);
setNextPageIndex(parseInt(nextPageIndex + 1, 10));
};
if (isNext === true) {
fetchImages(nextPageIndex, searchText)
.then((data) => {
const result = data.data.results;
setImage(image.concat(result));
setIsLoaded(false);
});
setIsNext(false);
}
return (
<React.Fragment>
<SearchBar
className="search-bar"
onSubmitHandler={onSubmitHandler}
onInputChange={onInputChange}
searchText={searchText}
/>
<div className="image-container">
{image && (
<ImageList
image={image}
isLoaded={isLoaded}
isHidden={isHidden}
onClickHandler={onClickHandling}
/>
)}
</div>
</React.Fragment>
);
};
export default SearchPage;
Child Component:
const ImageList = ({
image, isLoaded, isHidden, onClickHandling,
}) => {
const [imageIndex, setImageIndex] = useState();
const [isOpen, setIsOpen] = useState('false');
if (isLoaded) {
return (
<div className="spinner">
<ReactLoading type="spin" color="blue" />
</div>
);
}
const onClickHandler = (e) => {
setIsOpen(true);
setImageIndex(parseInt((e.target.id), 10));
};
const imgs = image.map((img, index) => (
<img
id={index}
key={img.id}
src={img.urls.small}
onClick={onClickHandler}
/>
));
if (imgs.length === 0) {
return (
<p>No images</p>
);
}
if (isOpen === true) {
return (
<Lightbox
onCloseRequest={() => setIsOpen(false)}
mainSrc={image[imageIndex].urls.regular}
onMoveNextRequest={() => setImageIndex((imageIndex + 1) % image.length)}
onMovePrevRequest={() => setImageIndex((imageIndex + image.length - 1) % image.length)}
nextSrc={image[(imageIndex + 1) % image.length].urls.regular}
prevSrc={image[(imageIndex + image.length - 1) % image.length].urls.regular}
imageTitle={image[imageIndex].alt_description}
imageCaption={`By ${image[imageIndex].user.name}`}
/>
);
}
return (
<React.Fragment>
{imgs}
{!isHidden && <Button onClick={onClickHandling}>Click me</Button> }
</React.Fragment>
);
};
export default ImageList;
There is a typo in the name used to pass the function prop from the parent component to the child component.
<ImageList
image={image}
isLoaded={isLoaded}
isHidden={isHidden}
onClickHandling={onClickHandling}// <-- Here.
/>

Error when trying to update in react-redux

I am trying to update my data in redux but I get an error when I have more than one value in the state.
How I am transferring data into the AllPalletes component below:
<Route exact path='/' render ={(routeProps) => <AllPalletes data = {this.props.palleteNames} />} />
The AllPalletes component, where I am setting up the edit form:
class connectingPalletes extends Component {
render () {
console.log(this.props)
return (
<div>
<Menu inverted>
<Menu.Item header>Home</Menu.Item>
<Menu.Item as = {Link} to = '/createpalette'>Create a Palette</Menu.Item>
</Menu>
<Container>
<Card.Group itemsPerRow={4}>
{this.props.data.map((card) => {
let cardName = card.Name.replace(/\s+/g, '-').toLowerCase()
return (
<Card key = {card._id}>
<Image src = 'https://images.pexels.com/photos/1212406/pexels-photo-1212406.jpeg?auto=compress&cs=tinysrgb&dpr=1&w=500' wrapped ui={false}/>
<Card.Content>
<Grid>
<Grid.Column floated = 'left' width ={7}>
{card.edit? (
<PaletteEditForm {...card}/>
) : (
<Card.Header as = {Link} to = {`/palette/${cardName}`}>{card.Name}</Card.Header>
)}
</Grid.Column>
<Grid.Column floated = 'right' width = {5}>
<Icon name = 'pencil' />
<Icon name = 'trash' onClick = {() => this.props.dispatch(removePalette({id: card._id}))}/>
</Grid.Column>
</Grid>
</Card.Content>
</Card>
)
})}
</Card.Group>
<Divider></Divider>
<Divider hidden></Divider>
<Grid centered columns={1}>
<Button as = {Link} to = '/testing'>Go Back</Button>
</Grid>
</Container>
</div>
)
}
}
const AllPalletes = connect()(connectingPalletes)
export default AllPalletes
And here is the edit form:
class EditForm extends Component {
constructor(props) {
super(props)
this.state = {
paletteName: this.props.Name
}
}
handleChange = (e) => {
const val = e.target.value,
s_name = e.target.name
this.setState (() => {
return {
[s_name]: val,
}
})
}
handleSubmit = () => {
let updates = {Name: this.state.paletteName, edit: false}
this.props.dispatch(editPalette(this.props._id, updates))
}
render() {
console.log(this.props)
return (
<Form onSubmit = {this.handleSubmit}>
<Input type = 'text' name = 'paletteName' value = {this.state.paletteName} onChange={this.handleChange} />
</Form>
)
}
}
const PaletteEditForm = connect()(EditForm)
export default PaletteEditForm
My Reducer:
import uuid from 'uuid/v1'
const paletteDefault = [{
Name: "Material UI",
myArray: [],
_id: uuid(),
edit: false
}, {
Name: "Splash UI",
myArray: [],
_id: uuid(),
edit: true
}]
const PaletteReducers = (state = paletteDefault, action) => {
console.log(action)
switch(action.type) {
case 'ADD_PALETTE':
return [...state, action.palette]
case 'REMOVE_PALETTE':
return state.filter(x => x._id !== action.id)
case 'EDIT_PALETTE':
return state.map((palette) => {
if(palette._id === action.id) {
return {
...palette,
...action.updates
}
}
})
default:
return state
}
}
export default PaletteReducers
My Action
// EDIT_PALETTE
const editPalette = (id, updates) => ({
type: 'EDIT_PALETTE',
id,
updates
})
export {addPalette, removePalette, editPalette}
I have a feeling that the problem could be in how I have set up the reducer case.
The edit dispatch only works when I have one value in the state. Otherwise, I am getting this error:
Uncaught TypeError: Cannot read property 'Name' of undefined
at AllPalletes.js:23
Please help..
I found the error. I had not give a return value in the 'EDIT_PALETTE' case, after the if-statement. It was
case 'EDIT_PALETTE':
return state.map((palette) => {
if(palette._id === action.id) {
return {
...palette,
...action.updates
}
}
})
And instead should be:
case 'EDIT_PALETTE':
return state.map((palette) => {
if(palette._id === action.id) {
return {
...palette,
...action.updates
}
}
return palette
})

UI Flickers when I drag and drop and item

I have a problem getting react-beautiful-dnd to work without flickering. I have followed the example in the egghead course. Here is my code sample.
Item List Container
onDragEnd = (result) => {
if (this.droppedOutsideList(result) || this.droppedOnSamePosition(result)) {
return;
}
this.props.itemStore.reorderItem(result);
}
droppedOnSamePosition = ({ destination, source }) => destination.droppableId
=== source.droppableId && destination.index === source.index;
droppedOutsideList = result => !result.destination;
render() {
return (
<DragDropContext onDragEnd={this.onDragEnd}>
<div>
{this.props.categories.map((category, index) => (
<ListCategory
key={index}
category={category}
droppableId={category._id}
/>
))}
</div>
</DragDropContext>
);
}
Item Category
const ListCategory = ({
category, droppableId,
}) => (
<Droppable droppableId={String(droppableId)}>
{provided => (
<div
{...provided.droppableProps}
ref={provided.innerRef}
>
<ListTitle
title={category.name}
/>
<ListItems category={category} show={category.items && showIndexes} />
{provided.placeholder}
</div>
)}
</Droppable>
);
List items
<Fragment>
{category.items.map((item, index) => (
<ListItem
key={index}
item={item}
index={index}
/>
))}
</Fragment>
Items
render() {
const {
item, index, categoryIndex, itemStore,
} = this.props;
return (
<Draggable key={index} draggableId={item._id} index={index}>
{(provided, snapshot) => (
<div
role="presentation"
className={cx({
'list-item-container': true,
'selected-list-item': this.isSelectedListItem(item._id),
})}
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
style={getItemStyle(snapshot.isDragging, provided.draggableProps.style)}
onClick={this.handleItemClick}
>
<div className={cx('select-title')}>
<p className={cx('list-item-name')}>{item.title}</p>
</div>
{capitalize(item.importance)}
</div>
</div>
)}
</Draggable>
);
}
Method to reorder Items (I'm using Mobx-State_Tree)
reorderItem: flow(function* reorderItem(result) {
const { source, destination } = result;
const categorySnapshot = getSnapshot(self.itemCategories);
const sourceCatIndex = self.itemCategories
.findIndex(category => category._id === source.droppableId);
const destinationCatIndex = self.itemCategories
.findIndex(category => category._id === destination.droppableId);
const sourceCatItems = Array.from(categorySnapshot[sourceCatIndex].items);
const [draggedItem] = sourceCatItems.splice(source.index, 1);
if (sourceCatIndex === destinationCatIndex) {
sourceCatItems.splice(destination.index, 0, draggedItem);
const prioritizedItems = setItemPriorities(sourceCatItems);
applySnapshot(self.itemCategories[sourceCatIndex].items, prioritizedItems);
try {
yield itemService.bulkEditPriorities(prioritizedItems);
} catch (error) {
console.error(`Problem editing priorities: ${error}`);
}
} else {
const destinationCatItems = Array.from(categorySnapshot[destinationCatIndex].items);
destinationCatItems.splice(destination.index, 0, draggedItem);
const prioritizedSourceItems = setItemPriorities(sourceCatItems);
applySnapshot(self.itemCategories[sourceCatIndex].items, prioritizedSourceItems);
const prioritizedDestItems = setItemPriorities(destinationCatItems);
applySnapshot(self.itemCategories[destinationCatIndex].items, prioritizedDestItems);
try {
const sourceCatId = categorySnapshot[sourceCatIndex]._id;
const originalItemId = categorySnapshot[sourceCatIndex].items[source.index]._id;
yield itemService.moveItemToNewCategory(originalItemId, sourceCatId, destinationCatIndex);
} catch (error) {
console.error(`Problem editing priorities: ${error}`);
}
}
}),
Sample data
const itemData = [
{
_id: 'category-1',
title: 'Backlog',
items: [
{ _id: 'item-1', title: 'Here and back again' },
},
{
_id: 'category-2',
title: 'In progress',
items: []
},
{
_id: 'category-3',
title: 'Done',
items: []
}
}
}
Summary
When and item is dragged and dropped, I check to see if the item is dropped in the outside the dnd context or in the same position it was dragged from. If true, i do nothing.
If the item is dropped within the context, i check to see if it was dropped in the same category. if true, i remove the item from its current position, put it in the target position, update my state, and make an API call.
If it was dropped in a different category, i remove the item from the source category, add to the new category, update the state and make an API call.
Am I missing something?
I am using both mst and the react-beautiful-dnd library
I will just paste my onDragEnd action method
onDragEnd(result: DropResult) {
const { source, destination } = result;
// dropped outside the list
if (!destination) {
return;
}
if (source.droppableId === destination.droppableId) {
(self as any).reorder(source.index, destination.index);
}
},
reorder(source: number, destination: number) {
const tempLayout = [...self.layout];
const toMove = tempLayout.splice(source, 1);
const item = toMove.pop();
tempLayout.splice(destination + lockedCount, 0, item);
self.layout = cast(tempLayout);
},
I think in order to avoid the flicker you need to avoid using applySnapshot
You can replace this logic
const sourceCatItems = Array.from(categorySnapshot[sourceCatIndex].items);
const [draggedItem] = sourceCatItems.splice(source.index, 1);
sourceCatItems.splice(destination.index, 0, draggedItem);
const prioritizedItems = setItemPriorities(sourceCatItems);
applySnapshot(self.itemCategories[sourceCatIndex].items, prioritizedItems);
just splice the items tree
const [draggedItem] = categorySnapshot[sourceCatIndex].items.splice(destination.index, 0, draggedItem)
this way you don't need to applySnapshot on the source items after
I believe this issue is caused by multiple dispatches happening at the same time.
There're couple of things going on at the same time. The big category of stuff is going on is the events related to onDragStart, onDragEnd and onDrop. Because that's where an indicator has to show to the user they are dragging and which item they are dragging from and to.
So especially you need to put a timeout to onDragStart.
const invoke = (fn: any) => { setTimeout(fn, 0) }
Because Chrome and other browser will cancel the action if you don't do that. However that is also the key to prevent flickery.
const DndItem = memo(({ children, index, onItemDrop }: DndItemProps) => {
const [dragging, setDragging] = useState(false)
const [dropping, setDropping] = useState(false)
const dragRef = useRef(null)
const lastEnteredEl = useRef(null)
const onDragStart = useCallback((e: DragEvent) => {
const el: HTMLElement = dragRef.current
if (!el || (
document.elementFromPoint(e.clientX, e.clientY) !== el
)) {
e.preventDefault()
return
}
e.dataTransfer.setData("index", `${index}`)
invoke(() => { setDragging(true) })
}, [setDragging])
const onDragEnd = useCallback(() => {
invoke(() => { setDragging(false) })
}, [setDragging])
const onDrop = useCallback((e: any) => {
invoke(() => { setDropping(false) })
const from = parseInt(e.dataTransfer.getData("index"))
onItemDrop && onItemDrop(from, index)
}, [setDropping, onItemDrop])
const onDragEnter = useCallback((e: DragEvent) => {
lastEnteredEl.current = e.target
e.preventDefault()
e.stopPropagation()
setDropping(true)
}, [setDropping])
const onDragLeave = useCallback((e: DragEvent) => {
if (lastEnteredEl.current !== e.target) {
return
}
e.preventDefault()
e.stopPropagation()
setDropping(false)
}, [setDropping])
return (
<DndItemStyle
draggable="true"
onDragStart={onDragStart}
onDragEnd={onDragEnd}
onDrop={onDrop}
onDragOver={onDragOver}
onDragEnter={onDragEnter}
onDragLeave={onDragLeave}
dragging={dragging}
dropping={dropping}
>
{(index < 100) && (
cloneElement(children as ReactElement<any>, { dragRef })
)}
</DndItemStyle>
)
})
I have to apply two more timeout invoke in the above DndItem, the reason for that is during the drop, there're two many events are competing with each other, to name a few
onDragEnd, to sugar code the indicator
onDrop, to re-order
I need to make sure re-order happens very quickly. Because otherwise you get double render, one with the previous data, and one with the next data. And that's why the flickery is about.
In short, React + Dnd needs to apply setTimeout so that the order of the paint can be adjusted to get the best result.

HOC is triggering while I use it on container

Could anyone can help me, because i can't understand why HOC is causing ifnite loop while I am using it on container. That's my container:
class UserContainer extends Component {
componentDidMount() {
const { onValuePassedThroughParams, match } = this.props;
const { user } = match.params;
if (user !== '') {
onValuePassedThroughParams(user);
}
}
render() {
const { user } = this.props;
return (
<UserView user={user} />
);
}
}
const UserContainerWithLoading = LoaderHOC(UserContainer);
const mapStateToProps = state => ({
user: state.user,
});
const mapDispatchToProps = dispatch => ({
onValuePassedThroughParams: val => dispatch(takeUserNameAndFetchData(val)),
});
export default
withRouter(
connect(mapStateToProps, mapDispatchToProps)(UserContainerWithLoading),
);
my HOC:
const LoaderHOC = WrappedComponent => props =>{
return(
props.user.isLoading
? <div className={styles.ldsHourglass} />
: <WrappedComponent {...props} />
)};
and also a thunk:
function fetchData(url) {
return (
fetch(url)
.then(result => result.json())
);
}
export default function takeUserNameAndFetchData(name) {
const userInfoUrl = `https://api.github.com/users/${name}`;
const userRepoUrl = `https://api.github.com/users/${name}/repos`;
return (dispatch) => {
dispatch(fetchUserBegin());
Promise.all([
fetchData(userInfoUrl),
fetchData(userRepoUrl),
])
.then(([info, repos]) => {
dispatch(fetchUserInfoSucces(info));
dispatch(fetchUserReposSuccess(repos));
dispatch(fetchUserLoadingEnd());
})
.catch((err) => {
console.log(`ERROR!${err}`);
dispatch(fetchUserError());
});
};
}
When I use an HOC on my View component everything is fine and it's stop geting data from the server, but when I am using it on container there is always an infinite loop. Do you have any advice for me?

Resources