How to Make Notification System Using React and Firebase? - reactjs

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>
);
};

Related

How to implement pagination and search in React

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

Property 'data' does not exist on type 'never[]'

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>
)
}

React Firebase login, show white screen instead of HomePage

I was developing an React App with authenticated with firebase, and use Redux.
My problems comes after register in the App when yo try to login, the 'section' tag desapear.
I try on other way, but stay logging until infinate of times.
The code of my firebase file is the followiing:
authActions.tsx
import { ThunkAction } from 'redux-thunk';
import { SignUpData, AuthAction, SET_USER, User, SET_LOADING, SIGN_OUT, SignInData, SET_ERROR, NEED_VERIFICATION, SET_SUCCESS } from '../types';
import { RootState } from '..';
import firebase from '../../firebase/config';
// Create user
export const signup = (data: SignUpData, onError: () => void): ThunkAction<void, RootState, null, AuthAction> => {
return async dispatch => {
try {
const res = await firebase.auth().createUserWithEmailAndPassword(data.email, data.password);
if(res.user) {
const userData: User = {
email: data.email,
firstName: data.firstName,
id: res.user.uid,
createdAt: firebase.firestore.FieldValue.serverTimestamp()
};
await firebase.firestore().collection('/users').doc(res.user.uid).set(userData);
await res.user.sendEmailVerification();
dispatch({
type: NEED_VERIFICATION
});
dispatch({
type: SET_USER,
payload: userData
});
}
} catch (err) {
if(err instanceof Error){
onError();
dispatch({
type: SET_ERROR,
payload: err.message
});
}
console.log(err);
}
}
}
// Get user by id
export const getUserById = (id: string): ThunkAction<void, RootState, null, AuthAction> => {
return async dispatch => {
try {
const user = await firebase.firestore().collection('users').doc(id).get();
if(user.exists) {
const userData = user.data() as User;
dispatch({
type: SET_USER,
payload: userData
});
}
} catch (err) {
console.log(err);
}
}
}
// Set loading
export const setLoading = (value: boolean): ThunkAction<void, RootState, null, AuthAction> => {
return dispatch => {
dispatch({
type: SET_LOADING,
payload: value
});
}
}
// Log in
export const signin = (data: SignInData, onError: () => void): ThunkAction<void, RootState, null, AuthAction> => {
//const history = useHistory();
/*TODO:
Mandar el useHistory desde el Screen que invoca cada funcion.
*/
return async dispatch => {
try {
await firebase.auth().signInWithEmailAndPassword(data.email, data.password)
} catch (err) {
if(err instanceof Error){
onError();
dispatch(setError(err.message));
}
console.log(err);
}
}
}
// Log out
export const signout = (): ThunkAction<void, RootState, null, AuthAction> => {
return async dispatch => {
try {
dispatch(setLoading(true));
await firebase.auth().signOut();
dispatch({
type: SIGN_OUT
});
} catch (err) {
console.log(err);
dispatch(setLoading(false));
}
}
}
// Set error
export const setError = (msg: string): ThunkAction<void, RootState, null, AuthAction> => {
return dispatch => {
dispatch({
type: SET_ERROR,
payload: msg
});
}
}
// Set need verification
export const setNeedVerification = (): ThunkAction<void, RootState, null, AuthAction> => {
return dispatch => {
dispatch({
type: NEED_VERIFICATION
});
}
}
// Set success
export const setSuccess = (msg: string): ThunkAction<void, RootState, null, AuthAction> => {
return dispatch => {
dispatch({
type: SET_SUCCESS,
payload: msg
});
}
}
// Send password reset email
export const sendPasswordResetEmail = (email: string, successMsg: string): ThunkAction<void, RootState, null, AuthAction> => {
return async dispatch => {
try {
await firebase.auth().sendPasswordResetEmail(email);
dispatch(setSuccess(successMsg));
} catch (err) {
if(err instanceof Error){
dispatch(setError(err.message));
}
console.log(err);
}
}
}
The code of my SignIn Screen is the next:`
SignIn.tsx
import React, { FC, useState, FormEvent, useEffect } from "react";
import { useDispatch, useSelector } from "react-redux";
import { Link, useHistory } from "react-router-dom";
import Input from "../UI/Input";
import Button from "../UI/Button";
import Message from "../UI/Message";
import { signin, setError } from "../../store/actions/authActions";
import { RootState } from "../../store";
/* TODO: Implementar las rutas entre componentes y pages. */
const SignIn: React.FC = () => {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [loading, setLoading] = useState(false);
const dispatch = useDispatch();
const history = useHistory();
const { error } = useSelector((state: RootState) => state.auth);
useEffect(() => {
return () => {
if (error) {
dispatch(setError(""));
}
};
}, [error, dispatch]);
const submitHandler = (e: FormEvent) => {
e.preventDefault();
if (error) {
dispatch(setError(""));
}
setLoading(true);
/*
Si lo pongo como abajo, carga una pagina en blanco y desaparece el section
Si lo pongo asi:
dispatch(signin({ email, password }, () => {
setLoading(false);
dispatch(history.push("../pages/Homepage.tsx"));
})
Se queda cargando tras intentar invocar al signin()
*/
dispatch(signin({ email, password }, () => setLoading(false)));
history.push("../pages/Homepage.tsx");
};
return (
<section className="section">
<div className="container">
<h2 className="has-text-centered is-size-2 mb-3">Sign In</h2>
<form className="form" onSubmit={submitHandler}>
{error && <Message type="danger" msg={error} />}
<Input
type="email"
name="email"
value={email}
onChange={(e) => setEmail(e.currentTarget.value)}
placeholder="Email address"
label="Email address"
/>
<Input
type="password"
name="password"
value={password}
onChange={(e) => setPassword(e.currentTarget.value)}
placeholder="Password"
label="Password"
/>
<p>
<Link to="/forgot-password">Forgot password ?</Link>
</p>
<p>
<Link to="/signup">Create Account</Link>
</p>
<Button
text={loading ? "Loading..." : "Sign In"}
className="is-primary is-fullwidth mt-5"
disabled={loading}
/>
</form>
</div>
</section>
);
};
export default SignIn;
Finally the main code of my App is this:
import React, { FC, useEffect } from "react";
import { useDispatch, useSelector } from "react-redux";
import { BrowserRouter, Switch } from "react-router-dom";
import "./App.css";
import Header from "./components/sections/Header";
import SignUp from "./components/pages/SignUp";
import SignIn from "./components/pages/SignIn";
import ForgotPassword from "./components/pages/ForgotPassword";
import Homepage from "./components/pages/Homepage";
import Dashboard from "./components/pages/Dashboard";
import PrivateRoute from "./components/auth/PrivateRoute";
import PublicRoute from "./components/auth/PublicRoute";
import Loader from "./components/UI/Loader";
import firebase from "./firebase/config";
import {
getUserById,
setLoading,
setNeedVerification,
} from "./store/actions/authActions";
import { RootState } from "./store";
const App: FC = () => {
const dispatch = useDispatch();
const { loading } = useSelector((state: RootState) => state.auth);
// Check if user exists
useEffect(() => {
dispatch(setLoading(true));
const unsubscribe = firebase.auth().onAuthStateChanged(async (user) => {
if (user) {
dispatch(setLoading(true));
await dispatch(getUserById(user.uid));
if (!user.emailVerified) {
dispatch(setNeedVerification());
}
}
dispatch(setLoading(false));
});
return () => {
unsubscribe();
};
}, [dispatch]);
if (loading) {
return <Loader />;
}
return (
<BrowserRouter>
<Header />
<Switch>
<PublicRoute path="/homepage" component={Homepage} exact />
<PublicRoute path="/signup" component={SignUp} exact />
<PublicRoute path="/signin" component={SignIn} exact />
<PublicRoute path="/forgot-password" component={ForgotPassword} exact />
<PrivateRoute path="/dashboard" component={Dashboard} exact />
</Switch>
</BrowserRouter>
);
};
export default App;
I add a screenshot of the app after logging in it.
I hope you can guess where the errors comes, and if like this, take thanks in advance!
Without a full reproducible example, it would be hard to ascertain the error cause as the flow of authentication would radically change.
Meanwhile, the error should be caused by the fact that the loading state is globally shared between the SignIn and main App components which would then be toggled on and off by these (and any other subscribing component) upon some conditions assertions which should be related to authentication in your case.
To have a correct authentication flow, you should encapsulate both authentication validation and conditional page routing to the same component being the App in this case:
// App.tsx
useEffect(() => {
dispatch(setLoading(true));
const unsubscribe = firebase.auth().onAuthStateChanged(async (user) => {
if (user) {
await dispatch(getUserById(user.uid));
if (user.emailVerified) {
history.push('/homepage');
} else {
dispatch(setNeedVerification());
}
}
dispatch(setLoading(false));
});
return () => {
unsubscribe();
};
});
The form submitHandler action creator would then be updated accordingly:
const submitHandler = (e: FormEvent) => {
e.preventDefault();
if (error) {
dispatch(setError(""));
}
setLoading(true);
dispatch(signin({ email, password }, () => setLoading(false)));
};

Writing tests with react and context api

I'm trying to write a test and I'm not getting the result thats its intended, can someone see what I'm doing wrong here please? I'm trying to call my addTodo but all I get is the error message below, quite new to testing so I'm not sure what it means.
my error message is this:
● <TodoForm /> #addTodo
expect(jest.fn()).toBeCalledWith(...expected)
Expected: {"payload": "a new todo", "type": "addTodo"}
Number of calls: 0
24 | form.find("button").simulate("click");
25 |
> 26 | expect(dispatch).toBeCalledWith({ type: "addTodo", payload: "a new todo" });
| ^
27 | });
28 |
at Object.<anonymous> (src/tests/TodoForm.test.js:26:20)
Here the relevant files:
the test: TodoForm.test.js
import React from "react";
import Enzyme, { mount } from "enzyme";
import Adapter from "enzyme-adapter-react-16";
import { Provider } from "../context/TodoContext";
import TodoForm from "../components/TodoForm";
Enzyme.configure({ adapter: new Adapter() });
test("<TodoForm /> #addTodo", async () => {
const dispatch = jest.fn();
const form = mount(
<Provider value={{ dispatch }}>
<TodoForm />
</Provider>
);
form.find("input").simulate("change", { target: { value: "a new todo" } });
form.find("button").simulate("click");
expect(dispatch).toBeCalledWith({ type: "addTodo", payload: "a new todo" });
});
the file I'm trying to write the test for:
import React, { useContext, useState } from "react";
import { Context } from "../context/TodoContext";
import "../css/TodoForm.css";
const TodoForm = ({ initialValues }) => {
const { addTodo } = useContext(Context);
const [title, setTitle] = useState("");
const [error, setError] = useState("");
function handleTodoAdd() {
if (title === "") {
setError(true);
} else {
setError(false);
}
setTitle("");
}
const onChange = e => {
setTitle(e.target.value);
};
/*
function handleSubmitForm(event) {
if (event.keyCode === 13) handleTodoAdd();
}
*/
return (
<div className="container">
<div className="inputContainer">
<div className="input-group">
<input
autoFocus={true}
aria-label="Enter the title of your todo"
placeholder="Enter new todo"
value={title}
onChange={onChange}
/>
<div className="errorContainer">
<span className="error">
{error ? "Please enter a value" : null}
</span>
</div>
<div className="input-group-append">
<button
aria-label="Add todo to your list"
className="addButton"
onClick={() => addTodo(title)}
>
Add
</button>
</div>
</div>
</div>
</div>
);
};
TodoForm.defaultProps = {
initialValues: {
title: "adsda"
}
};
export default TodoForm;
TodoContext.js I'm using this file to pull my provider
import React from "react";
import createDataContext from "./createDataContext";
export const TodoContext = React.createContext();
export default function todoReducer(state, action) {
switch (action.type) {
case "addTodo":
return [
...state,
{ id: Math.floor(Math.random() * 999), title: action.payload }
];
case "deleteTodo":
return state.filter(todo => todo.id !== action.payload);
case "editTodo":
return state.map(todo => {
return todo.id === action.payload.id ? action.payload : todo;
});
default:
return state;
}
}
const addTodo = dispatch => {
return title => {
dispatch({ type: "addTodo", payload: title });
};
};
const deleteTodo = dispatch => {
return id => {
dispatch({ type: "deleteTodo", payload: id });
};
};
const editTodo = dispatch => {
return (id, title) => {
dispatch({ type: "editTodo", payload: { id, title } });
};
};
export const { Context, Provider } = createDataContext(
todoReducer,
{ addTodo, deleteTodo, editTodo },
[]
);
auto creates my context data
import React, { useReducer } from "react";
export default (reducer, actions, initialState) => {
const Context = React.createContext();
const Provider = ({ children }) => {
const [state, dispatch] = useReducer(reducer, initialState);
const boundActions = {};
for (let key in actions) {
boundActions[key] = actions[key](dispatch);
}
return (
<Context.Provider value={{ state, ...boundActions }}>
{children}
</Context.Provider>
);
};
return { Context, Provider };
};

How can I avoid the infinite loop in useEffect?

I need your help. I'm creating an app with useContext and useReducer hooks and I a have problems. I have a function to get all notes from my database. I called that function inside off useEffect hook:
import React, { useContext, useEffect } from "react";
import { useTranslation } from "react-i18next";
//Context
import AuthContext from "../../context/auth/authContext";
import NoteContext from '../../context/notes/noteContext';
//Components
import { Row, Col, Container, Button } from "react-bootstrap";
import Canva from '../Canva/Canva';
import Note from '../Note/Note';
const Dashboard = () => {
const { t, i18n } = useTranslation();
const authContext = useContext(AuthContext);
const { authUser, user } = authContext;
const noteContext = useContext(NoteContext);
const { notes, getNotes, addNote } = noteContext;
useEffect(() => {
getNotes();
}, []);
return (
<>
<Container>
<Row>
<Col sm={12} md={10}>
<Button onClick={() => addNote()} type='button' className='mb-2'>
Añadir elemento
</Button>
<Canva>
{notes && (notes.map(note => {
return (
<Note key={note._id} note={note} />
)
}))}
</Canva>
</Col>
</Row>
</Container>
</>
);
};
export default Dashboard;
If I called that function that way, my state doesn't change:
notes: undefined
But if I introduce a dependency inside of useEffect, my app goes into an infinite loop. For example:
useEffect(() => {
getNotes();
}, [notes])
//Or:
useEffect(() => {
getNotes()
}, [getNotes])
How can I avoid the infinite loop?
You need to use 2 useEffect hooks, one for fetch data and second to proceed it:
useEffect(() => {
getNotes();
}, []);
useEffect(() => {
if (notes && notes.length) {
....setState or what else
}
}, [notes]);
My note state looks like:
import React, { useReducer } from 'react';
import clientAxios from '../../config/clientAxios';
import NoteContext from './noteContext';
import NoteReducer from './noteReducer';
import {
GET_NOTES,
ADD_NOTE,
DELETE_NOTE,
UPDATE_NOTE,
} from '../../types';
const NoteState = ({ children }) => {
const initialState = {
notes: [],
noteError: false,
};
const [state, dispatch] = useReducer(NoteReducer, initialState);
const getNotes = async () => {
try {
const response = await clientAxios.get('/user/Notes');
dispatch({
type: GET_NOTES,
payload: response.data
})
} catch (error) {
console.log(error.response);
}
}
const addNote = async data => {
try {
const response = await clientAxios.post('/addNote', data);
dispatch({
type: ADD_NOTE,
payload: response.data.data
})
} catch (error) {
console.log(error.response);
}
}
const updateNote = async (id, { title, description }) => {
try {
const response = await clientAxios.put(`updateNote/${id}`, { title, description });
console.log(response.data);
dispatch({
type: UPDATE_NOTE,
payload: response.data
})
} catch (error) {
console.log(error.response)
}
}
const deleteNote = async id => {
try {
await clientAxios.put(`/deleteNote/${id}`);
dispatch({
type: DELETE_NOTE,
payload: id
})
} catch (error) {
console.log(error.response);
}
}
return(
<NoteContext.Provider
value={{
notes: state.notes,
noteError: state.noteError,
getNotes,
addNote,
updateNote,
deleteNote,
}}
>
{children}
</NoteContext.Provider>
);
}
export default NoteState;
and my reducer:
import {
GET_NOTES,
ADD_NOTE,
DELETE_NOTE,
UPDATE_NOTE,
} from '../../types';
export default (action, state) => {
switch(action.type) {
case GET_NOTES:
return {
...state,
notes: action.payload
}
case ADD_NOTE:
return {
...state,
notes: [...state.notes, action.payload]
}
case UPDATE_NOTE:
return {
...state,
notes: state.notes.map(note => note._id === action.payload._id ? action.payload : note)
}
case DELETE_NOTE:
return {
...state,
notes: state.notes.filter(note => note._id !== action.payload),
}
default:
return state;
}
}

Resources