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);
when I lose in a hangman I want to reload my data in the API so that a new password appears. Unfortunately I have no idea how to reload it without reloading the whole page, is it even possible to run the api again on a button click for example?
import { createSlice, createAsyncThunk } from '#reduxjs/toolkit';
import axios from 'axios';
const url = 'https://random-word-api.herokuapp.com/word?number=1';
const initialState = {
password: '1',
misstake: 0,
usedChart: [],
};
export const getPassword = createAsyncThunk(
'hangman/getPassword',
async (thunkAPI) => {
console.log(1)
try {
const resp = await axios(url)
return resp.data
} catch(error){
return thunkAPI.rejectWithValue('api not working');
}
}
);
const HangManSlice = createSlice({
name: 'hangman',
initialState,
reducers: {
increaseError: (state) => {
state.misstake += 1
},
usedCharts: (state, action) => {
state.usedChart.push(action.payload)
},
restart: (state) => {
state.misstake = 0
state.usedChart = []
getPassword()
}
},
extraReducers: (builder) => {
builder
.addCase(getPassword.fulfilled, (state, action) => {
state.password = action.payload;
})
}
})
export const { increaseError, usedCharts, restart } = HangManSlice.actions
export default HangManSlice.reducer
You should dispatch the getPassword() from the "restart" button handler instead of calling it your reducer.
const RestartButton = () => {
const dispatch = useDispatch();
const restartHandler = () => {
dispatch(getPassword());
dispatch(restart())
};
return (
<button onClick={restartHandler}>Restart</button>
);
}
Another option is to use the getPassword thunk to initialize a new game like so:
import { createSlice, createAsyncThunk } from '#reduxjs/toolkit';
import axios from 'axios';
const url = 'https://random-word-api.herokuapp.com/word?number=1';
const initialState = {
loading: false,
password: '1',
misstake: 0,
usedChart: [],
};
export const startGame = createAsyncThunk(
'hangman/getPassword',
async (thunkAPI) => {
console.log(1)
try {
const resp = await axios(url)
return resp.data
} catch(error){
return thunkAPI.rejectWithValue('api not working');
}
}
);
const HangManSlice = createSlice({
name: 'hangman',
initialState,
reducers: {
increaseError: (state) => {
state.misstake += 1
},
usedCharts: (state, action) => {
state.usedChart.push(action.payload)
},
},
extraReducers: (builder) => {
builder
.addCase(startGame.pending, (state, action) => {
state.loading = true;
})
.addCase(startGame.rejected, (state, action) => {
state.loading = false;
})
.addCase(startGame.fulfilled, (state, action) => {
state.loading = false;
// store new password
state.password = action.payload;
// reset state
state.misstake = 0
state.usedChart = []
})
}
})
export const { increaseError, usedCharts, restart } = HangManSlice.actions
export default HangManSlice.reducer
Now you can dispatch startGame(), and the state will be reset with a new password.
I'm learning react context and while developing a todo application using useContext, I'm facing an issue where on submitting one task, the same task gets added two times to an array. The output component would loop through this array and display the results. While debugging I observed that, although the submit of task add only one entry into the array, not sure why and how, the consumer component gets the array with duplicate entry. Please let me know, what I'm missing.
Here is my code of index file that maintains context
import { createContext, useReducer } from "react";
import ContextReducer, { initialState } from "./ContextReducer";
const taskContext = createContext();
const ContextProvider = (props) => {
const [state, dispatch] = useReducer(ContextReducer, initialState);
const setTaskInput = (taskInput) => {
dispatch({
type: "SET_TASKINPUT",
payload: taskInput,
});
};
const addTask = (task) => {
dispatch({
type: "ADD_TASK",
payload: task,
});
};
const deleteTask = (id) => {
dispatch({
type: "DELETE_TASK",
payload: id,
});
};
const todoContext = {
todo: state.todo,
taskInput: state.taskInput,
setTaskInput,
addTask,
deleteTask,
};
return (
<taskContext.Provider value={todoContext}>
{props.children}
</taskContext.Provider>
);
};
export { taskContext };
export default ContextProvider;
This is the code for reducer
const initialState = {
todo: [],
taskInput: "",
};
const ContextReducer = (state = initialState, action) => {
if (action.type === "SET_TASKINPUT") {
state.taskInput = action.payload;
return {
todo: state.todo,
taskInput: state.taskInput,
};
}
if (action.type === "ADD_TASK") {
state.todo = [...state.todo, action.payload];
return {
todo: state.todo,
taskInput: state.taskInput,
};
}
if (action.type === "DELETE_TASK") {
state.todo = state.todo.filter((todo) => todo.id !== action.payload);
return {
todo: state.todo,
taskInput: state.taskInput,
};
}
return state;
};
export { initialState };
export default ContextReducer;
This is the code of output component or say, consumer component
import React, { Fragment, useContext } from "react";
import { taskContext } from "../../Context";
import styles from "./Content.module.css";
const Output = () => {
const { todo, deleteTask } = useContext(taskContext);
const deleteHandler = (e) => {
deleteTask(+e.target.parentElement.parentElement.id);
};
return (
<Fragment>
{todo.length > 0 && (
<div className={styles.outputDiv}>
<ul>
{todo.map((task) => {
return (
<li key={task.id} id={task.id}>
<div className={styles.row1}>{task.task}</div>
<div className={styles.row2}>
<button className={styles.edit}>Edit</button>
<button className={styles.delete} onClick={deleteHandler}>
Delete
</button>
</div>
</li>
);
})}
</ul>
</div>
)}
</Fragment>
);
};
export default Output;
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.
I am using hooks and context api.I have multiple actions that write them into seperate file.my problem this:in another file how can I access state?
I use this file for create my contexts:
createContext.js
import React, { useReducer } from "react";
export default (reducer, actions, defaultValue) => {
const Context = React.createContext();
const Provider = ({ children }) => {
const [state, dispatch] = useReducer(reducer, defaultValue);
const boundActions = {};
for (let key in actions) {
boundActions[key] = actions[key](dispatch);
}
return (
<Context.Provider value={{ state, ...boundActions }}>
{children}
</Context.Provider>
);
};
return { Context, Provider };
};
and when I want to create context I pass actions ,reducer and default values to createContext file and get Context and Provider from that.like this:
productContext.js
import createDataContext from "./createDataContext";
import {storeProducts, detailProduct} from "../data";
const productReducer = (state, action) => {
switch (action.type) {
case "GET_ITEM":
return {...state, productDetail: action.productDetail};
case "ADD_TOTALS":
return {
...state,
cartSubTotal: action.cartSubTotal,
cartTotal: action.cartTotal,
cartTax: action.cartTax
};
case "ADD_TO_CART":
return {
...state,
products: action.tempProducts,
cart: [...state.cart, action.product]
};
default:
return state;
}
};
const getItem = (id) => {
const product = **products**.find(item => item.id === id);
return product;
}
const handleDetail = dispatch => (id) => {
const productDetail = getItem(id);
dispatch({type: "GET_ITEM", productDetail})
};
const addToCart = dispatch => (id) => {
let tempProducts = [...storeProducts];
const index = tempProducts.indexOf(getItem(id));
const product = tempProducts[index];
product.inCart = true;
product.count = 1;
const price = product.price;
product.total = price;
dispatch({
type: "ADD_TO_CART",
tempProducts,
product
});
const data = addTotals();
dispatch({
type: "ADD_TOTALS",
cartSubTotal: data.cartSubTotal,
cartTotal: data.cartTotal,
cartTax: data.cartTax
});
};
const addTotals = () => {
let subTotal = 0;
**cart**.map(item =>{ (subTotal += item.total)});
const tempTax = subTotal * 0.1;
const tax = parseFloat(tempTax.toFixed(2));
const total = subTotal + tax;
return {cartSubTotal: subTotal, cartTax: tax, cartTotal: total};
};
export const {Provider, Context} = createDataContext(
productReducer,
{
handleDetail,
},
{
products: storeProducts,
productDetail: detailProduct,
cart: [],
modalOpen: false,
modalProduct: detailProduct,
cartSubTotal: 0,
cartTax: 0,
cartTotal: 0
);
I can not access cart and products that are bold.how can I use them?
It looks like you're doing a lot of work in the action creator function that would make more sense as part of the reducer. For example, instead of this:
const productReducer = (state, action) => {
switch (action.type) {
case 'GET_ITEM':
return { ...state, productDetail: action.productDetail };
default:
return state;
}
};
const getItem = (id) => {
// no access to state!
const product = products.find((item) => item.id === id);
return product;
};
const handleDetail = (dispatch) => (id) => {
const productDetail = getItem(id);
dispatch({ type: 'GET_ITEM', productDetail });
};
You can do this:
// action value
{ type: 'GET_ITEM', id: 1234 }
// reducer
const productReducer = (state, action) => {
switch (action.type) {
case 'GET_ITEM':
const productDetail = state.products.find(
(item) => item.id === action.id
);
return { ...state, productDetail };
default:
return state;
}
};
Inside the reducer is where you have access to both the action and the state. Try to design your actions so that they contain the smallest amount of information possible to achieve your intention.