"myGroups" is a context variable. In my context store, I am fetching data from a database and populating this "myGroups" variable. So initially it contains only an empty array, after some time it contains an array of objects. e.g. [{id: "", data: ""}]
I want to render these groups. So I am mapping through the myGroups variable, and trying to render them.
But the problem is that even after context updating, my component does not re-render. I have console logged and seen that the fetching of data works absolutely fine, though it takes some time to do so.
Does changing the context does not rerender it's consumer? Why is the component not rerendering? It would be of great help if you can provide some solution. Thanks in Advance.
Here is my code.
import React, { useContext, useEffect, useState } from 'react';
import "../css/MyGroups.css";
import GroupCard from './GroupCard';
import { GlobalContext } from '../context/GlobalState';
const MyGroups = () => {
const { myGroups } = useContext(GlobalContext);
useEffect(() => console.log(myGroups), [myGroups]); // Debugging
return (
<div className="my__groups">
<h1 className="my__groups__heading">My Groups</h1>
<div className="my__groups__underline"></div>
<div className="my__groups__grid__container">
{
myGroups.map(({id, data}) => (
<GroupCard
key={id}
name={data.name}
image={data.image}
/>
))
}
</div>
</div>
)
}
export default MyGroups
This is what I get on the console when the context changes:
Console log image
My Global Provider:
<GlobalProvider>
<BrowserRouter>
<Main />
</BrowserRouter>
</GlobalProvider>
The MyGroups Component is a descendant of the Main Component.
Edit 1: Fetch Function of my Store
function fetchGroupsFromDatabase(id) {
let myGroups = [];
db.collection("users").doc(id).get() // Fetch user details with given id
.then(doc => {
doc.data().groupIDs.map(groupID => { // Fetch all group IDs of the user
db.collection("groups").doc(groupID).get() // Fetch all the groups
.then(doc => {
myGroups.push({id: doc.id, data: doc.data()})
})
})
})
.then(() => {
const action = {
type: FETCH_GROUPS_FROM_DATABASE,
payload: myGroups
};
dispatch(action);
})
}
Edit 2: Reducer
const Reducer = (state, action) => {
switch(action.type) {
case FETCH_GROUPS_FROM_DATABASE:
return {
...state,
myGroups: action.payload
};
default:
return state;
}
}
export default Reducer;
As Yousaf said, not need to using useState and useEffect. You can use context in two different way
first:
const MyGroups = () => (
<GlobalContext.Consumer>
{({ myGroups }) => (
<div className="my__groups">
<h1 className="my__groups__heading">My Groups</h1>
<div className="my__groups__underline"></div>
<div className="my__groups__grid__container">
{myGroups.map(({id, data}) => (
<GroupCard
key={id}
name={data.name}
image={data.image}
/>
))
}
</div>
</div>
)}
</GlobalContext.Consumer>
);
second:
const MyGroups = () => {
const { myGroups } = useContext(GlobalContext);
return (
<div className="my__groups">
<h1 className="my__groups__heading">My Groups</h1>
<div className="my__groups__underline"></div>
<div className="my__groups__grid__container">
{myGroups.map(({ id, data }) => (
<GroupCard key={id} name={data.name} image={data.image} />
))}
</div>
</div>
);
};
Edit:
the problem comes from the Fetch Function it based on this answer should be like this:
async function fetchGroupsFromDatabase(id) {
const doc = await db.collection("users").doc(id).get() // Fetch user details with given id
const myGroups = await Promise.all(
doc.data().groupIDs.map(groupID => // Fetch all group IDs of the user
db.collection("groups").doc(groupID).get() // Fetch all the groups
.then(doc => ({ id: doc.id, data: doc.data() }))
)
);
const action = {
type: FETCH_GROUPS_FROM_DATABASE,
payload: myGroups
};
dispatch(action);
}
Related
I'm building a simple venue review app using react/redux toolkit/firebase.
The feature VenueList.js renders a list of venues. When the user clicks on a venue, it routes them to Venue.js page which renders information about the specific venue clicked on.
Here's the problem: Venue.js renders on the first page load, but crashes when I try to refresh the page.
After some investigating I found that in Venues.js, the useSelector hook returned the correct state on first load, and then an empty array upon refresh:
Intial page load:
On page refresh
Why is this happeing and how can I fix this so that the page renders in all circumstances?
Here's Venue.js
import { useParams } from "react-router-dom";
import { useSelector } from "react-redux";
import AddReview from "../../components/AddReview";
import Reviews from "../../components/Reviews";
const Venue = () => {
const { id } = useParams();
const venues = useSelector((state) => state.venues);
const venue = venues.venues.filter((item) => item.id === id);
console.log(venues)
const content = venue.map((item) => (
<div className="venue-page-main" key = {item.name}>
<h2>{item.name}</h2>
<img src={item.photo} alt = "venue"/>
</div>
));
return (
<>
{content}
<AddReview id = {id}/>
{/* <Reviews venue = {venue}/> */}
</>
);
};
export default Venue;
The list of venues in VenueList.js
import { Link } from "react-router-dom";
import { useEffect } from "react";
import { fetchVenues } from "./venueSlice";
import { useSelector,useDispatch } from "react-redux";
const VenueList = () => {
const dispatch = useDispatch();
useEffect(() => {
dispatch(fetchVenues());
}, [dispatch]);
const venues = useSelector((state) => state.venues);
const content = venues.venues.map((venue) => (
<Link to={`/venue/${venue.id}`} style = {{textDecoration: "none"}} key = {venue.name}>
<div className="venue-item">
<h2>{venue.name}</h2>
<img src={venue.photo} />
</div>
</Link>
));
return (
<div className="venue-list">
{content}
</div>
);
};
export default VenueList;
And here's the slice venueSlice.js controlling all the API calls
import { createSlice,createAsyncThunk } from "#reduxjs/toolkit";
import { collection,query,getDocs,doc,updateDoc,arrayUnion, arrayRemove, FieldValue } from "firebase/firestore";
import { db } from "../../firebaseConfig";
const initialState = {
venues: []
}
export const fetchVenues = createAsyncThunk("venues/fetchVenues", async () => {
try {
const venueArray = [];
const q = query(collection(db, "venues"));
const querySnapshot = await getDocs(q);
querySnapshot.forEach((doc) =>
venueArray.push({ id: doc.id, ...doc.data() })
);
return venueArray;
} catch (err) {
console.log("Error: ", err);
}
});
export const postReview = createAsyncThunk("venues/postReview", async (review) => {
try {
const venueRef = doc(db,"venues",review.id)
await updateDoc(venueRef, {
reviews: arrayUnion({
title:review.title,
blurb:review.blurb,
reviewId:review.reviewId })
})
} catch (err) {
console.log('Error :', err)
}
})
export const deleteReview = createAsyncThunk("venues/deleteReview", async (review) => {
const newReview = {blurb:review.blurb, title: review.title, reviewId: review.reviewId}
try {
const venueRef = doc(db,"venues",review.id)
await updateDoc(venueRef, {
reviews: arrayRemove(newReview)
})
} catch (err) {
console.log('Error: ', err)
}
})
const venueSlice = createSlice({
name: "venues",
initialState,
reducers: {},
extraReducers(builder) {
builder
.addCase(fetchVenues.fulfilled, (state, action) => {
state.venues = action.payload;
})
},
});
export default venueSlice.reducer
I think this is what is going on:
First time you load this page, you first visit the list of venues so the call to fetch them is made and the venues are stored to redux. Then when you visit a specific venue, the list exists so the selector always returns data.
dispatch(fetchVenues());
When you refetch the page you are in the /venue/${venue.id} route.
The dispatch to fetch the list hasn't been called and so you get the errors you mention.
There are a couple of ways to fix your issue
Fetch the venues if the data are not available. In Venue.js do something like:
const Venue = () => {
const { id } = useParams();
const venues = useSelector((state) => state.venues) || [];
const venue = venues.venues.filter((item) => item.id === id);
useEffect(() => {
if(venues?.length === 0) {
dispatch(fetchVenues());
}
}, [dispatch, venues, id]);
console.log(venues)
// You need to check if the venue exists, otherwise your code will throw errors
if(!venue) {
return <div>Some loader or error message<div/>
}
const content = venue.map((item) => (
<div className="venue-page-main" key = {item.name}>
<h2>{item.name}</h2>
<img src={item.photo} alt = "venue"/>
</div>
));
return (
<>
{content}
<AddReview id = {id}/>
{/* <Reviews venue = {venue}/> */}
</>
);
};
export default Venue;
Second option would be to use something like redux-persist so your data remains when the reload happens
const venues = useSelector((state) => state.venues)
render(){
<React.Fragment>
{
(venues && venues.venues && venues.venues instanceof Array && venues.venues.length>0) && venues.venues.map((elem,index)=>{
return(
<div className="venue-page-main" key={index}>
<h2>{elem.name}</h2>
<img src={elem.photo} alt="venue" />
</div>
);
})
}
</React.Fragment>
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
for learning purposes I'm creating a CRUD todo list with React and JSON-server. I got stuck with PATCH method, as it only updates data in JSON-server on the first click. I want to update the data with the component's state value.
Service file with requests:
const serverAddress = 'http://localhost:8000';
const collection = 'todoItems';
const fetchAll = async () => {
const response = await fetch(`${serverAddress}/${collection}`);
const todoItems = response.json();
return todoItems;
};
const complete = async (id) => {
const completed = {
completed : true
}
// how to set the 'completed' value in json-server based on item's state?
const response = await fetch(`${serverAddress}/${collection}/${id}`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
body: JSON.stringify(completed ),
});
const data = await response.json();
return data;
}
const TodoItemsService = {
fetchAll,
create,
remove,
complete
};
export default TodoItemsService;
Card component which holds all todo items:
const Card = () => {
const [todoItems, setTodoItems] = useState([]);
const fetchAllTodoItems = async () => {
const fetchedTodoItems = await TodoItemsService.fetchAll();
setTodoItems(fetchedTodoItems);
};
useEffect(() => {
(async () => {
fetchAllTodoItems();
})();
}, []);
const handleComplete = async (id) => {
await TodoItemsService.complete(id);
}
return (
<div className='card'>
<CardHeader />
<AddTodoForm onAddTodoItem={handleAddTodoItem} />
<TodoItemsContainer
todoItems={todoItems}
onDelete={handleDelete}
onComplete={handleComplete}
/>
</div>
)
}
export default Card;
TodoItemsContainer component
const TodoItemsContainer = ({ todoItems, onDelete, onComplete }) => {
return (
<div className='todo-items-container'>
{todoItems.length === 0 &&
<div className='empty'>
<img src={NoTodoItems} alt="" />
</div>}
{todoItems.map(({ id, text }) => (
<TodoItem
key={id}
id={id}
text={text}
onDelete={onDelete}
onComplete={onComplete}
/>
))}
</div>
)
}
export default TodoItemsContainer;
TodoItem component
const TodoItem = ({ id, text, onDelete, onComplete }) => {
const [isComplete, setIsComplete] = useState(false);
const handleIsCompleteById = () => {
onComplete(id);
setIsComplete(!isComplete);
};
const handleDeleteTodoItemById = () => {
onDelete(id);
};
return (
<div className={`todo-item ${isComplete ? 'complete' : ''}`}>
<p>{text}</p>
<div>
<TodoItemComplete onComplete={handleIsCompleteById}/>
<TodoItemDelete onDelete={handleDeleteTodoItemById}/>
</div>
</div>
)
}
export default TodoItem;
TodoItemComplete button component
const TodoItemComplete = ({ onComplete }) => {
return (
<button type='button' onClick={onComplete}>
<div className='icon'>
{<SVGComplete />}
</div>
</button>
)
}
export default TodoItemComplete;
From React perspective it works fine, it marks the item as complete based on state, but I also want to reflect todo item's status as complete in my json-server. Does anyone have any tips or can see the mistake?
Simply had to pass the state as the second param in complete service and other functions that handle complete action.
I have these configurations in my products component:
import axios from "axios";
import React, { useEffect } from "react";
import { useDispatch } from "react-redux";
import { connect } from "react-redux";
import productsAction from "../redux/actions/productsAction";
import { FETCH_PRODUCTS } from "../redux/actions/types";
function products({ products }) {
const dispatch = useDispatch();
useEffect(() => {
axios.get("http://localhost:8197/Plant/GetPlant").then(({ data }) =>
dispatch({
type: FETCH_PRODUCTS,
payload: data,
})
);
}, []);
return (
<div>
{products.map((product, index) => (
<h1 key={index}>{product.title}</h1>
))}
</div>
);
}
export const mapStateToProps = (state) => {
return {
products: state.products.products,
};
};
export default connect(mapStateToProps)(products);
Everything is okay, but when I run the project, It throws an error:
TypeError: Cannot read properties of undefined (reading 'map')
And when I inspect the project by redux devtools, the products array is equal to [].
How can I fix error?
The most efficient way do this is to add a loading state and a loader. When axios is sending a request set the loading state to true and when the data is received set it back to false.
And while the loading state is true show a loader on your page.
const Products = () => {
const [loading, setLoading] = useState(false)
useEffect(() => {
setLoading(true) // <------Set loading state to true------>
axios.get("http://localhost:8197/Plant/GetPlant").then(({ data }) =>
dispatch({
type: FETCH_PRODUCTS,
payload: data,
})
);
setLoading(false) // <---------Set loading state to false after data is retreived------->
}, []);
if(loading) {
return <p>Loading...</p> // <-----Return a loading component which shows a loader when loading state is true
}
return (
<div>
{products && products.map((product, index) => (
<h1 key={index}>{product.title}</h1>
))}
</div>
);
}
your code is accessing products before it is initialized. What you could do now is conditional rendering.
<div>
{products && products.map((product, index) => (
<h1 key={index}>{product.title}</h1>
))}
</div>
You are rendering the products before the API call which will definitely return undefined.
So instead write
{
products?.map((product, index) => (
<h1 key={index}>{product.title}</h1>
))
}
or
{
!!products && products.map((product, index) => (
<h1 key={index}>{product.title}</h1>
))
}
Here is react/redux application.
This a basic stripped down version of what I am trying to accomplish. showFolder() produces a list of folders and a button to click where it calls the removeFolder action from FolderActions.js. The button works and will call the function in FolderActions.js however will not dispatch the type. The functions works as I can see the console.log message but will not dispatch the type using redux..
I have a strong feeling it's the way I'm calling the function however I am lost at the moment
import {
addFolder,
getFolder,
removeFolder,
} from "../../../actions/FolderActions";
class Folders extends Component {
onRemoveFolder = (e,id) => {
e.preventDefault();
console.log(id);
this.props.removeFolder(id);
};
showFolders = () => {
return (
<ul>
{this.props.folder.map((key, index) => (
<form onSubmit={(e) => this.onRemoveFolder(e,key._id)}>
<input type="submit"></input>
</form>
))}
</ul>
);
};
render() {
let { isShown } = this.state;
return (
<div>
<div className="folder_names">{this.showFolders()}</div>
</div>
);
}
}
const mapStateToProps = state => {
return {
userId: state.auth.user._id,
folder: state.folder.data
};
};
export default connect(mapStateToProps, {removeFolder,addFolder,getFolder})(
Folders
);
FolderActions.js
export const removeFolder = id => dispatch => {
console.log("called")
axios
.delete(`api/folders/${id}`)
.then(res =>
dispatch({
type: DELETE_FOLDER,
payload: id
})
)
.catch(err => {
console.log(err);
});
};
Your function call looks strange to me...
Can you try defining a proper mapDispatchToProps and calling dispatch within that instead of within your function?
const mapDispatchToProps = dispatch => ({
removeFolder: (id) => dispatch( removeFolder(id) ),
addFolder: (id) => dispatch( addFolder(id) ),
getFolder: (id) => dispatch( getFolder(id) ),
})
export default connect(mapStateToProps, mapDispatchToProps)(
Folders
);
export const removeFolder = id => {
// code block
};
I know that's more a rework that you probably were hoping for, but does it work?
Correct me if I'm wrong but my server never sent a response.
Because the server never sent a response, when I made a request, res is not true so cannot dispatch the type and payload.
Sorry for wasting peoples time!
I'm trying to delete an item from a collection in Firestore by referencing the id of the selected item. I'm successfully passing on the id by mapDispatchToProps until the action but stops short when trying to delete in Firestore by the delete(). I think the problem may be in the my method to delete in firestore as it stops there. Can anyone kindly please tell me what could be wrong with my code?
import React from 'react'
import { connect } from 'react-redux'
import { firestoreConnect } from "react-redux-firebase";
import { compose } from 'redux'
import { Redirect } from 'react-router-dom'
import moment from 'moment'
import { deleteProject } from '../../store/actions/projectActions'
const handleClick = (e, prop) => {
e.preventDefault()
deleteProject(prop)
console.log(prop)
}
const ProjectDetails = (props) => {
const { auth, project } = props;
if (!auth.uid) return <Redirect to='/signin' />
if (project) {
return (
<div className="container section project-details">
<div className="card z-depth-0">
// content here
</div>
<button onClick={(e) => handleClick(e, props.id)}>Delete</button>
</div>
</div>
)
} else {
return (
<div className="container center">
<p>Loading...</p>
</div>
)
}
}
const mapStateToProps = (state, ownProps) => {
const id = ownProps.match.params.id;
const projects = state.firestore.data.projects;
const project = projects ? projects[id] : null
return {
project: project,
id: id,
auth: state.firebase.auth
}
}
const matchDispatchToProps = (dispatch) => {
return {
deleteProject: (id) => dispatch(deleteProject(id))
}
}
export default compose(
connect(mapStateToProps, matchDispatchToProps),
firestoreConnect([
{ collection: 'projects' }
])
)(ProjectDetails)
export const deleteProject = (id) => {
console.log("dispatch", id) \\ successfully shows "dispatch", id
return(dispatch, getState, {getFirestore}) => {
const firestore = getFirestore();
firestore.collection('projects').doc(id).delete()
.then(() => {
console.log('deleted') \\ does not show deleted here
dispatch({ type: 'DELETE_PROJECT_SUCCESS' });
}).catch(err => {
dispatch({ type: 'DELETE_PROJECT_ERROR' });
})
}
}
You are calling the imported version of deleteProject rather than the mapDispatchToProps version. This is a common gotcha.
One way to fix this (and prevent it happening in future) is to rename your action in your mapDispatchToProps to something different:
const matchDispatchToProps = (dispatch) => {
return {
dispatchDeleteProject: (e, id) => {
e.preventDefault()
dispatch(deleteProject(id))
})
}
}
Then you can destructure this out of your props and call it:
const ProjectDetails = (props) => {
const { auth, project, dispatchDeleteProject } = props;
if (!auth.uid) return <Redirect to='/signin' />
if (project) {
return (
<div className="container section project-details">
<div className="card z-depth-0">
// content here
</div>
<button onClick={e=>dispatchDeleteProject(e, props.id)}>Delete</button>
</div>
</div>
)
}
This is happening because your action deleteProject is not getting called from redux's dispatch.
If you will observer correctly, in your handleClick function, you are calling deleteProject function function action directly.
handleClick function should call deleteProject function from prop like this.
Your handleClick function should be -
const handleClick = (e, id, deleteProject) => { // passing deleteProject function from prop
e.preventDefault()
deleteProject(id)
console.log(id)
}
You HTML should be -
<button onClick={(e) => handleClick(e, props.id, props.deleteProject)}>Delete</button>