I'm using Jest/Testing-Library to write UI unit tests.
Components are not rendering on the DOM, and the culprit was the component 'RequireScope' which wraps all of the components individually.
In other words, every component returns this:
return ( <RequireScope> // some MUI stuff</RequireScope>
)
This is preventing my components from being rendered in the DOM tree when tested.
This is because RequireScope makes sure to render its children only if authentication goes through.
How can I simulate a logged-in user given the following code?
RequireScope:
import React, { useEffect, useState } from 'react';
import useAuth from 'src/hooks/useAuth';
export interface RequireScopeProps {
scopes: string[];
}
const RequireScope: React.FC<RequireScopeProps> = React.memo((props) => {
const { children, scopes } = props;
const { isInitialized, isAuthenticated, permissions } = useAuth();
const [isPermitted, setIsPermitted] = useState(false);
useEffect(() => {
if (isAuthenticated && isInitialized) {
(async () => {
const hasPermissions = scopes
.map((s) => {
return permissions.includes(s);
})
.filter(Boolean);
if (hasPermissions.length === scopes.length) {
setIsPermitted(true);
}
})();
}
}, [isAuthenticated, isInitialized, scopes, permissions]);
if (isPermitted) {
return <>{children}</>;
}
return null;
});
export default RequireScope;
The ultimate goal is to have 'isPermitted' to be true. In order to do this 'isInitialized, isAuthenticated, permissions' has to be true. We bring these 3 values from useAuth().
useAuth:
import { useContext } from 'react';
import AuthContext from '../contexts/JWTContext';
const useAuth = () => useContext(AuthContext);
export default useAuth;
JWTContext:
const handlers: Record<string, (state: State, action: Action) => State> = {
INITIALIZE: (state: State, action: InitializeAction): State => {
const { isAuthenticated, permissions, user } = action.payload;
return {
...state,
isAuthenticated,
isInitialized: true,
permissions,
user,
};
},
LOGIN: (state: State): State => {
return {
...state,
isAuthenticated: true,
};
},
LOGOUT: (state: State): State => ({
...state,
isAuthenticated: false,
permissions: [],
}),
};
const reducer = (state: State, action: Action): State =>
handlers[action.type] ? handlers[action.type](state, action) : state;
const AuthContext = createContext<AuthContextValue>({
...initialState,
platform: 'JWT',
login: () => Promise.resolve(),
logout: () => Promise.resolve(),
});
export const AuthProvider: FC<AuthProviderProps> = (props) => {
const { children } = props;
const [state, dispatch] = useReducer(reducer, initialState);
const router = useRouter();
const reduxDispatch = useDispatch();
useEffect(() => {
const initialize = async (): Promise<void> => {
try {
if (router.isReady) {
const { token, permissions, user, companyId } = router.query;
const accessToken =
(token as string) || window.localStorage.getItem('accessToken');
const permsStorage = window.localStorage.getItem('perms');
const perms = (permissions as string) || permsStorage;
const userStorage = window.localStorage.getItem('user');
const selectedCompanyId =
(companyId as string) || window.localStorage.getItem('companyId');
const authUser = (user as string) || userStorage;
if (accessToken && perms) {
setSession(accessToken, perms, authUser);
try {
// check if user is admin by this perm, probably want to add a flag later
if (perms.includes('create:calcs')) {
if (!selectedCompanyId) {
const response = await reduxDispatch(getAllCompanies());
const companyId = response.payload[0].id;
reduxDispatch(companyActions.selectCompany(companyId));
reduxDispatch(getCurrentCompany({ companyId }));
} else {
reduxDispatch(
companyActions.selectCompany(selectedCompanyId),
);
await reduxDispatch(
getCurrentCompany({ companyId: selectedCompanyId }),
);
}
} else {
reduxDispatch(companyActions.selectCompany(selectedCompanyId));
await reduxDispatch(
getCurrentCompany({ companyId: selectedCompanyId }),
);
}
} catch (e) {
console.warn(e);
} finally {
dispatch({
type: 'INITIALIZE',
payload: {
isAuthenticated: true,
permissions: JSON.parse(perms),
user: JSON.parse(authUser),
},
});
}
if (token || permissions) {
router.replace(router.pathname, undefined, { shallow: true });
}
} else {
dispatch({
type: 'INITIALIZE',
payload: {
isAuthenticated: false,
permissions: [],
user: undefined,
},
});
setSession(undefined);
if (router.pathname !== '/client-landing') {
router.push('/login');
}
}
}
} catch (err) {
console.error(err);
dispatch({
type: 'INITIALIZE',
payload: {
isAuthenticated: false,
permissions: [],
user: undefined,
},
});
//router.push('/login');
}
};
initialize();
}, [router.isReady]);
const login = useCallback(async (): Promise<void> => {
const response = await axios.get('/auth/sign-in-with-intuit');
window.location = response.data;
}, []);
const logout = useCallback(async (): Promise<void> => {
const token = localStorage.getItem('accessToken');
// only logout if already logged in
if (token) {
dispatch({ type: 'LOGOUT' });
}
setSession(null);
router.push('/login');
}, [dispatch, router]);
return (
<AuthContext.Provider
value={{
...state,
platform: 'JWT',
login,
logout,
}}
>
{state.isInitialized && children}
</AuthContext.Provider>
);
};
AuthProvider.propTypes = {
children: PropTypes.node.isRequired,
};
export default AuthContext;
To achieve what is described above, we just have to make sure the 'finally' statement runs if I am correct. Thus the conditional statements:
if (router.isReady)
and
if (accessToken && perms)
has to be met.
How can I make the router to exist when I render this AuthProvider component in Jest?
Or are there any other alternatives to simulate a logged in user?
My test looks like this:
// test BenchmarksPage
test('renders benchmark', () => {
render(
<HelmetProvider>
<Provider store={mockStore(initState)}>
<AuthProvider>
<BenchmarksPage />
</AuthProvider>
</Provider>
</HelmetProvider>,
);
localStorage.setItem('accessToken', 'sampletokenIsInR5cCI6');
localStorage.setItem(
'perms',
JSON.stringify([
'create:calcs',
// and so on
}}
As your component has side effects in it (i.e. gtm.push, redux-thunk) you may need to wait for the component state to be stable before testing it (as I don't know what is going on in the CalculationTable component). Hence try changing your test to:
// Make the test asynchronous by adding `async`
test('renders header and export dropdown', async () => {
const initState = {};
const middlewares = [thunk];
const mockStore = configureStore(middlewares);
const { findByRole, getByText, getByTestId } = render(
<Provider store={mockStore(initState)}>
<CalculationsPage />
</Provider>,
);
// findByRole will wait for the element to be present.
// Note the `await` keyword
const header = await findByRole('heading', { name: /calculations/i });
await waitFor(() => expect(getByTestId('analysis-categories-header')).toBeVisible());
}
"findBy methods are a combination of getBy queries and waitFor." - see here for more info.
Related
I have a NextJS application that is using the ShopifyBuy SDK. I have been successfully able to implement a solution where I am able to fetch the products from Store and display them to the User. The user is also able to go to a product page and add the product to the cart.
However, when the user refreshes the page, the cart is reset, and the data does not persist. The code is below:
context/cart.js:
import { createContext, useContext, useEffect, useReducer } from "react";
import client from "../lib/client";
import Cookies from "js-cookie";
const CartStateContext = createContext();
const CartDispatchContext = createContext();
const SET_CART = "SET_CART";
const initalState = {
lineItems: [],
totalPrice: 0,
webUrl: "",
id: "",
};
const reducer = (state, action) => {
switch (action.type) {
case SET_CART:
return { ...state, ...action.payload };
default:
throw new Error(`Unknown action: ${action.type}`);
}
};
export const CartProvider = ({ children }) => {
const savedState = Cookies.get("cartState");
const [state, dispatch] = useReducer(reducer, savedState || initalState);
useEffect(() => {
Cookies.set("cartState", state, { expires: 7 });
}, [state]);
useEffect(() => {
getCart();
}, []);
const setCart = (payload) => dispatch({ type: SET_CART, payload });
const getCart = async () => {
try {
const cart = await client.checkout.create();
setCart(cart);
} catch (err) {
console.log(err);
}
};
return (
<CartDispatchContext.Provider value={{ setCart }}>
<CartStateContext.Provider value={{ state }}>
{children}
</CartStateContext.Provider>
</CartDispatchContext.Provider>
);
};
export const useCartState = () => useContext(CartStateContext);
export const useCartDispatch = () => useContext(CartDispatchContext);
products/[handle].tsx:
import React, { useState, useEffect } from "react";
import client from "../../lib/client";
import { useCartDispatch, useCartState } from "../../context/cart";
import Link from "next/link";
import cookie from "js-cookie";
export const getStaticPaths = async () => {
const res = await client.product.fetchAll();
const paths = res.map((product: any) => {
return {
params: { handle: product.handle.toString() },
};
});
return {
paths,
fallback: false,
};
};
export const getStaticProps = async (context: any) => {
const handle = context.params.handle;
const res = await client.product.fetchByHandle(handle);
const product = JSON.stringify(res);
return {
props: {
product,
},
};
};
function Product({ product }: any) {
const { state } = useCartState();
const { setCart } = useCartDispatch();
const addToCart = async () => {
const checkoutId = state.id;
const lineItemsToAdd = [
{
variantId: product.variants[0].id,
quantity: 1,
},
];
const res = await client.checkout.addLineItems(checkoutId, lineItemsToAdd);
setCart(res);
};
product = JSON.parse(product);
return (
<div>
<div className=" flex-col text-2xl font-bold m-8 flex items-center justify-center ">
<h1>{product.title}</h1>
<button onClick={addToCart}>Add to Cart</button>
<Link href="/cart">Checkout</Link>
</div>
</div>
);
}
export default Product;
pages/cart/index.tsx:
import React, { useEffect } from "react";
import { useCartState, useCartDispatch } from "../../context/cart";
import client from "../../lib/client";
function Cart() {
const { state } = useCartState();
return (
<div>
<h1>Cart</h1>
{state.lineItems &&
state.lineItems.map((item: any) => {
return (
<div key={item.id}>
<h2>{item.title}</h2>
<p>{item.variant.title}</p>
<p>{item.quantity}</p>
</div>
);
})}
</div>
);
}
export default Cart;
I have tried using a library called js-cookie and also localStorage. I'm not sure where the problem lies or if the solutions that I've tried are wrong.
P.S.: I'm fairly new to NextJS and Typescript so go easy on the syntax. This code is for a personal project. Thanks in advance!
Answering this because I ended up coming up with a solution that works for me, at least.
Here it is:
const getCart = async () => {
try {
const checkoutId = Cookies.get("checkoutId");
let cart;
if (checkoutId) {
cart = await client.checkout.fetch(checkoutId);
} else {
cart = await client.checkout.create();
Cookies.set("checkoutId", cart.id);
}
setCart(cart);
} catch (err) {
console.log(err);
}
};
From my understanding, what this does is the following:
Check the cookies to see if one exists called "checkoutId"
If it exists, fetch the cart using that checkoutId
Otherwise, create a new cart and create a cookie using the cart.id that is returned in the response
Then, inside my individual Product page ([handle].tsx), I'm doing the following:
const addToCart = async () => {
const checkoutId = state.id;
const lineItemsToAdd = [
{
variantId: product.variants[0].id,
quantity: 1,
},
];
const res = await client.checkout.addLineItems(checkoutId, lineItemsToAdd);
console.log(res);
if (cookie.get("checkoutId") === undefined) {
cookie.set("checkoutId", res.id);
}
setCart(res);
};
Using cookies to store your object cart, as far as I know, is not a good idea. You could use localStorage, like so:
import { createContext, useContext, useEffect, useReducer } from "react";
import client from "../lib/client";
const CartStateContext = createContext();
const CartDispatchContext = createContext();
const SET_CART = "SET_CART";
const initalState =
typeof localStorage !== "undefined" && localStorage.getItem("cartState")
? JSON.parse(localStorage.getItem("cartState"))
: {
lineItems: [],
totalPrice: 0,
webUrl: "",
id: "",
};
const reducer = (state, action) => {
switch (action.type) {
case SET_CART:
return { ...state, ...action.payload };
default:
throw new Error(`Unknown action: ${action.type}`);
}
};
export const CartProvider = ({ children }) => {
const [state, dispatch] = useReducer(reducer, initalState);
useEffect(() => {
localStorage.set("cartState", JSON.stringify(state));
}, [state]);
useEffect(() => {
getCart();
}, []);
const setCart = (payload) => dispatch({ type: SET_CART, payload });
const getCart = async () => {
try {
const cart = await client.checkout.create();
setCart(cart);
} catch (err) {
console.log(err);
}
};
return (
<CartDispatchContext.Provider value={{ setCart }}>
<CartStateContext.Provider value={{ state }}>{children}</CartStateContext.Provider>
</CartDispatchContext.Provider>
);
};
export const useCartState = () => useContext(CartStateContext);
export const useCartDispatch = () => useContext(CartDispatchContext);
I'm fairly new to redux toolkit so I'm still having a few issues with it!
I'm stuck on this. I couldn't find what to do, how to follow a method. so i need your help.
My only request is to redirect the user to the home page when he logs in successfully, but I couldn't find what to check, where and how. I will share the code pages that I think will help you.
import { createAsyncThunk } from "#reduxjs/toolkit";
import axios from "axios";
import { TokenService } from "./token.service";
export const AuthService = {};
AuthService.userLogin = createAsyncThunk(
"userSlice/userLogin",
async ({ username, password }) => {
console.log(username, password);
try {
const response = await axios.post(
"https://localhost:7163/api/Login",
{ username, password },
{
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
username: typeof username === "string" ? username.toString() : "",
password: typeof password === "string" ? password.toString() : "",
}),
}
);
if (response.data.status) {
const tokenResponse = { accessToken: response?.data?.data?.token };
TokenService.setToken(tokenResponse);
} else {
console.log("false false false");
return response.data.status;
}
return response.data;
} catch (error) {
console.error(error);
}
}
);
import { createSlice } from "#reduxjs/toolkit";
import { AuthService } from "../../services/auth.service";
const userToken = localStorage.getItem("userToken")
? localStorage.getItem("userToken")
: null;
const initialState = {
userToken,
isLoading: false,
hasError: false,
userinfo: null,
};
const userSlice = createSlice({
name: "userSlice",
initialState,
reducers: {},
extraReducers: (builder) => {
builder
.addCase(AuthService.userLogin.pending, (state, action) => {
state.isLoading = true;
state.hasError = false;
})
.addCase(AuthService.userLogin.fulfilled, (state, action) => {
state.userToken = action.payload;
state.isLoading = false;
state.hasError = false;
})
.addCase(AuthService.userLogin.rejected, (state, action) => {
state.isLoading = false;
state.hasError = true;
});
},
});
export const selectUserToken = (state) => state.userSlice.userToken;
export const selectLoadingState = (state) => state.userSlice.isLoading;
export const selectErrorState = (state) => state.userSlice.hasError;
export default userSlice.reducer;
export default function SignIn() {
const navigate = useNavigate();
const error = useSelector(selectErrorState);
console.log(error);
const [username, setName] = useState("");
const [password, setPassword] = useState("");
const dispatch = useDispatch();
const data = {
username: username,
password: password,
};
const handleNameChange = (event) => {
setName(event.target.value);
};
const handlePasswordChange = (event) => {
setPassword(event.target.value);
};
const handleSubmit = (e) => {
e.preventDefault();
console.log(username, password);
const response = dispatch(AuthService.userLogin(data))
.unwrap()
.then(() => {
if (response.success) {
navigate("/");
}
});
console.log("RESPONSE", response);
console.log(error);
};
//index.js
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<React.StrictMode>
<Provider store={store}>
<BrowserRouter>
<Routes>
<Route path="login" element={<Login />} />
<Route path="register" element={<Register />} />
<Route path="" element={<Home />} />
<Route path="events" element={<Events />} />
</Routes>
</BrowserRouter>
</Provider>
</React.StrictMode>
);
log
my result from API:
Does it make more sense to use the status property here?
{data: {…}, status: true, exception: null, code: 200, message: null}
code: 200
data: {token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1bmlxdWVfb…yMzl9.1P25An_PHA9n4dyQ9JRKOjwPWWtQShWgW9In-gqS7Ek'}
exception: null
message: null
status: true
[[Prototype]]: Object
Even if I enter wrong information, the promise always seems to be fulfilled.
I think I need to use token in this process. The user will see his own information. I can get the token somehow, but I couldn't use it after I put it in localstorage.
While on the login page, it struggled with the data returned from the login meta, but I couldn't. I want to know what is the most logical method in such a situation. Thank you in advance for your answers.
and also useEffect also not working in my file. i tried many way but useEffect not working. if i comments everything file and try then useEffect working else not working.
and also i am getting null value from
const { currentVideo } = useSelector((state) => state.video);
this is my useEffect ->
useEffect(() => {
const fetchData = async () => {
try {
const videoRes = await axiosInstance.get(`/videos/find/${path}`, {
withCredentials: true,
});
// console.log(videoRes);
const updatedView = await axiosInstance.put(`videos/view/${path}`);
// console.log(updatedView.data, "view is updating");
const channelRes = await axiosInstance.get(
`/users/find/${videoRes.data.userId}`,
{ withCredentials: true }
);
setChannel(channelRes.data);
dispatch(fetchSuccess(videoRes.data));
} catch (err) {
console.log(err);
return "opps something went wrong!";
}
};
fetchData();
}, [path, dispatch]);
here i am getting null value
const Video = () => {
const { currentUser } = useSelector((state) => state.user);
const { currentVideo } = useSelector((state) => state.video);
const dispatch = useDispatch();
const path = useLocation().pathname.split("/")[2];
// console.log(currentVideo); // getting null
const [channel, setChannel] = useState({});
and my full video.jsx is
import React, { useEffect, useState } from "react";
import styled from "styled-components";
import ThumbUpOutlinedIcon from "#mui/icons-material/ThumbUpOutlined";
import ThumbDownOffAltOutlinedIcon from "#mui/icons-material/ThumbDownOffAltOutlined";
import ReplyOutlinedIcon from "#mui/icons-material/ReplyOutlined";
import AddTaskOutlinedIcon from "#mui/icons-material/AddTaskOutlined";
import ThumbUpIcon from "#mui/icons-material/ThumbUp";
import ThumbDownIcon from "#mui/icons-material/ThumbDown";
import Comments from "../components/Comments";
import { useDispatch, useSelector } from "react-redux";
import { useLocation } from "react-router-dom";
import axios from "axios";
import { fetchSuccess, like, dislike } from "../redux/videoSlice";
import axiosInstance from "../axios";
import { subscription } from "../redux/userSlice";
import Recommendation from "../components/Recommendation";
import { format } from "timeago.js";
const Video = () => {
const { currentUser } = useSelector((state) => state.user);
const { currentVideo } = useSelector((state) => state.video);
const dispatch = useDispatch();
const path = useLocation().pathname.split("/")[2];
// console.log(currentVideo); // getting null
const [channel, setChannel] = useState({});
/*
// console.log(path); ok hai video user id aa rahi hai.
// its working its getting all the data.
const test = async () => {
const isWorking = await axios.get(
"http://localhost:5000/api/videos/find/63931e44de7c22e61c4ffd6c"
);
console.log(isWorking.data);
console.log(isWorking.data.videoUrl);
};
const videoRes = test();
console.log(videoRes);
*/
// {withCredentials: true}
useEffect(() => {
const fetchData = async () => {
try {
const videoRes = await axiosInstance.get(`/videos/find/${path}`, {
withCredentials: true,
});
// console.log(videoRes);
const updatedView = await axiosInstance.put(`videos/view/${path}`);
// console.log(updatedView.data, "view is updating");
const channelRes = await axiosInstance.get(
`/users/find/${videoRes.data.userId}`,
{ withCredentials: true }
);
setChannel(channelRes.data);
dispatch(fetchSuccess(videoRes.data));
} catch (err) {
console.log(err);
return "opps something went wrong!";
}
};
fetchData();
}, [path, dispatch]);
const handleLike = async () => {
try {
await axiosInstance.put(`/users/like/${currentVideo._id}`, {
withCredentials: true,
});
dispatch(like(currentUser._id));
} catch (err) {
console.log(err);
return "opps something went wrong!";
}
};
const handleDislike = async () => {
try {
await axiosInstance.put(`/users/dislike/${currentVideo._id}`, {
withCredentials: true,
});
dispatch(dislike(currentUser._id));
} catch (err) {
console.log(err);
}
};
const handleSub = async () => {
currentUser.subscribedUsers.includes(channel._id)
? await axiosInstance.put(`/users/unsub/${channel._id}`, {
withCredentials: true,
})
: await axiosInstance.put(`/users/sub/${channel._id}`, {
withCredentials: true,
});
dispatch(subscription(channel._id));
};
if (!currentUser) return "Loading....";
return (
<Container>
<Content>
<VideoWrapper>
<VideoFrame src={currentVideo?.videoUrl} controls />
</VideoWrapper>
<Title>{currentVideo?.title}</Title>
<Details>
<Info>
{currentVideo?.views} views •{format(currentVideo?.createdAt)}
</Info>
<Buttons>
<Button onClick={() => handleLike()}>
{currentVideo?.likes.includes(currentUser._id) ? (
<ThumbUpIcon />
) : (
<ThumbUpOutlinedIcon />
)}{" "}
{currentVideo?.likes.length}
</Button>
<Button onClick={() => handleDislike()}>
{currentVideo?.dislikes.includes(currentUser._id) ? (
<ThumbDownIcon />
) : (
<ThumbDownOffAltOutlinedIcon />
)}
Dislike
</Button>
<Button>
<ReplyOutlinedIcon /> Share
</Button>
<Button>
<AddTaskOutlinedIcon /> Save
</Button>
</Buttons>
</Details>
<Hr />
<Channel>
<ChannelInfo>
<Image src={channel.img} />
<ChannelDetail>
<ChannelName>{channel.name}</ChannelName>
<ChannelCounter>{channel.subscribers} subscribers</ChannelCounter>
<Description>{currentVideo?.desc}</Description>
</ChannelDetail>
</ChannelInfo>
<Subscribe onClick={handleSub}>
{currentUser.subscribedUsers.includes(channel._id)
? "SUBSCRIBED"
: "SUBSCRIBE"}
</Subscribe>
</Channel>
<Hr />
<Comments videoId={currentVideo?._id} />
</Content>
<Recommendation tags={currentVideo?.tags} />
</Container>
);
};
export default Video;
and this is my redux videoslice file videoSlice.js
import { createSlice } from "#reduxjs/toolkit";
const initialState = {
currentVideo: null,
loading: false,
error: false,
};
export const videoSlice = createSlice({
name: "video",
initialState,
reducers: {
fetchStart: (state) => {
state.loading = true;
},
fetchSuccess: (state, action) => {
state.loading = false;
state.currentVideo = action.payload;
},
fetchFailure: (state) => {
state.loading = false;
state.error = true;
},
like: (state, action) => {
if (!state.currentVideo.likes.includes(action.payload)) {
state.currentVideo.likes.push(action.payload);
state.currentVideo.dislikes.splice(
state.currentVideo.dislikes.findIndex(
(userId) => userId === action.payload
),
1
);
}
},
dislike: (state, action) => {
if (!state.currentVideo.dislikes.includes(action.payload)) {
state.currentVideo.dislikes.push(action.payload);
state.currentVideo.likes.splice(
state.currentVideo.likes.findIndex(
(userId) => userId === action.payload
),
1
);
}
},
views: (state, action) => {
if (state.currentVideo.views.includes(action.payload)) {
state.currentVideo.views.push(action.payload);
}
},
},
});
export const { fetchStart, fetchSuccess, fetchFailure, like, dislike, views } =
videoSlice.actions;
export default videoSlice.reducer;
in browser i am also getting null
i am trying to call my api and render data. but useeffect not working in my code. also from useSelector i am getting null value.
I am new to react and MongoDB, I am trying to add months to a date in my database in mongo, but it only updates the first time I click on the <Price> button, I need it to update every time I click it. The user has to log out and log back in for it to work again, but still only 1 update can be made to the database. Can someone explain to me why this is happening, and how could it be fixed?
This is the function
import React, { useContext } from "react";
import { useState } from "react";
import useFetch from "../../hooks/useFetch";
import Footer from "../../components/Footer";
import Navbar from "../../components/Navbar";
import Sidebar from "../../components/Sidebar";
import {
ContractContainer,
HeadingContainer,
TypeH1,
ActiveUntil,
MonthlyWrapper,
MonthlyContainer,
MonthNumber,
Price,
Navbarback,
} from "./userinfoElements";
import { AuthContext } from "../../context/AuthContext";
import moment from "moment";
import axios from "axios";
const Userinfo = () => {
// for nav bars
const [isOpen, setIsOpen] = useState(false);
// set state to true if false
const toggle = () => {
setIsOpen(!isOpen);
};
const { user } = useContext(AuthContext);
let { data, loading, reFetch } = useFetch(`/contracts/${user.contractType}`);
let dateFormat = moment(user.activeUntil).format("DD/MMMM/yyyy");
const updateDate = async () => {
try {
let newDate = moment(user.activeUntil).add(1, "months");
dateFormat = newDate.format("DD/MMMM/yyyy");
axios.put(`/activedate/${user.namekey}`, {
activeUntil: newDate,
});
} catch (err) {
console.log(err);
}
reFetch();
};
return (
<>
<Sidebar isOpen={isOpen} toggle={toggle} />
{/* navbar for smaller screens*/}
<Navbar toggle={toggle} />
<Navbarback /> {/* filling for transparent bacground navbar*/}
{loading ? (
"Loading components, please wait"
) : (
<>
<ContractContainer>
<HeadingContainer>
<TypeH1>{data.contractType}</TypeH1>
<ActiveUntil>Subscription active until {dateFormat}</ActiveUntil>
</HeadingContainer>
<MonthlyWrapper>
<MonthlyContainer>
<MonthNumber>1 Month</MonthNumber>
<Price onClick={updateDate}>{data.month1Price}$</Price>
</MonthlyContainer>
<MonthlyContainer>
<MonthNumber>3 Month</MonthNumber>
<Price onClick={updateDate}>{data.month3Price}$</Price>
</MonthlyContainer>
<MonthlyContainer>
<MonthNumber>6Month</MonthNumber>
<Price onClick={updateDate}>{data.month6Price}$</Price>
</MonthlyContainer>
<MonthlyContainer>
<MonthNumber>12Month</MonthNumber>
<Price onClick={updateDate}>{data.month12Price}$</Price>
</MonthlyContainer>
</MonthlyWrapper>
</ContractContainer>
</>
)}
<Footer />
</>
);
};
export default Userinfo;
this is the fetch hook
import { useEffect, useState } from "react";
import axios from "axios";
const useFetch = (url) => {
const [data, setData] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(false);
useEffect(() => {
const fetchData = async () => {
setLoading(true);
try {
const res = await axios.get(url);
setData(res.data);
} catch (err) {
setError(err);
}
setLoading(false);
};
fetchData();
}, [url]);
const reFetch = async () => {
setLoading(true);
try {
const res = await axios.get(url);
setData(res.data);
} catch (err) {
setError(err);
}
setLoading(false);
};
return { data, loading, error, reFetch };
};
export default useFetch;
Any help is appreciated!
EDIT: added AuthContext file and server sided controllers if needed
import React from "react";
import { createContext, useEffect, useReducer } from "react";
const INITIAL_STATE = {
user: JSON.parse(localStorage.getItem("user")) || null,
loading: false,
error: null,
};
export const AuthContext = createContext(INITIAL_STATE);
const AuthReducer = (state, action) => {
switch (action.type) {
case "LOGIN_START":
return {
user: null,
loading: true,
error: null,
};
case "LOGIN_SUCCESS":
return {
user: action.payload,
loading: false,
error: null,
};
case "LOGIN_FAILURE":
return {
user: null,
loading: false,
error: action.payload,
};
case "LOGOUT":
return {
user: null,
loading: false,
error: null,
};
default:
return state;
}
};
export const AuthContextProvider = ({ children }) => {
const [state, dispatch] = useReducer(AuthReducer, INITIAL_STATE);
useEffect(() => {
localStorage.setItem("user", JSON.stringify(state.user));
}, [state.user]);
return (
<AuthContext.Provider
value={{
user: state.user,
loading: state.loading,
error: state.error,
dispatch,
}}
>
{children}
</AuthContext.Provider>
);
};
api Controller to update active date
import User from "../models/User.js";
export const updateActiveDate = async (req, res, next) => {
try {
await User.updateOne({ $set: { activeUntil: req.body.activeUntil } });
res.status(200).json("Active date has been updated.");
} catch (err) {
next(err);
}
};
api Controller to find contracts
import Contracts from "../models/Contracts.js";
export const getContract = async (req, res, next) => {
try {
const Contract = await Contracts.findOne({
contractType: req.params.contractType,
});
res.status(200).json(Contract);
} catch (err) {
next(err);
}
};
api Controller for login authentication
export const login = async (req, res, next) => {
try {
const user = await User.findOne({ namekey: req.body.namekey });
if (!user) return next(createError(404, "User not found!"));
if (req.body.password === undefined) {
return next(createError(500, "Wrong password or namekey!"));
}
const isPasswordCorrect = await bcrypt.compare(
req.body.password,
user.password
);
if (!isPasswordCorrect)
return next(createError(400, "Wrong password or namekey!"));
const token = jwt.sign({ id: user._id }, process.env.JWT);
const { password, ...otherDetails } = user._doc;
res
.cookie("access_token", token, {
httpOnly: true,
})
.status(200)
.json({ details: { ...otherDetails } });
} catch (err) {
next(err);
}
};
You should update the stored user state to reflect the activeUntil date change.
Define a 'UPDATE_USER_DATE' action in your reducer to update the user instance:
case "UPDATE_USER_DATE":
const updatedUser = { ...state.user };
updatedUser.activeUntil = action.payload;
return {
...state,
user: updatedUser
};
Then, after updating the date in updateDate, update the user state as well:
const { user, dispatch } = useContext(AuthContext);
const updateDate = async () => {
try {
let newDate = moment(user.activeUntil).add(1, "months");
dateFormat = newDate.format("DD/MMMM/yyyy");
await axios.put(`/activedate/${user.namekey}`, {
activeUntil: newDate,
});
dispatch({ type: "UPDATE_USER_DATE", payload: newDate });
} catch (err) {
console.log(err);
}
reFetch();
};
Give this a try. It awaits the put request, and only once that has responded it calls reFetch. Without the await you're calling the reFetch before the put request has had a chance to complete its work.
const updateDate = async () => {
try {
let newDate = moment(user.activeUntil).add(1, "months");
dateFormat = newDate.format("DD/MMMM/yyyy");
await axios.put(`/activedate/${user.namekey}`, {
activeUntil: newDate,
});
} catch (err) {
console.log(err);
} finally {
reFetch();
}
};
I am creating an app about goal tracker. When I logout from an account, everything goes okay except the the logout gets stuck in pending state.
There is also error in console saying Cannot read properties of null (reading 'token') Dashboard.jsx:20.
Dashboard.jsx
import React from 'react';
import { useEffect } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { useNavigate } from 'react-router-dom';
import GoalForm from '../components/GoalForm';
import Spinner from '../components/Spinner';
import { getGoals, reset } from '../features/goals/goalSlice';
import GoalItem from '../components/GoalItem';
function Dashboard() {
const navigate = useNavigate();
const dispatch = useDispatch();
const { user } = useSelector(store => store.auth);
const { goals, isLoading, isError, message } = useSelector(
store => store.goals,
);
useEffect(() => {
if (isError) console.log(message);
if (!user) navigate('/login');
dispatch(getGoals());
return () => {
dispatch(reset());
};
}, [user, isError, message, navigate, dispatch]);
if (isLoading) return <Spinner />;
return (
<>
<section className='heading'>
<h1>Welcome {user && user.name}</h1>
<p>Goals Dashboard</p>
</section>
<GoalForm />
<section className='content'>
{goals.length > 0 ? (
<div className='goals'>
{goals.map(goal => (
<GoalItem key={goal._id} goal={goal} />
))}
</div>
) : (
<h3>You have not set any goals</h3>
)}
</section>
</>
);
}
export default Dashboard;
goalSlice.js
import { createSlice, createAsyncThunk } from '#reduxjs/toolkit';
import goalService from './goalService';
const initialState = {
goals: [],
isError: false,
isSuccess: false,
isLoading: false,
message: '',
};
// Create goal
export const createGoal = createAsyncThunk(
'goals/create',
async (goalData, thunkAPI) => {
try {
const token = thunkAPI.getState().auth.user.token;
return await goalService.createGoal(goalData, token);
} catch (error) {
const message =
(error.response &&
error.response.data &&
error.response.data.message) ||
error.message ||
error.toString();
return thunkAPI.rejectWithValue(message);
}
},
);
// Get Goals
export const getGoals = createAsyncThunk('goals/get', async (_, thunkAPI) => {
try {
const token = thunkAPI.getState().auth.user.token;
return await goalService.getGoals(token);
} catch (error) {
const message =
(error.response && error.response.data && error.response.data.message) ||
error.message ||
error.toString();
return thunkAPI.rejectWithValue(message);
}
});
// Delete goal
export const deleteGoal = createAsyncThunk(
'goals/delete',
async (id, thunkAPI) => {
try {
const token = thunkAPI.getState().auth.user.token;
return await goalService.deleteGoal(id, token);
} catch (error) {
const message =
(error.response &&
error.response.data &&
error.response.data.message) ||
error.message ||
error.toString();
return thunkAPI.rejectWithValue(message);
}
},
);
export const goalSlice = createSlice({
name: 'goal',
initialState,
reducers: {
reset: state => initialState,
},
extraReducers: builder => {
builder
.addCase(createGoal.pending, state => {
state.isLoading = true;
})
.addCase(createGoal.fulfilled, (state, action) => {
state.isLoading = false;
state.isSuccess = true;
state.goals.push(action.payload);
})
.addCase(createGoal.rejected, (state, action) => {
state.isLoading = false;
state.isError = true;
state.message = action.payload;
})
.addCase(getGoals.pending, state => {
state.isLoading = true;
})
.addCase(getGoals.fulfilled, (state, action) => {
state.isLoading = false;
state.isSuccess = true;
state.goals = action.payload;
})
.addCase(getGoals.rejected, (state, action) => {
state.isLoading = false;
state.isError = true;
state.message = action.payload;
})
.addCase(deleteGoal.pending, state => {
state.isLoading = true;
})
.addCase(deleteGoal.fulfilled, (state, action) => {
state.isLoading = false;
state.isSuccess = true;
state.goals = state.goals.filter(
goal => goal._id !== action.payload.id,
);
})
.addCase(deleteGoal.rejected, (state, action) => {
state.isLoading = false;
state.isError = true;
state.message = action.payload;
});
},
});
export const { reset } = goalSlice.actions;
export default goalSlice.reducer;
goalService.js
import axios from 'axios';
const API_URL = '/api/goals';
// Create goal
const createGoal = async (goalData, token) => {
const config = {
headers: {
'x-auth-token': `${token}`,
},
};
const response = await axios.post(API_URL, goalData, config);
return response.data;
};
// Get goals
const getGoals = async token => {
const config = {
headers: {
'x-auth-token': `${token}`,
},
};
const response = await axios.get(API_URL, config);
return response.data;
};
// Delete goal
const deleteGoal = async (goalId, token) => {
const config = {
headers: {
'x-auth-token': `${token}`,
},
};
const response = await axios.get(`${API_URL}/${goalId}`, config);
// console.log(response);
return response.data;
};
const goalService = {
createGoal,
getGoals,
deleteGoal,
};
export default goalService;
And this is logout part: builder.addCase(logout.fulfilled, state => {state.user = null});
When i try to logout, the user is logged out but the error appears continuously, like re-rendering the page. The page is re-rendered itself and state is stuck in logout.
Delete Goal is also not working
You must check if the user is logged in before attempt to get the goals
useEffect(() => {
if (isError) console.log(message);
if (!user) {
navigate('/login');
} else {
dispatch(getGoals());
}
return () => {
dispatch(reset());
};
}, [user, isError, message, navigate, dispatch]);