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);
see the screenshot . Why images getting corrupted? I am trying to upload images using axois post but axois post can't processing my images correctly . My code divided in two part. First part where I writing logic for upload multi image and second part I am using it in my page component.
first part
this code for upload multi image
export const MultiImageUpload = ({Setimage}) => {
const [selectedImages, setSelectedImages] = useState([]);
const onSelectFile = (event) => {
const selectedFiles = event.target.files;
const selectedFilesArray = Array.from(selectedFiles);
const imagesArray = selectedFilesArray.map((file) => {
return URL.createObjectURL(file);
});
setSelectedImages((previousImages) => previousImages.concat(imagesArray));
Setimage((previousImages) => previousImages.concat(imagesArray));
// FOR BUG IN CHROME
event.target.value = "";
};
function deleteHandler(image) {
setSelectedImages(selectedImages.filter((e) => e !== image));
Setimage(selectedImages.filter((e) => e !== image));
URL.revokeObjectURL(image);
}
second part
now I am importing this component in my page
const AdsPost = ({data}) => {
const[image,Setimage] = useState([])
var data = new FormData();
image.forEach(file=>{
data.append("files", file)
console.log("image_url:",file)
})
let submit_ads = axios.post(url,data,{headers:headers}).then((res)=>{
console.log(res)
})
here is myjsx
<MultiImageUpload
Setimage={Setimage}/>
I can upload image using postman but don't know why axois post can't upload images.
Here is your problem :
image.forEach(file=>{
data.append("files", file)
console.log("image_url:",file)
})
The parameter file is not a file, but it's the result of
const imagesArray = selectedFilesArray.map((file) => {
return URL.createObjectURL(file); // <--- this line
});
In other words, you are essentially doing
data.append("files", URL.createObjectURL(file));
Fix that and your code should work.
Solution
Here is a sandbox with a proposed solution, the idea is to delegate the state of the files to a Provider, and use the context down in child components as needed.
./context/files.js
import { createContext } from "react";
export default createContext({
/** #return {{ file:File, dataUrl:string }[]]} */
get files() {
return [];
},
/** #return {Error[]} */
get errors() {
return [];
},
/** #param {File[]} files */
addFiles(files) {},
/** #param {File} file */
removeFile(file) {},
/** #param {Error[]} errors */
setErrors(errors) {}
});
./providers/FilesProvider.jsx
import { useContext, useMemo, useState } from "react";
import FilesContext from "../context/files";
const FilesProvider = ({ children }) => {
const [internal, setInternal] = useState(() => ({
files: [],
errors: []
}));
const contextValue = useMemo(
() => ({
get files() {
return internal.files;
},
get errors() {
return internal.errors;
},
addFiles(filesAdded) {
setInternal(({ files, errors }) => ({
files: files.concat(
filesAdded.map((file) => ({
file,
dataUrl: URL.createObjectURL(file)
}))
),
errors
}));
},
removeFile(fileRemoved) {
URL.revokeObjectURL(fileRemoved);
setInternal(({ files, errors }) => ({
files: files.filter(({ file }) => file !== fileRemoved),
errors
}));
},
setErrors(errors) {
setInternal(({ files }) => ({ files, errors }));
}
}),
[internal]
);
return (
<FilesContext.Provider value={contextValue}>
{children}
</FilesContext.Provider>
);
};
const useFiles = () => useContext(FilesContext);
export default FilesProvider;
export { useFiles };
Usage
<FilesProvider>
<FilesSelectComponent />
</FilesProvider>
and
const { files, errors, addFiles, removeFile, setErrors } = useFiles();
I want to know if that is the correct way i do it.
Firstly i fetch the hard coded data from my API and display it into the screen. I also have a form from which i send data and i want the axios.get method to instantly fetch the newest updated data that was sent from a form. I made a helper state that is put in to "useEffect array dependencies" and whenever that state changes its value, app reloads and fetches again.
Context file with useEffect hook:
import { createContext, useState, useEffect } from "react";
import { apiService } from "../services/api/api.service";
const Context = createContext({
footer: false,
subjectForm: false,
storedSubjects: [],
footerVisibilityHandler: () => {},
subjectFormVisibilityHandler: () => {},
});
export const ContextProvider = ({ children }) => {
const [footer, setFooter] = useState(false);
const [subjectForm, setSubjectForm] = useState(false);
const [storedSubjects, setStoredSubjects] = useState([]);
const [send, setSend] = useState(false);
useEffect(() => {
const getData = async () => {
try {
const getSubjects = await apiService.getSubjects();
const tableRow = getSubjects.data.map((subject) => {
return {
name: subject.name,
};
});
setStoredSubjects(tableRow);
} catch (err) {
console.log(err);
}
};
getData();
}, [send]);
const footerVisibilityHandler = () => {
setFooter((previousState) => !previousState);
setSubjectForm(false);
};
const subjectFormVisibilityHandler = () => {
setSubjectForm((previousState) => !previousState);
};
const context = {
footer,
subjectForm,
storedSubjects,
footerVisibilityHandler,
subjectFormVisibilityHandler,
setSend,
};
return <Context.Provider value={context}>{children}</Context.Provider>;
};
export default Context;
Form from which i send data:
import Context from "../../store/context";
import FormContainer from "../UI/FormContainer";
import { apiService } from "../../services/api/api.service";
import { useContext, useRef } from "react";
const AddSubject = () => {
const ctx = useContext(Context);
const subject = useRef("");
const sendData = (e) => {
e.preventDefault();
ctx.setSend((prevState) => !prevState);
apiService.addSubject(subject.current.value);
};
return (
<FormContainer show={ctx.subjectForm} send={sendData}>
<label htmlFor="subject">Subject Name: </label>
<input type="text" id="subject" ref={subject} />
<button type="submit">Add</button>
</FormContainer>
);
};
export default AddSubject;
Api endpoints file:
import axios from "axios";
const api = () => {
const baseUrl = "https://localhost:5001/api";
let optionAxios = {
headers: {
"Content-Type": "multipart/form-data",
},
};
return {
getSubjects: () => axios.get(`${baseUrl}/subjectcontroller/getsubjects`),
addSubject: (subjectName) =>
axios.post(
`${baseUrl}/subjectcontroller/createsubject`,
{
name: subjectName,
},
optionAxios
),
};
};
export const apiService = api();
I'd like to use custom api request hook like so
// hooks.js
import { useState, useEffect } from 'react';
import api from '../utils/api';
function useAPI(fn, payload) {
const [state, setState] = useState({
loading: false,
data: null,
error: null,
});
const callAPI = async () => {
const setAPIState = update => setState({ ...state, ...update });
try {
setAPIState({ loading: true });
const data = await fn(payload);
setAPIState({ data, loading: false });
} catch (error) {
setAPIState({ error, loading: false });
}
};
useEffect(() => {
callAPI();
}, [fn, payload]);
return state;
}
export const useFetchTransactions = payload => {
const fetchTransactions = api.fetchTransactions;
return useAPI(fetchTransactions, payload);
};
and call it within my React component like so:
// Component.jsx
import { useState } from 'react';
import { useFetchTransactions } from '../hooks.js';
export default const Component = () => {
const [id, setID] = useState('');
const [date, setDate] = useState('12/13/21');
const { loading, data, error } = useFetchTransactions({ id, date });
return (
<div>{data.map...}</div>
)
}
However, this looks wrong. I think useFetchTransactions should live inside of a useEffect, so that the component then becomes something like:
// Updated Component.jsx
import { useState, useEffect } from 'react';
import { useFetchTransactions } from '../hooks.js';
export default const Component = () => {
const [id, setID] = useState('');
const [date, setDate] = useState('12/13/21');
const [apiState, setApiState] = useState({
data: null,
error: null,
loading: false
})
useEffect(() => {
const result = useFetchTransactions({ id, date });
setState({ ...apiState, ...result });
}, [date, id])
return (
<div>{apiState.data.map...}</div>
)
}
But the above feels cumbersome and redundant. Can anyone please lend some knowledge on this? Thanks!
You can do it like this
function useAPI(fn) {
const [state, setState] = useState({
loading: false,
data: null,
error: null,
});
const callAPI = async (payload) => {
const setAPIState = update => setState({ ...state, ...update });
try {
setAPIState({ loading: true });
const data = await fn(payload);
setAPIState({ data, loading: false });
} catch (error) {
setAPIState({ error, loading: false });
}
};
return {state, callAPI};
}
export const useFetchTransactions = () => {
const fetchTransactions = api.fetchTransactions;
return useAPI(fetchTransactions);
};
And then in your component
import { useState } from 'react';
import { useFetchTransactions } from '../hooks.js';
export default const Component = () => {
const [id, setID] = useState('');
const [date, setDate] = useState('12/13/21');
const { callAPI, loading, data, error } = useFetchTransactions();
useEffect(() => {
callAPI({ id, date })
}, [date, id])
return (
<div>{data.map...}</div>
)
}
I have a component and I want to fetch isbn data on button click using react hook useEffect, performing a get on the route ${basicUrl}/editorials/${isbn}, so i wrote this component:
import React, { Fragment, useState } from "react";
import "./Home.css";
import { V3_BASIC_URL } from "../../constants/endpoints";
import { useDataApi } from "../../store/effects/dataEffects";
import SearchIsbnElement from "../../components/SearchIsbnElement/SearchIsbnElement";
import IsbnPanelElement from "../../components/IsbnPanelElement/IsbnPanelElement";
function Home() {
const [query, setQuery] = useState<string>("9788808677853");
const [isValid, setIsValid] = useState<boolean>(true);
const url = `${V3_BASIC_URL(
process.env.REACT_APP_API_ENV
)}/editorials/${query}`;
const [{ isbn, isLoading, isError }, doFetch] = useDataApi(url, {
isLoading: false,
isError: false,
isbn: undefined,
});
const buttonCallback = () => {
doFetch(url);
};
const isbnRegexp = /^97\d{11}$/
const validateQuery = (query: string): boolean => isbnRegexp.test(query)
const inputCallback = (query: string) => {
setQuery(query)
setIsValid(validateQuery(query));
};
return (
<div id="isbn-panel-home" className="Home">
<SearchIsbnElement
inputCallback={inputCallback}
buttonCallback={buttonCallback}
query={query}
isValid={isValid}
></SearchIsbnElement>
{isError && <div>Il servizio al momento non è disponibile, riprova più tardi</div>}
{isLoading ? (
<div>Loading ...</div>
) : (
!isError &&
<Fragment>
<IsbnPanelElement isbn={isbn}></IsbnPanelElement>
<p>{isbn?.scheda_volume == null && 'isbn non trovato'}</p>
</Fragment>
)}
</div>
);
}
export default Home;
the useDataApi function uses the hook useEffect and returns state and setUrl action to set the new url on isbn value change. This is the useDataApi file:
import { useState, useEffect, useReducer } from "react";
import {
dataFetchFailure,
dataFetchInit,
dataFetchSuccess,
} from "../actions/dataActions";
import { dataFetchReducer, ISBNState } from "../reducers/dataReducers";
import { get } from "../../tools/request";
type InitialState = {
isLoading: boolean,
isError: boolean,
isbn: undefined,
}
export const useDataApi = (initialUrl: string, initialData: InitialState) : [ISBNState, (value: string) => void] => {
const [url, setUrl] = useState(initialUrl);
const [state, dispatch] = useReducer(dataFetchReducer, initialData);
useEffect(() => {
let didCancel: boolean = false;
const fetchData = async (): Promise<any> => {
dispatch(dataFetchInit());
const options = {
headers: {
Accept: 'application/json',
'Content-Type': 'application/json'
},
auth: {
username: `${process.env.REACT_APP_API_AUTH_USER}`,
password: `${process.env.REACT_APP_API_AUTH_PWD}`
}
}
try {
const {data} = await get(url, options);
if (!didCancel) {
dispatch(dataFetchSuccess(data));
}
} catch (error) {
if (!didCancel) {
dispatch(dataFetchFailure(error));
}
}
};
fetchData();
return () => {
didCancel = true;
};
}, [url]);
return [state, setUrl];
};
with this code fetching starts on page load, but i want to fetch data only on button click. How can I do this?
useEffect() is a hook to manipulate the component through the different lifecycle methods. In order to do something onClick you need to create a method for that:
const fetchData = async (): Promise<any> => {
dispatch(dataFetchInit());
const options = {
headers: {
Accept: 'application/json',
'Content-Type': 'application/json'
},
auth: {
username: `${process.env.REACT_APP_API_AUTH_USER}`,
password: `${process.env.REACT_APP_API_AUTH_PWD}`
}
}
try {
const {data} = await get(url, options);
if (!didCancel) {
dispatch(dataFetchSuccess(data));
}
} catch (error) {
if (!didCancel) {
dispatch(dataFetchFailure(error));
}
}
};
Just do that and you will be fine
Edit: the new version of useDataApi
export const useDataApi = (
url: string,
initialData: InitialState
): [ISBNState, (value: string) => void] => {
const [state, dispatch] = useReducer(dataFetchReducer, initialData);
const fetchData = useCallback(async (): Promise<any> => {
dispatch(dataFetchInit());
const options = {
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
auth: {
username: `${process.env.REACT_APP_API_AUTH_USER}`,
password: `${process.env.REACT_APP_API_AUTH_PWD}`,
},
};
try {
const { data } = await get(url, options);
dispatch(dataFetchSuccess(data));
} catch (error) {
dispatch(dataFetchFailure(error));
}
}, [url]);
return [state, fetchData];
};
The useDataApi hook returns [,doFetch], but the doFetch is actually setUrl so if you wanted that to work as expected you can let the initial value for the url be null or falsey and only allow a fetch inside the effect when the url is valid/truthy. When you click the button, thats when you setUrl and that's when the effect will allow a fetchData to occur because by then the value of url will be set.
export const useDataApi = (initialUrl: string, initialData: InitialState): [ISBNState, (value: string) => void] => {
// make this default to null here, or where you intende to use this hook
const [url, setUrl] = useState(null);
// custom hook body
useEffect(() => {
// effect body
if (url) {
fetchData();
}
// hook cleanup
}, [url]);
return [state, setUrl];
};
Although, the better solution is directly calling the function fetchData on the button click. One way you can do that is by modifying your useDataApi hook to return 'fetchData' directly allowing it to accept the url as an argument and removing the need for the const [url,setUrl] = useState(initialUrl) entirely
export const useDataApi = (initialUrl: string, initialData: InitialState): [ISBNState, (value: string) => void] => {
const [state, dispatch] = useReducer(dataFetchReducer, initialData);
const fetchData = useCallback(async (url): Promise<any> => {
dispatch(dataFetchInit());
const options = {
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
auth: {
username: `${process.env.REACT_APP_API_AUTH_USER}`,
password: `${process.env.REACT_APP_API_AUTH_PWD}`,
},
};
try {
const { data } = await get(url, options);
if (!didCancel) {
dispatch(dataFetchSuccess(data));
}
} catch (error) {
if (!didCancel) {
dispatch(dataFetchFailure(error));
}
}
}, []);
return [state, fetchData];
};
You can also drop initialUrl from the hook useDataApi