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
Related
I'm using React and Firebase to create chat web app.
And I don't know how user can get notofications, when another user sent him a message in the chat.
Can someone tell me how to do it?
If you want to look at the whole project, here is github: https://github.com/PHILLyaHI/diplom-work/tree/main/React-Frontend/frontend
But for now, I’ll show the files below that I think are important for creating a notification system.
Input.js File with sending system
import React, { useContext, useState } from "react";
import Img from "../../static/chat/add-img.png";
import Attach from "../../static/chat/attach.png";
import { AuthContext } from "./AuthContext";
import { ChatContext } from "./ChatContext";
import {
arrayUnion,
doc,
serverTimestamp,
Timestamp,
updateDoc,
} from "firebase/firestore";
import { db, storage } from "../../firebase";
import { v4 as uuid } from "uuid";
import { getDownloadURL, ref, uploadBytesResumable } from "firebase/storage";
const Input = () => {
const [text, setText] = useState("");
const [img, setImg] = useState(null);
const { currentUser } = useContext(AuthContext);
const { data } = useContext(ChatContext);
const handleSend = async () => {
if (img) {
const storageRef = ref(storage, uuid());
const uploadTask = uploadBytesResumable(storageRef, img);
uploadTask.on(
(error) => {
//TODO:Handle Error
},
() => {
getDownloadURL(uploadTask.snapshot.ref).then(async (downloadURL) => {
await updateDoc(doc(db, "chats", data.chatId), {
messages: arrayUnion({
id: uuid(),
text,
senderId: currentUser.uid,
date: Timestamp.now(),
img: downloadURL,
}),
});
});
}
);
} else {
await updateDoc(doc(db, "chats", data.chatId), {
messages: arrayUnion({
id: uuid(),
text,
senderId: currentUser.uid,
date: Timestamp.now(),
}),
});
}
await updateDoc(doc(db, "userChats", currentUser.uid), {
[data.chatId + ".lastMessage"]: {
text,
},
[data.chatId + ".date"]: serverTimestamp(),
});
await updateDoc(doc(db, "userChats", data.user.uid), {
[data.chatId + ".lastMessage"]: {
text,
},
[data.chatId + ".date"]: serverTimestamp(),
});
setText("");
setImg(null);
};
return (
<div className="input">
<input
type="text"
placeholder="Type something..."
onChange={(e) => setText(e.target.value)}
value={text}
/>
<div className="send">
<img src={Attach} alt="" />
<input
type="file"
style={{ display: "none" }}
id="file"
onChange={(e) => setImg(e.target.files[0])}
/>
<label title="IMAGE SUPPOSE TO BE LESS THAN 250KB" htmlFor="file">
<img src={Img} alt="" />
</label>
<button onClick={handleSend}>Send</button>
</div>
</div>
);
};
export default Input;
Chats.js The sidebar with users.
import React, { useContext, useEffect, useState } from "react";
import { doc, onSnapshot } from "firebase/firestore";
import { AuthContext } from "./AuthContext";
import { db } from "../../firebase";
import { ChatContext } from "./ChatContext";
import addNotification from "react-push-notification";
const Chats = () => {
const [chats, setChats] = useState([]);
const {currentUser} = useContext(AuthContext);
const { dispatch } = useContext(ChatContext)
useEffect(() => {
const getChats = () => {
const unsub = onSnapshot(doc(db, "userChats", currentUser.uid), (doc) => {
setChats(doc.data())
});
return () => {
unsub();
};
};
currentUser.uid && getChats()
}, [currentUser.uid]);
const handleSelect = (u) => {
dispatch({type: "CHANGE_USER", payload: u})
}
return (
<div className="chats">
{Object.entries(chats)?.map((chat) => (
<div className="userChat" key={chat[0]} onClick={()=>handleSelect(chat[1].userInfo)}>
{chats?.unread && (
<small className="unread">New</small>
)}
<img src={chat[1].userInfo.photoURL} alt=""/>
<div className="userChatInfo">
<span>{chat[1].userInfo.displayName}</span>
<p>{chat[1].lastMessage?.text}</p>
</div>
</div>
))};
</div>
);
};
export default Chats;
AuthContext.js
import { createContext, useEffect, useState } from "react";
import { auth } from "../../firebase";
import { onAuthStateChanged } from "firebase/auth";
export const AuthContext = createContext();
export const AuthContextProvider = ({ children }) => {
const [currentUser, setCurrentUser] = useState({});
useEffect(() => {
const unsub = onAuthStateChanged(auth, (user) => {
setCurrentUser(user);
console.log(user);
});
return () => {
unsub();
};
}, []);
return (
<AuthContext.Provider value={{ currentUser }}>
{children}
</AuthContext.Provider>
);
};
import { createContext, useEffect, useState } from "react";
import { auth } from "../../firebase";
import { onAuthStateChanged } from "firebase/auth";
export const AuthContext = createContext();
export const AuthContextProvider = ({ children }) => {
const [currentUser, setCurrentUser] = useState({});
useEffect(() => {
const unsub = onAuthStateChanged(auth, (user) => {
setCurrentUser(user);
console.log(user);
});
return () => {
unsub();
};
}, []);
return (
<AuthContext.Provider value={{ currentUser }}>
{children}
</AuthContext.Provider>
);
};
ChatContext.js
import {
createContext,
useContext,
useReducer,
} from "react";
import { AuthContext } from "./AuthContext";
export const ChatContext = createContext();
export const ChatContextProvider = ({ children }) => {
const { currentUser } = useContext(AuthContext);
const INITIAL_STATE = {
chatId: "null",
user: {},
};
const chatReducer = (state, action) => {
switch (action.type) {
case "CHANGE_USER":
return {
user: action.payload,
chatId:
currentUser.uid > action.payload.uid
? currentUser.uid + action.payload.uid
: action.payload.uid + currentUser.uid,
};
default:
return state;
}
};
const [state, dispatch] = useReducer(chatReducer, INITIAL_STATE);
return (
<ChatContext.Provider value={{ data:state, dispatch }}>
{children}
</ChatContext.Provider>
);
};
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;
I am making React application with Typescript, React Query and Recoil. I don't know why I am getting this error in the terminal. If u want more information (more code) of something like that to find the solution, I will update question.
import { atom } from 'recoil';
export const Books = atom({
key: 'book',
default: []
})
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
})
}
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, data } = 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>{pageNumber}</p>
<button onClick={() => setPageNumber((page) => page + 1)} disabled={data.metadata.records_per_page * data.metadata.page > data.metadata.total_records}>Next page</button>
</div>
<BookContent>
<Navbar />
{displayBooks}
</BookContent>
</BookContainer>
)
}
Hi i am new developer testing platform. I have a problem but I did not find a solution or work it with correct way. I am trying to login component test with to parameter by Inputs. Firstly I filled these are userEvent.type. After I am clicking my button. And when I was waiting my method that call by onSubmitForTest in one time , I am facing an error like fallowing image.
What is the reason of this ? How can I solve my problem ? Thanks for your helps.
My Login.tsx component:
import React, { FC, useState } from "react";
import { useTranslation } from "react-i18next";
import Input from "../../components/Input";
import InputPassword from "../../components/Input/InputPassword";
import ButtonLoading from "../../components/Button/ButtonLoading";
import { GetLoginInfo, ILoginRequest } from "../../store/actions/loginActions";
interface ILoginState {
emailorUsername: string;
password: string;
}
const initialState = {
emailorUsername: "",
password: "",
};
interface IProps {
onSubmitForTest: (items: any) => void
}
const Login: FC<IProps> = ({ onSubmitForTest }) => {
const { t } = useTranslation();
const [state, setstate] = useState<ILoginState>(initialState);
const onChange = (key: string, value: string | number) => {
setstate({ ...state, [key]: value });
};
const handleLogin = async () => {
const loginRequest: ILoginRequest = {
emailOrUsername: state.emailorUsername,
password: state.password,
returnUrl: "",
};
const response = await GetLoginInfo(loginRequest);
if (response.isSucceed) { } else { }
};
const renderLoginPart = () => {
return (
<div className="flex">
<Input
name="emailorUsername"
label={t("emailorUsername")}
value={state.emailorUsername}
onChange={(val: any) => onChange("emailorUsername", val)}
/>
<InputPassword
name="password"
label={t("password")}
value={state.password}
onChange={(val: any) => onChange("password", val)}
/>
<ButtonLoading
text={t("login")}
variant="contained"
onClick={() => {
if (onSubmitForTest) {
const loginRequestItemForTest = {
emailOrUsername: "testUsername",
password: "testPassword",
};
onSubmitForTest(loginRequestItemForTest)
}
handleLogin()
}}
dataTestid={"login-button-element"}
/>
</div>
);
};
return <div className="">{renderLoginPart()}</div>;
};
export default Login;
My index.test.js :
import React from 'react'
import { render, screen, waitFor } from "#testing-library/react"
import LoginPage from "../index"
import userEvent from "#testing-library/user-event"
const onSubmit = jest.fn()
beforeEach(()=>{
const { } = render(<LoginPage />)
onSubmit.mockClear()
})
test('Login form parametre olarak doğru data gönderme testi', async () => {
const eMail = screen.getByTestId('text-input-element')
const password = screen.getByTestId('password-input-element')
userEvent.type(eMail, "fillWithTestUsername")
userEvent.type(password, "fillWithTestPassword")
userEvent.click(screen.getByTestId('login-button-element'))
await waitFor(()=>{
expect(onSubmit).toHaveBeenCalledTimes(1)
})
})
beforeEach(()=>{
render(<LoginPage onSubmitForTest={onSubmit} />)
})
Please try doing this in beforeEach. If this still doesn't work you can try replacing toHaveBeenCalledTimes with toBeCalledTimes like below
await waitFor(()=>{
expect(onSubmit).toBeCalledTimes(1)
})
import React, {useState, useEffect} from 'react';
import {connect} from 'react-redux';
import {
fetchRecipes
} from '../../store/actions';
import './BeerRecipes.css';
const BeerRecipes = ({recipesData, fetchRecipes}) => {
useEffect(() => {
fetchRecipes();
}, [])
return (
<div className='beer_recipes_block'>
<div className='title_wrapper'>
<h2 className='title'>Beer recipes</h2>
</div>
<div className='beer_recipes'>
<ul className='beer_recipes_items'>
{
recipesData && recipesData.recipes &&.recipesData.recipes.map(recipe =>
<li className='beer_recipes_item' id={recipe.id}>{recipe.name}</li>
)
}
</ul>
</div>
</div>
);
};
const mapStateToProps = state => {
return {
recipesData: state.recipes
}
}
const mapDispatchToProps = dispatch => {
return {
fetchRecipes: () => dispatch(fetchRecipes())
}
}
export default connect(mapStateToProps, mapDispatchToProps)(BeerRecipes);
this is my component where I would like to create infinite scroll and below is my redux-action with axios:
import axios from "axios";
import * as actionTypes from "./actionTypes";
export const fetchRecipesRequest = () => {
return {
type: actionTypes.FETCH_RECIPES_REQUEST
}
}
export const fetchRecipesSuccess = recipes => {
return {
type: actionTypes.FETCH_RECIPES_SUCCESS,
payload: recipes
}
}
export const fetchRecipesFailure = error => {
return {
type: actionTypes.FETCH_RECIPES_FAILURE,
payload: error
}
}
export const fetchRecipes = (page) => {
return (dispatch) => {
dispatch(fetchRecipesRequest)
axios
.get('https://api.punkapi.com/v2/beers?page=1')
.then(response => {
const recipes = response.data;
dispatch(fetchRecipesSuccess(recipes));
})
.catch(error => {
const errorMsg = error.message;
dispatch(fetchRecipesFailure(errorMsg));
})
}
}
I want to create a scroll. I need, firstly, to display first 10 elements and then to add 5 elements with every loading. I have 25 elements altogether and when the list is done it should start from the first five again.