I'm building a simple venue review app using react/redux/firebase.
On the homepage are a list of venues; when you click on a specific venue it routes the user to a page specifically about the venue (Venue.js).
Problem: Venue.js renders correctly upon first render, but crashes the entire app when I refresh the page. It also only renders when the user clicks on link to the specific venue - if I try to reach the page by entering the venue page URL, it crashes.
I've tried using optional chaining in Venue.js (as follows) but it hasn't worked
changed
const venue = venues.venues.filter((item) => item.id === id);
to
const venue = venues?.venues?.filter((item) => item.id === id);
Any ideas on how to fix this? I figured it's an async problem, or maybe I'm using useEffect incorrectly.
venueSlice.js : Manages state for venues and API calls to firebase
import { createSlice,createAsyncThunk } from "#reduxjs/toolkit";
import { collection,query,getDocs,doc,updateDoc,arrayUnion } 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)
}
})
const venueSlice = createSlice({
name: "venues",
initialState,
reducers: {},
extraReducers(builder) {
builder
.addCase(fetchVenues.fulfilled, (state, action) => {
state.venues = action.payload;
})
},
});
export default venueSlice.reducer
VenueList.js : where all venues are listed
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"}}>
<div className="venue-item">
<h2>{venue.name}</h2>
<img src={venue.photo} />
</div>
</Link>
));
return (
<div className="venue-list">
{content}
</div>
);
};
export default VenueList;
Venue.js : where specific venue information is provided
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);
const content = venue.map((item) => (
<div className="venue-page-main">
<h2>{item.name}</h2>
<img src={item.photo} alt = "venue"/>
</div>
));
return (
<>
{content}
<AddReview id = {id}/>
<Reviews venue = {venue}/>
</>
);
};
export default Venue;
App.js
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
import Venue from "./features/venues/Venue";
import VenueList from "./features/venues/VenueList";
import AddReview from "./components/AddReview";
import './styles.css'
const App = () => {
return (
<Router>
<Routes>
<Route path = '/' element = {<VenueList/>}/>
<Route path = '/add-review' element = {<AddReview/>}/>
<Route path = '/venue/:id' element = {<Venue/>}/>
</Routes>
</Router>
);
}
export default App;
Related
I'm making a venue review app with react/redux toolkit/firebase.
The user can submit, edit, and delete reviews for a particular venue. The problem I have is that CRUD operations for reviews are not instantly shown in the component - I have to refresh the page to see the changes.
I suspect this it's because how I've structured my firestore database, and/or with how I'm handling state in the front end.
My firebase db is structured as follows:
There's one collection "venues", with documents that have the following shape:
name:'',
photo:'',
reviews: [
{
title:'',
blurb:'',
reviewId:''
},
{...}
]
In my front end, I'm fetching all the "venues" documents in venueSlice.js :
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: [],
isLoading: true
}
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);
}
});
...
const venueSlice = createSlice({
name: "venues",
initialState,
reducers: {},
extraReducers(builder) {
builder
.addCase(fetchVenues.fulfilled, (state, action) => {
state.isLoading = false
state.venues = action.payload;
})
.addCase(fetchVenues.pending, (state) => {
state.isLoading = true
})
.addCase(fetchVenues.rejected, (state) => {
state.isLoading = false
})
},
});
As as example of CRUD operation, I'm deleting reviews in Reviews.js
import { useDispatch } from "react-redux";
import { deleteReview,fetchVenues } from "../features/venues/venueSlice";
import { Link } from "react-router-dom";
const Reviews = ({ venue }) => {
const dispatch = useDispatch()
const venueId = venue[0]?.id
const removeReview = (review) => {
dispatch(deleteReview({...review, id:venueId}))
}
const content = venue[0]?.reviews.map(review => (
<div className="review" key = {review.reviewId}>
<h2>{review.title}</h2>
<h3>{review.blurb}</h3>
<div>
<Link to = {`/venue/${venue[0].id}/${review.reviewId}/edit`}><button>Edit</button></Link>
<button onClick = {() => removeReview(review)}>Delete</button>
</div>
</div>
))
return (
<div className="all-reviews">
{content}
</div>
)
}
export default Reviews;
....which is handled by a thunk in venueSlice.js
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)
}
})
Lastly, venues are fetched in App.js
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
import { useEffect } from "react";
import { useSelector,useDispatch } from "react-redux";
import { fetchVenues,fetchReviews } from "./features/venues/venueSlice";
import Venue from "./features/venues/Venue";
import VenueList from "./features/venues/VenueList";
import AddReview from "./components/AddReview";
import EditReview from "./components/EditReview";
import './styles.css'
const App = () => {
const dispatch = useDispatch()
useEffect(() => {
dispatch(fetchVenues())
},[])
return (
<Router>
<Routes>
<Route path = '/' element = {<VenueList/>}/>
<Route path = '/add-review' element = {<AddReview/>}/>
<Route path = '/venue/:id' element = {<Venue/>}/>
<Route path = '/venue/:id/:reviewId/edit' element = {<EditReview/>}/>
</Routes>
</Router>
);
}
export default App;
I was thinking the problem is this: the venues collection is fetched and set to global state, and my application will respond to changes in venue state - but not changes of state within the reviews nested array. A possible solution would be to make a new "reviews" collection, or make a reviews subcollection in every venue document.
Suggestions?
Venues are not fetched in App.js after you delete a review. useEffect's dependency array is empty, which means useEffect's effect (first argument's body) only gets executed when the component mounts.
One possible solution is fetching venues right after successfully deleting a review.
To get realtime updates, you can use onSnapshot (docs) instead of getDocs
I am trying to create a favourite functionality, for now I am trying to get the recipe by it's id and then save the data in const variables and then create a new recipe with the same data and add it to the favourites data collection. It adds a recipe to the favourites page, but it doesn't show the data, it shows only the html model of the recipe. I hope that's clear enough.
import "./RecipeModel.css";
import { Link, NavLink, useNavigate, useParams } from 'react-router-dom';
import { BrowserRouter,Route,Routes, Switch, Redirect } from 'react-router-dom';
import React, { useLayoutEffect, useState,useEffect, useContext } from 'react';
import * as userService from '../../../services/userService';
import { AuthContext } from "../../../contexts/AuthContext";
const RecipeModel = ({
recipe
}) => {
const history = useNavigate();
const {user} = useContext(AuthContext);
const {recipeId} = useParams();
const [recipec,setRecipe] = useState();
useEffect(() => {
userService.getOne(recipeId)
.then(result => {
setRecipe(result);
})
},[recipeId]);
const HandleFavourite = (e) => {
e.preventDefault();
// const recipesd = async () => {
// let reciperes = await userService.getOne(recipeId);
// console.log(reciperes);
// setRecipe(reciperes);
// };
// useEffect(() => { recipesd(recipec); },[]);
console.log(`Recipe ${recipec._id}`);
// let recipeData = Object.fromEntries(new FormData(e.currentTarget))
// userService.addFavourite(recipe._id, recipeData);
// history('/favourites');
// const formData = new FormData(e.currentTarget);
const name = recipec.name;
const time = recipec.time;
const imageUrl = recipec.imageUrl;
const ingredients = recipec.ingredients;
const instructions = recipec.instructions;
userService.addFavourite({
name,
time,
imageUrl,
ingredients,
instructions
},user.accessToken)
.then(result => {
console.log(result);
history('/favourites');
})
}
return (
<article className="articles">
<img className="img2" src={recipe.imageUrl}/>
<h1>{recipe.name}</h1>
<p className="cut-text">{recipe.instructions}</p>
<div className="btns1">
<Link smooth= "true" className="btnd" to={`/recipe-details/recipe-number:${recipe._id}`}>Details</Link>
<button className="favour" onClick={HandleFavourite} ><i className="fas fa-solid fa-heart-circle-plus"></i></button>
</div>
</article>
);
};
export default RecipeModel;
The method to getOne works fine for the other data collections I fetched.
That's the Favourites component
import "./Favourites.css";
import { Link, NavLink } from 'react-router-dom';
import { BrowserRouter,Route,Routes, Switch, Redirect } from 'react-router-dom';
import RecipeList from "../RecipeList/RecipeList";
import CreateRecipe from "../Recipe/CreateRecipe/CreateRecipe";
import React, { useLayoutEffect, useState ,useEffect} from 'react';
import RecipeFavourite from "../Recipe/RecipeFavourite/RecipeFavourite";
import * as recipeService from "../../services/recipeService";
import * as userService from '../../services/userService';
function Favourites() {
// const [recipes,setRecipes] = useState([]);
// useEffect(() => {
// recipeService.getAll()
// .then(result => {
// setRecipes(result);
// })
// },[]);
const [favourites,setFavourites] = useState([]);
useEffect(() => {
userService.getAllFavourites()
.then(result => {
setFavourites(result);
})
},[]);
useLayoutEffect(() => {
window.scrollTo(0, 0)
});
const scrollToTop = () => {
window.scrollTo(0, 0)
}
return (
<div>
<h1 className = "h1m">Your Favourite Recipes</h1>
<section className="secfav">
{favourites.map(x => <RecipeFavourite key ={x._id} recipe={x} />)}
{/* {recipes.map(x => <RecipeFavourite key ={x._id} recipe={x}/>)} */}
</section>
</div>
);
}
export default Favourites;
Thats's the RecipeFavourite component
import "./RecipeFavourite.css";
import { Link, NavLink } from 'react-router-dom';
import { BrowserRouter,Route,Routes, Switch, Redirect } from 'react-router-dom';
import React, { useLayoutEffect, useState } from 'react';
const RecipeFavourite = ({recipe,recipes}) => {
return (
<article className="articles">
<img className="img4" src={recipe.imageUrl}/>
<h1>{recipe.name}</h1>
<p className="cut-text">{recipe.instructions}</p>
<div className="btns1">
<Link smooth= "true" className="btnd" to={`/recipe-details/recipe-number:${recipe._id}`}>Details</Link>
<button className="favnon"><i className="fas fa-solid fa-heart-circle-minus"></i></button>
</div>
</article>
);
};
export default RecipeFavourite;
Thats the AuthContext logic
import { createContext, useContext } from 'react';
export const AuthContext = createContext();
The userService logic
export const addFavourite = async (recipeData,token) => {
let response = await fetch(`${baseUrl}/favourites`, {
method: 'POST',
headers: {
'content-type': 'application/json',
'X-Authorization': token,
},
body: JSON.stringify(recipeData)
});
let result = await response.json();
return result;
};
export const getAllFavourites = async () => {
let response = await fetch(`${baseUrl}/favourites`)
let recipes = await response.json();
let result = Object.values(recipes);
return result;
}
Good evening! I am creating simple React app using TS + React Query + Recoil. App is about 'online library'. I would like to create pagination and search input (to find specific author or title).
My idea was, when app starts I am fetching data from page 1. Then when I'll click 2nd button on my pagination bar I'll fetch data from page 2 etc. Code looks like this:
Main component
import { useGetBooks } from '../../hooks/useGetBooks';
import { BookType } from '../../types/Book';
import { SingleBook } from './SingleBook';
import styled from 'styled-components';
import { Navbar } from './Navbar';
import { Loader } from '../utilities/Loader';
import { Error } from '../utilities/Error';
import { useState } from 'react';
import { useRecoilState } from 'recoil';
import { Books } from '../../recoil/globalState';
type bookType = BookType;
export const BookList = () => {
const [pageNumber, setPageNumber] = useState(1);
const [books, setBooks] = useRecoilState(Books);
const { isLoading, isError } = useGetBooks(pageNumber, setBooks);
if (isLoading) {
return <Loader isLoading={isLoading} />
}
if (isError) {
return <Error />
}
const displayBooks = books.data.map((book: bookType) => {
return (
<SingleBook key={book.id} book={book} />
)
})
return (
<BookContainer>
<div className='test'>
<button onClick={() => setPageNumber((page) => page - 1)} disabled={pageNumber == 1}>Prev page</button>
<p>{books.metadata.page}</p>
<button onClick={() => setPageNumber((page) => page + 1)} disabled={books.metadata.records_per_page * books.metadata.page > books.metadata.total_records}>Next page</button>
</div>
<Navbar />
<BookContent>
{displayBooks}
</BookContent>
</BookContainer>
)
}
React query:
import { useQuery } from "react-query";
import axios from 'axios';
const fetchBooks = async (pageNumber: number) => {
const res = await axios.get(`http://localhost:3001/api/book?page=${pageNumber}`);
return res.data
}
export const useGetBooks = (pageNumber: number, setBooks: any) => {
return useQuery(['books', pageNumber], () => fetchBooks(pageNumber),
{
onSuccess: (data) => setBooks(data),
keepPreviousData: true
})
}
Recoil:
import { atom } from 'recoil';
export const Books = atom({
key: 'book',
default: [] as any
})
And books response:
books: {
data: [
{
author: 'Some crazy',
title: 'Some crazy'
},
{
author: 'Some crazy1',
title: 'Some crazy1'
},
],
metadata: {
page: 1,
records_per_page: 10,
total_records: 17
}
}
Search Input Implementation:
import React, { useState } from "react"
import { useGetFilteredBooks } from "../../hooks/useGetFilteredBooks"
import { useRecoilState } from "recoil"
import { Books } from "../../recoil/globalState"
export const SearchBook = () => {
const [text, setText] = useState('')
const [books, setBooks] = useRecoilState(Books);
const { data, refetch } = useGetFilteredBooks(text, setBooks);
const handleOnChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setText(event.target.value)
}
const handleOnSubmit = (e: any) => {
e.preventDefault();
refetch();
}
return (
<>
<form onSubmit={handleOnSubmit}>
<Input value={text} onChange={handleOnChange} placeholder="Enter the name of the book or author" />
<button type="submit">Show</button>
</form>
<button onClick={() => console.log(books)}>plasda</button>
</>
)
}
React query:
import { useQuery } from "react-query";
import axios from 'axios';
const fetchFilteredsBooks = async (searchText: string) => {
const res = await axios.get(`http://localhost:3001/api/book?search=${searchText}`);
return res.data
}
export const useGetFilteredBooks = (searchText: string, setBooks: any) => {
return useQuery(['filteredBooks', searchText], () => fetchFilteredsBooks(searchText),
{
onSuccess: (data) => setBooks(data),
enabled: false
})
}
We can only display 10 items per 1 page.
PROBLEM:
When we search something and we get data back, we can have scenario, when data will need to be display not on 1 page. So when we have filtered data, and we click 2nd button on pagination, the filtered data will disapeared and we see not filtered data from page 2
i´m trying to fetch all the product i have in my DB (mongodb) using the fetch API. The result i get i store in a slice using Redux Toolkit. The problem is when i pass the data fetched and stored to a component as a props, the result is not beeing displayed.
slice
import { createSlice } from "#reduxjs/toolkit";
const initialState = {
products: [],
};
const productSlice = createSlice({
name: "product",
initialState,
reducers: {
setProducts(state, action) {
state.products.push(action.payload);
},
},
});
export const { setProducts } = productSlice.actions;
export default productSlice.reducer;
store
import { configureStore } from "#reduxjs/toolkit";
import uiSlice from "./ui-slice";
import userSlice from "./user-slice";
import productSlice from './product-slice';
const store = configureStore({
reducer: {
ui: uiSlice,
user: userSlice,
products: productSlice
},
});
export default store;
function i used to fetch the products
export const fetchProduct = () => {
return async (dispatch) => {
try {
const response = await fetch("http://localhost:xx/xxx");
const data = await response.json();
let loadedProducts = [];
for (const key in data) {
loadedProducts.push({
id: data[key]._id,
productName: data[key].productName,
price: data[key].price,
imageUrl: data[key].imageUrl,
});
}
dispatch(setProducts(loadedProducts));
} catch (error) {
console.log(error);
}
};
I get the value stored in my redux state with useSelector and use it to fetch the products
import { useEffect, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { Container } from "react-bootstrap";
import { fetchProduct } from "../../actions/productActions";
import Hero from "../hero/Hero";
import Footer from "../footer/Footer";
import DisplayProductsList from "../displayProduct/DisplayProductsList";
export default function Home() {
const productsInfo = useSelector((state) => state.products.products);
const dispatch = useDispatch();
useEffect(() => {
dispatch(fetchProduct());
}, []);
return (
<>
<Hero />
<DisplayProductsList products={productsInfo} />
<Container fluid>
<Footer></Footer>
</Container>
</>
);
}
And then i map it
export default function DisplayProductsList(props) {
console.log(props)
return (
<ul>
{props.products.map((product) => (
<DisplayProducts
key={product.id}
imageUrl={product.imageUrl}
name={product.productName}
price={product.price}
/>
))}
</ul>
);
}
console log the props i sent
if i console log the state in the selector this is what i get
code in the backend
module.exports.fetchProduct = async (req, res) => {
try {
const products = await Product.find({});
console.log(products)
if (products) {
res.json(products);
}
} catch (error) {
console.log(error);
}
};
As I can see in your code, you are storing an array of objects i.e. 'loadedProducts' into 'products' which is also an array. Something like this:
nested array
So in order to get the productsInfo in DisplayProductsList component, instead of doing this:
{props.products.map((product) => (
<DisplayProducts
key={product.id}
imageUrl={product.imageUrl}
name={product.productName}
price={product.price}
/>
))}
you should do something like this:
{props.products[0].map((product) => (
//rest of your code
))}
I just can't decide the pattern I want to follow.
I'm implementing what I call a UserParent component. Basically a list of users and when you click on a user, it loads their resources.
Approach 1: Redux
import React, { useEffect, useState } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { NavList } from '../nav/NavList'
import { ResourceList } from '../resource/ResourceList'
import { getUserResources, clearResources } from './userSlice'
import CircularProgress from '#mui/material/CircularProgress';
import { getAllUsers } from './userSlice';
export const UserParent = () => {
const users = useSelector((state) => state.users.users )
const resources = useSelector((state) => state.users.user.resources )
const [highLightedUsers, setHighLightedItems] = useState([]);
const isLoading = useSelector((state) => state.users.isLoading)
let dispatch = useDispatch();
useEffect(() => {
dispatch(getAllUsers());
}, [])
const onUserClick = (user) => {
if (highLightedUsers.includes(user.label)) {
setHighLightedItems([])
dispatch(clearResources())
} else {
setHighLightedItems([...highLightedUsers, user.label])
dispatch(getUserResources(user.id))
}
}
return(
<>
{ isLoading === undefined || isLoading ? <CircularProgress className="search-loader" /> :
<div className="search-container">
<div className="search-nav">
<NavList
items={users}
onItemClick={onUserClick}
highLightedItems={highLightedUsers}
/>
</div>
<div className="search-results">
<ResourceList resources={resources} />
</div>
</div> }
</>
)
}
And then we have the reducer code:
import { createSlice } from '#reduxjs/toolkit';
import Api from '../../services/api';
const INITIAL_STATE = {
users: [],
isLoading: true,
user: { resources: [] }
};
export const userSlice = createSlice({
name: 'users',
initialState: INITIAL_STATE,
reducers: {
loadAllUsers: (state, action) => ({
...state,
users: action.payload,
isLoading: false
}),
toggleUserLoader: (state, action) => ({
...state,
isLoading: action.payload
}),
loadUserResources: (state, action) => ({
...state, user: { resources: action.payload }
}),
clearResources: (state) => ({
...state,
isLoading: false,
user: { resources: [] }
})
}
});
export const {
loadAllUsers,
toggleUserLoader,
loadUserResources,
clearResources
} = userSlice.actions;
export const getAllUsers = () => async (dispatch) => {
try {
const res = await Api.fetchAllUsers()
if (!res.errors) {
dispatch(loadAllUsers(res.map(user => ({id: user.id, label: user.full_name}))));
} else {
throw res.errors
}
} catch (err) {
alert(JSON.stringify(err))
}
}
export const getUserResources = (userId) => async (dispatch) => {
try {
const res = await Api.fetchUserResources(userId)
if (!res.errors) {
dispatch(loadUserResources(res));
} else {
throw res.errors
}
} catch (err) {
alert(JSON.stringify(err))
}
}
export default userSlice.reducer;
This is fine but I am following this pattern on every page in my app. While it is easy follow I don't believe I'm using global state properly. Every page makes and API call and loads the response into redux, not necessarily because it needs to be shared (although it may be at some point) but because it's the pattern I'm following.
Approach 2: Local State
import React, { useEffect, useState } from 'react'
import { NavList } from '../nav/NavList'
import { ResourceList } from '../resource/ResourceList'
import CircularProgress from '#mui/material/CircularProgress';
import Api from '../../services/api';
export const UserParent = () => {
const [users, setUsers] = useState([])
const [resources, setResources] = useState([])
const [highLightedUsers, setHighLightedItems] = useState([]);
const [isLoading, setIsLoading] = useState(true)
const getUsers = async () => {
try {
const res = await Api.fetchAllUsers()
setUsers(res.map(user => ({id: user.id, label: user.full_name})))
setIsLoading(false)
} catch (error) {
console.log(error)
}
}
const getUserResources = async (userId) => {
try {
setIsLoading(true)
const res = await Api.fetchUserResources(userId)
setResources(res)
setIsLoading(false)
} catch (error) {
console.log(error)
}
}
useEffect(() => {
getUsers()
}, [])
const onUserClick = (user) => {
if (highLightedUsers.includes(user.label)) {
setHighLightedItems([])
} else {
setHighLightedItems([...highLightedUsers, user.label])
getUserResources(user.id)
}
}
return(
<>
{ isLoading === undefined || isLoading ? <CircularProgress className="search-loader" /> :
<div className="search-container">
<div className="search-nav">
<NavList
items={users}
onItemClick={onUserClick}
highLightedItems={highLightedUsers}
/>
</div>
<div className="search-results">
<ResourceList resources={resources} />
</div>
</div>}
</>
)
}
What I like about this is that it uses local state and doesn't bloat global state however, I don't like that it still has business logic in the component, I could just move these to a different file but first I wanted to try React Query instead.
Approach 3: React Query
import React, { useState } from 'react'
import { NavList } from '../nav/NavList'
import { ResourceList } from '../resource/ResourceList'
import CircularProgress from '#mui/material/CircularProgress';
import Api from '../../services/api';
import { useQuery } from "react-query";
export const UserParent = () => {
const [resources, setResources] = useState([])
const [highLightedUsers, setHighLightedItems] = useState([]);
const getUsers = async () => {
try {
const res = await Api.fetchAllUsers()
return res
} catch (error) {
console.log(error)
}
}
const { data, status } = useQuery("users", getUsers);
const getUserResources = async (userId) => {
try {
const res = await Api.fetchUserResources(userId)
setResources(res)
} catch (error) {
console.log(error)
}
}
const onUserClick = (user) => {
if (highLightedUsers.includes(user.label)) {
setHighLightedItems([])
} else {
setHighLightedItems([...highLightedUsers, user.label])
getUserResources(user.id)
}
}
return(
<>
{ status === 'loading' && <CircularProgress className="search-loader" /> }
<div className="search-container">
<div className="search-nav">
<NavList
items={data.map(user => ({id: user.id, label: user.full_name}))}
onItemClick={onUserClick}
highLightedItems={highLightedUsers}
/>
</div>
<div className="search-results">
<ResourceList resources={resources} />
</div>
</div>
</>
)
}
This is great but there is still business logic in my component, so I can move those functions to a separate file and import them and then I end up with this:
import React, { useState } from 'react'
import { UserList } from '../users/UserList'
import { ResourceList } from '../resource/ResourceList'
import CircularProgress from '#mui/material/CircularProgress';
import { getUsers, getUserResources } from './users'
import { useQuery } from "react-query";
export const UserParent = () => {
const [resources, setResources] = useState([])
const [highLightedUsers, setHighLightedItems] = useState([]);
const { data, status } = useQuery("users", getUsers);
const onUserClick = async (user) => {
if (highLightedUsers.includes(user.full_name)) {
setHighLightedItems([])
} else {
setHighLightedItems([...highLightedUsers, user.full_name])
const res = await getUserResources(user.id)
setResources(res)
}
}
return(
<>
{ status === 'loading' && <CircularProgress className="search-loader" /> }
<div className="search-container">
<div className="search-nav">
<UserList
users={data}
onUserClick={onUserClick}
highLightedUsers={highLightedUsers}
/>
</div>
<div className="search-results">
<ResourceList resources={resources} />
</div>
</div>
</>
)
}
In my opinion this is so clean! However, is there anything wrong with the first approach using Redux? Which approach do you prefer?
The first approach you are using shows a very outdated style of Redux.
Modern Redux is written using the official Redux Toolkit (which is the recommendation for all your production code since 2019. It does not use switch..case reducers, ACTION_TYPES, immutable reducer logic, createStore or connect. Generally, it is about 1/4 of the code.
What RTK also does is ship with RTK-Query, which is similar to React Query, but even a bit more declarative than React Query. Which one of those two you like better is probably left to personal taste.
I'd suggest that if you have any use for Redux beyond "just api fetching" (which is a solved problem given either RTK Query or React Query), you can go with Redux/RTK-Query.
If you don't have any global state left after handling api caching, you should probably just go with React Query.
As for learning modern Redux including RTK Query, please follow the official Redux tutorial.
Personally I prefer React-Query for all API-calls, it is great it useMutate and how it manages re-fetching, invalidating queries and more.
I am using your third approach where I create the queries in separate files and then import them where needed.
So far it has been great, and I am using RecoilJS for managing global states. And with the right approach there is really not much that actually needs to be in a global state IMO. Some basic auth/user info and perhaps notification management. But other than that I have been putting less and less in global states keeping it much simpler and scoped.