I am trying to fetch some data from a firebase collection, however I cannot seem to get it to work properly.
My code is as follows:-
component:-
import React, { Component } from 'react'
import { connect } from 'react-redux';
import { fetchPlayers } from '../../redux/players/players.actions';
class Players extends Component {
componentDidMount() {
this.props.fetchPlayers();
}
render() {
const { players } = this.props;
// const playerItems = players.map(player => (
// <div key={player.id}>
// <h3>{player.name}</h3>
// </div>
// ));
return (
<div>
<h1>PLAYERS</h1>
{/* {playerItems} */}
</div>
)
}
}
const mapStateToProps = state => ({
players: state.players.items,
newPlayer: state.players.item
});
export default connect(mapStateToProps, {fetchPlayers})(Players);
My actions file looks like this :-
import { FETCH_PLAYERS, NEW_PLAYER } from "./players.types";
import { getPlayers, addPlayer } from '../../db/firebase/players.firebase';
// this syntax below will return the dispatch (to the reducer)
export const fetchPlayers = () => dispatch => {
const players = getPlayers();
dispatch({
type: FETCH_PLAYERS,
payload: players
});
};
export const createPlayer = playerToAdd => dispatch => {
addPlayer(playerToAdd);
dispatch({
type: NEW_PLAYER,
payload: playerToAdd
});
};
And my db function is as follows :-
import firebase from "../../config/firebase";
const db = firebase.firestore();
export const getPlayers = () => {
const players = fetchData();
return players;
}
export const addPlayer = (playerToAdd) => {
db.collection("players").add({ ...playerToAdd });
}
const fetchData = async () => {
let playerArr = [];
await db.collection("players")
.get()
.then(function(querySnapshot) {
querySnapshot.forEach(function(doc) {
playerArr = playerArr.concat(doc.data());
});
})
.catch(function(error) {
console.log("Error getting documents: ", error);
});
return playerArr;
}
My problem is that in my component, the players remains as a promise, and the fetchData() db call is being returned after.
How can I await the array to be populate first (in the fetchData()) and then pass this to my component through the actions?
Thanks for your help, very much appreciated!
I have managed to fix it in this way, not sure if this is the correct way though, still fairly new to Redux:-
component:-
import React, { Component } from 'react'
import { connect } from 'react-redux';
import { fetchPlayers } from '../../redux/players/players.actions';
class Players extends Component {
componentDidMount() {
this.props.fetchPlayers();
}
render() {
const {players} = this.props;
let playerItems = ''
if (players) {
playerItems = players.map(player => (
<div key={player.id}>
<h3>{player.name}</h3>
</div>
));
}
return (
<div>
<h1>PLAYERS</h1>
{playerItems}
</div>
)
}
}
const mapStateToProps = state => ({
players: state.players.items,
newPlayer: state.players.item
});
export default connect(mapStateToProps, {fetchPlayers})(Players);
actions:-
import { FETCH_PLAYERS, NEW_PLAYER } from "./players.types";
import { getPlayers, addPlayer } from '../../db/firebase/players.firebase';
// this syntax below will return the dispatch (to the reducer)
export const fetchPlayers = () => async dispatch => {
const players = await getPlayers();
dispatch({
type: FETCH_PLAYERS,
payload: players
});
};
export const createPlayer = playerToAdd => dispatch => {
addPlayer(playerToAdd);
dispatch({
type: NEW_PLAYER,
payload: playerToAdd
});
};
firebase:-
import firebase from "../../config/firebase";
const db = firebase.firestore();
export const getPlayers = async () => {
const players = await fetchData();
return players;
}
export const addPlayer = (playerToAdd) => {
db.collection("players").add({ ...playerToAdd });
}
const fetchData = async () => {
let playerArr = [];
await db.collection("players")
.get()
.then(function(querySnapshot) {
querySnapshot.forEach(function(doc) {
playerArr = playerArr.concat(doc.data());
});
})
.catch(function(error) {
console.log("Error getting documents: ", error);
});
return playerArr;
}
In this way its working however if you have any suggestions, more than welcome to listen to.
Thanks
Related
I have this custom http hook with abort when you try to go to a different page (I saw it in a tutorial but I am not truly sure I need it). When I fetch data with it and useEffect(), I have this error on the backend but the request is executed and everything is as planned. My question is, how to improve my code so it does not throw this error and do I need this functionality with abortController() ?
http-hook.ts
import { useCallback, useRef, useEffect } from "react";
import { useSelector } from "react-redux";
import { useDispatch } from "react-redux";
import { selectError, showError } from "src/redux/error";
import { selectLoading, startLoading, stopLoading } from "src/redux/loading";
export const useHttpClient = () => {
const dispatch = useDispatch();
const error = useSelector(selectError);
const loading = useSelector(selectLoading);
const activeHttpRequests: any = useRef([]);
const sendRequest = useCallback(
async (url, method = "GET", body = null, headers = {}) => {
dispatch(startLoading());
const httpAbortCtrl = new AbortController();
activeHttpRequests.current.push(httpAbortCtrl);
try {
const response = await fetch(url, {
method,
body,
headers,
signal: httpAbortCtrl.signal,
});
const responseData = await response.json();
activeHttpRequests.current = activeHttpRequests.current.filter(
(reqCtrl) => reqCtrl !== httpAbortCtrl
);
if (!response.ok) {
throw new Error(responseData.message);
}
dispatch(stopLoading());
return responseData;
} catch (err) {
dispatch(showError(err.message));
dispatch(stopLoading());
throw err;
}
},
[]
);
useEffect(() => {
return () => {
activeHttpRequests.current.forEach((abortCtrl: any) => abortCtrl.abort());
};
}, []);
return { loading, error, sendRequest };
};
UserInfo.tsx
import React, { Fragment, useEffect, useState } from "react";
import { useParams } from "react-router-dom";
import { useHttpClient } from "src/hooks/http-hook";
import classes from "./UserInfo.module.css";
const UserInfo = () => {
const { sendRequest } = useHttpClient();
const [currentUser, setCurrentUser] = useState<any>();
const userId = useParams<any>().userId;
useEffect(() => {
const fetchCurrentUser = async () => {
try {
const responseData = await sendRequest(
`http://localhost:5000/api/user/${userId}`
);
setCurrentUser(responseData.user);
console.log("currentUser ", currentUser);
} catch (err) {
console.log(err);
}
};
fetchCurrentUser();
}, [sendRequest ,userId]);
return currentUser ? (
<Fragment>
<div className={classes.cover} />
<div className={classes.user_info}>
<img
alt="user_img"
src={`http://localhost:5000/${currentUser.image}`}
className={classes.user_img}
/>
<div className={classes.text}>
<p>
Name: {currentUser.name} {currentUser.surname}
</p>
<p>Email: {currentUser.email}</p>
<p>Age: {currentUser.age}</p>
</div>
</div>{" "}
</Fragment>
) : (
<p>No current user</p>
);
};
export default UserInfo;
Backend
getCurrentUser.ts controller
const getCurrentUser = async (
req: express.Request,
res: express.Response,
next: express.NextFunction
) => {
const userId = req.params.userId;
let user;
try {
user = await User.findById(userId);
} catch (err) {
const error = new HttpError("Could not fetch user", 500);
return next(error);
}
res.json({ user: user.toObject({ getters: true }) });
};
I'm building a simple venue review app using react/redux toolkit/firebase.
My Firestore has one collection, venues , and each document within this collection is structured as follows:
name:'',
photo:'',
reviews: [
{
title:'',
blurb:'',
reviewId:''
},
{...}
]
My app has one slice to control all the state related to venues, including the reviews - but should reviews have their own slice?
I'm asking this because my app is displaying glitchy behaving including crashing on page refresh, and not updating the components when new state is added. Is how I'm handling reviews responsible for causing the problems in my app, and if so, how do I handle it?
Currently my venue state is managed as follows:
Venues fetched in VenueList.js...
useEffect(() => {
dispatch(fetchVenues());
}, [dispatch]);
...which triggers thunk in venueSlice.js
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);
}
});
Reviews added in AddReview.js
const handleClick = (e) => {
e.preventDefault();
if (title && blurb) {
const reviewId = nanoid()
const review = { id, title, blurb, reviewId };
dispatch(postReview(review));
}
};
...handled with another thunk in venueSlice.js
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)
}
})
Note: Reviews posted to firestore without updating redux global state:
const venueSlice = createSlice({
name: "venues",
initialState,
reducers: {},
extraReducers(builder) {
builder
.addCase(fetchVenues.fulfilled, (state, action) => {
state.venues = action.payload;
})
},
});
I extract venue state into Venue.js. Venue state (containing reviews) is then passed as props to Reviews.js, where reviews are added/deleted.
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);
console.log(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;
Reviews.js
import { useDispatch } from "react-redux";
import { deleteReview,fetchVenues } from "../features/venues/venueSlice";
import { useSelector } from "react-redux";
import { useParams } from "react-router-dom";
const Reviews = ({venue}) => {
const venueId = venue[0].id
const dispatch = useDispatch()
const removeReview = (review) => {
dispatch(deleteReview({...review, id:venueId}))
}
const content = venue && venue[0].reviews && venue[0].reviews.map(review => (
<div className="review" key = {review.reviewId}>
<h2>{review.title}</h2>
<h3>{review.blurb}</h3>
<div>
<p>Edit</p>
<button onClick = {() => removeReview(review)}>Delete</button>
</div>
</div>
))
return (
<div className="all-reviews">
{content}
</div>
)
}
export default Reviews;
Here's the entire venueSlice.js file
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
Hey folks really hope someone can help me here. I'm successfully updating my object in my mongo cluster, it updates but it does not render that update straight away to the browser. It will only update after a reload or when I run my update function again, it doesn't fetch that update straight away and I can't understand why. Does anyone have any suggestions?
I'm using context and reducer.
PlantDetails
import { usePlantsContext } from "../hooks/usePlantsContext";
import formatDistanceToNow from "date-fns/formatDistanceToNow";
import { useState } from "react";
import CalendarComponent from "./CalendarComponent";
const PlantDetails = ({ plant }) => {
const [watered, setWatered] = useState(false)
const [newWaterDate, setNewWaterDate] = useState("")
const { dispatch } = usePlantsContext();
const handleClick = async () => {
const response = await fetch("/api/plants/" + plant._id, {
method: "DELETE",
});
const json = await response.json();
if (response.ok) {
dispatch({ type: "DELETE_PLANT", payload: json });
}
};
const updatePlant = async (e) => {
e.preventDefault()
plant.nextWaterDate = newWaterDate
const response = await fetch("api/plants/" + plant._id, {
method: "PATCH",
body: JSON.stringify(plant),
headers: {
'Content-Type': 'application/json'
}
})
const json = await response.json()
if(response.ok) {
dispatch({ type: "UPDATE_PLANT", payload: json })
}
console.log('updated')
setWatered(false)
}
return (
<div className="plant-details">
<h4>{plant.plantName}</h4>
<p>{plant.quickInfo}</p>
<p>
{formatDistanceToNow(new Date(plant.createdAt), { addSuffix: true })}
</p>
<span onClick={handleClick}>delete</span>
<div>
<p>next water date: {plant.nextWaterDate}</p>
<input onChange={(e) => setNewWaterDate(e.target.value)}/>
<button onClick={updatePlant}>update</button>
<input value={watered} type="checkbox" id="toWater" onChange={() => setWatered(true)}/>
<label for="toWater">watered</label>
{watered && <CalendarComponent updatePlant={updatePlant} setNextWaterDate={setNewWaterDate}/>}
</div>
</div>
);
};
export default PlantDetails;
Context which wraps my
import { createContext, useReducer } from 'react'
export const PlantsContext = createContext()
export const plantsReducer = (state, action) => {
switch(action.type) {
case 'SET_PLANTS':
return {
plants: action.payload
}
case 'CREATE_PLANT':
return {
plants: [action.payload, ...state.plants]
}
case 'DELETE_PLANT':
return {
plants: state.plants.filter((p) => p._id !== action.payload._id)
}
case 'UPDATE_PLANT':
return {
plants: state.plants.map((p) => p._id === action.payload._id ? action.payload : p )
}
default:
return state
}
}
export const PlantsContextProvider = ({ children }) => {
const [state, dispatch] = useReducer(plantsReducer, {
plants: null
})
return (
<PlantsContext.Provider value={{...state, dispatch}}>
{ children }
</PlantsContext.Provider>
)
}
My plantController (update)
const updatePlant = async (req, res) => {
const { id } = req.params
if(!mongoose.Types.ObjectId.isValid(id)) {
return res.status(404).json({ error: "No plant" })
}
const plant = await Plant.findByIdAndUpdate({ _id: id }, {
...req.body
})
if (!plant) {
return res.status(400).json({ error: "No plant" })
}
res.status(200)
.json(plant)
}
Home component
import { useEffect } from "react";
import PlantDetails from "../components/PlantDetails";
import PlantForm from "../components/PlantForm";
import CalendarComponent from "../components/CalendarComponent";
import { usePlantsContext } from "../hooks/usePlantsContext";
const Home = () => {
const { plants, dispatch } = usePlantsContext();
useEffect(() => {
const fetchPlants = async () => {
console.log("called");
// ONLY FOR DEVELOPMENT!
const response = await fetch("/api/plants");
const json = await response.json();
if (response.ok) {
dispatch({ type: "SET_PLANTS", payload: json });
}
};
fetchPlants();
}, [dispatch]);
return (
<div className="home">
<div className="plants">
{plants &&
plants.map((plant) => <PlantDetails key={plant._id} plant={plant} />)}
</div>
<PlantForm />
</div>
);
};
export default Home;
Any help would be greatly appreciated.
My patch requests were going through smoothly but my state would not update until I reloaded my page. It was not returning the document after the update was applied.
https://mongoosejs.com/docs/tutorials/findoneandupdate.html#:~:text=%3B%20//%2059-,You,-should%20set%20the
I have axios making a get request to fetch a request a list of vessels and their information,
i am trying to use redux slice, and populate the data using dispute, however the state is always empty dispute not having any errors
the vesselSlice :
import { createSlice } from "#reduxjs/toolkit";
import { api } from "../components/pages/screens/HomeScreen";
const vesselSlice = createSlice({
name: "vessels",
initialState: {
vessels: [],
},
reducers: {
getVessels: (state, action) => {
state.vessels = action.payload;
},
},
});
export const vesselReducer = vesselSlice.reducer;
const { getVessels } = vesselSlice.actions;
export const fetchVessels = () => async (dispatch) => {
try {
await api
.get("/vessels")
.then((response) => dispatch(getVessels(response.data)));
} catch (e) {
return console.error(e.message);
}
};
the HomeScreen.js :
import React, { useEffect } from "react";
import VesselCard from "../../VesselCard";
import axios from "axios";
import { useDispatch, useSelector } from "react-redux";
import { fetchVessels } from "../../../features/vesselSlice";
export const api = () => {
axios
.get("http://127.0.0.1:8000/api/vessels/info")
.then((data) => console.log(data.data))
.catch((error) => console.log(error));
};
function HomeScreen() {
const { vessels, isLoading } = useSelector((state) => state.vessels);
const dispatch = useDispatch();
// This part:
useEffect(() => {
fetchVessels(dispatch);
}, [dispatch]);
return (
<div>
Fleet vessels :
<div className="fleet-vessels-info">
{vessels.map((vessel) => (
<VesselCard vessel={vessel} />
))}
</div>
</div>
);
}
export default HomeScreen;
You receive dispatch this way. It is not being received from any middleware.
export const fetchVessels = async (dispatch) =>{}
If you want to continue using your approach, call function this way
useEffect(() => {
fetchVessels()(dispatch);
}, [dispatch]);
Api function error
export const api = async () => {
try {
const res = await axios.get("http://127.0.0.1:8000/api/vessels/info");
return res.data;
} catch (error) {
console.log(error);
}
};
export const fetchVessels = () => async (dispatch) => {
try {
await api()
.then((data) => dispatch(getVessels(data)));
} catch (e) {
return console.error(e.message);
}
};
I was searching for a loading spinner for react+redux and came across the react-redux-spinner library. I included it in my project, added the reducer, called [pendingTask]: begin/end in my actions, added the Spinner component to render, but it just won't show at all, even though in the redux logs I can see that pending tasks in the store are incremented and decremented accordingly to the action called. Here is some of my code:
store:
const rootReducer = combineReducers({
pendingTasks: pendingTasksReducer
// other reducers
});
const store = createStore(rootReducer, /* middlewares */);
export default store;
actions
export const fetchData = params => {
const request = params => ({
type: 'FETCH_REQUEST',
[pendingTask]: begin,
payload: { params }
});
const success = data => ({
type: 'FETCH_SUCCESS',
[pendingTask]: end,
payload: { data }
});
const failure = error => ({
type: 'FETCH_FAILURE',
[pendingTask]: end,
payload: { error }
});
return async dispatch => {
dispatch(request(params));
try {
const res = await service.fetchData(params);
dispatch(success(res.data));
return res.data;
} catch (e) {
const msg = e.toString();
dispatch(failure(msg));
return Promise.reject(msg);
}
}
}
page
const Page = props => {
const { data } = props;
useEffect(() => {
async function fetchData(params) {
try {
await props.fetchData(params);
} catch (e) {
console.log(e);
}
}
fetchData(data.params);
}
return (
<div className="wrapper">
{
data.map(({ field1, field2 }, key) => ({
<div>{field1}: {field2}</div>
}));
}
</div>
);
};
const mapStateToProps = state => {
const { data } = state;
return { data };
};
const actionCreators = {
fetchData: actions.fetchData
};
export default connect(mapStateToProps, actionCreators)(Page);
app component
export const App = props => {
return (
<main className="App">
<Spinner config={{ trickeRate: 0.02 }} />
<Page/>
</main>
);
}
I've double-checked that I use the correct names for the store and for the actions, and they do fire up - but the spinner itself never gets rendered on the page at all, even though with each action the pendingTasks value change. What could I possibly do wrong or miss here? Infinitely grateful in advance for pointing out!