React. How to save on LocalStorage when using Context - reactjs

I am trying to keep track of the number of favorites in the app.
It is working fine, except that I would like to store the numeric value in LocalStorage.
This is the custom Hook:
import { useState } from "react"
const initialState = {
favCounter: 0
}
const useInitialState = () => {
const [state, setState] = useState(initialState)
const incrementFav = () => {
setState({
...state,
favCounter: state.favCounter + 1
})
}
const decrementFav = () => {
setState({
...state,
favCounter: state.favCounter - 1
})
}
return {
state,
incrementFav,
decrementFav
}
}
export default useInitialState
and this is the component where I apply it:
const { incrementFav, decrementFav } = useContext(AppContext)
const handleFav = () => {
if (liked) {
decrementFav()
} else {
incrementFav()
}
}
<button onClick={
() => {
handleFav()
}
}>
<Icon size="28px" />
</button>

Solved, paste code in case someone need it
import { useState } from "react"
const initialState = {
// favCounter: 0
favCounter: JSON.parse(window.localStorage.getItem("favCounter"))
}
const useInitialState = () => {
const [state, setState] = useState(initialState)
const incrementFav = () => {
setState({
...state,
favCounter: state.favCounter + 1
})
window.localStorage.setItem("favCounter", JSON.stringify(state.favCounter + 1))
}
const decrementFav = () => {
setState({
...state,
favCounter: state.favCounter - 1
})
window.localStorage.setItem("favCounter", JSON.stringify(state.favCounter - 1))
}
return {
state,
incrementFav,
decrementFav
}
}
export default useInitialState

Related

I need to refresh the page to login | React and Axios

I have a problem when I want to log in to the login by entering the email and password. What happens is that when I enter with the correct email and correct password, the animation appears but it stays cycled, and if I refresh the page and try again, now it lets me enter into the application
Here's my login form code:
import axios from "axios";
import { useRef, useState } from "react";
import { storeToken } from "../utils/authServices";
import { useNavigate } from "react-router-dom";
import { useLoading } from "../context/hooks/useLoading";
import { LoginForm } from "../components";
export const Login = () => {
const API_URL = "https://api.app"; //I hide the API for security reasons
const { run } = useLoading();
const [error, setError] = useState(false);
const [errorMessage, setErrorMessage] = useState("");
const navigate = useNavigate();
const correoRef = useRef("");
const passwordRef = useRef("");
const handleSubmit = async (e) => {
e.preventDefault();
const { value: correo } = correoRef.current;
const { value: password } = passwordRef.current;
await axios
.post(`${API_URL}/api/auth/login/`, {
correo,
password,
})
.then((response) => {
storeToken(response.data.token);
run();
setTimeout(() => {
navigate("/nueva-solicitud");
}, 1000);
})
.catch((err) => {
console.log(err.response.data);
setError(true);
setErrorMessage(err.response.data.msg);
});
};
return (
<LoginForm
correoRef={correoRef}
passwordRef={passwordRef}
handleSubmit={handleSubmit}
error={error}
errorMessage={errorMessage}
/>
);
};
import { createContext, useReducer, useContext } from "react";
const initialState = {
loading: false,
alerts: [],
};
const reducers = (state, action) => {
switch (action.type) {
case "LOADING_RUN":
return {
...state,
loading: true,
};
case "LOADING_STOP":
return {
...state,
loading: false,
};
default:
return { ...state };
}
};
const AppContext = createContext();
const AppContextProvider = (props) => {
const [state, dispatch] = useReducer(reducers, initialState);
return <AppContext.Provider value={{ state, dispatch }} {...props} />;
};
const useAppContext = () => useContext(AppContext);
export { AppContextProvider, useAppContext };
import { useMemo } from "react";
import { useAppContext } from "../AppContext";
export const useLoading = () => {
const { dispatch } = useAppContext();
const loading = useMemo(
() => ({
run: () => dispatch({ type: "LOADING_RUN" }),
stop: () => dispatch({ type: "LOADING_STOP" }),
}),
[dispatch]
);
return loading;
};
import jwt_decode from "jwt-decode";
export const storeToken = (token) => {
localStorage.setItem("token", token);
};
export const getToken = (decode = false) => {
const token = localStorage.getItem("token");
if (decode) {
const decoded = jwt_decode(token);
return decoded;
}
return token;
};
export const logout = () => {
localStorage.removeItem("token");
};
How can I log in without refreshing the page?
There's two problems here. One is you're using await with a .then .catch block. Pick one or the other. You're also never calling the stop() dispatch when your async call is complete which appears to be responsible for removing the loader.
Instead of:
const { run } = useLoading();
Use:
const { run, stop } = useLoading();
Then change this:
setTimeout(() => {
navigate("/nueva-solicitud");
}, 1000);
To this:
setTimeout(() => {
navigate("/nueva-solicitud");
stop();
}, 1000);
Although I would just recommend writing the entire promise like this:
try {
run();
const response = await axios
.post(`${API_URL}/api/auth/login/`, {
correo,
password,
});
storeToken(response.data.token);
navigate("/nueva-solicitud");
stop();
} catch (err) {
stop();
console.log(err.response.data);
setError(true);
setErrorMessage(err.response.data.msg);
}

the useReducer dispatch is not called in a callback

I hope someone can help me with that. I'm experience the following using the React useReducer:
I need to search for items in a list.
I'm setting up a global state with a context:
Context
const defaultContext = [itemsInitialState, (action: ItemsActionTypes) => {}];
const ItemContext = createContext(defaultContext);
const ItemProvider = ({ children }: ItemProviderProps) => {
const [state, dispatch] = useReducer(itemsReducer, itemsInitialState);
const store = useMemo(() => [state, dispatch], [state]);
return <ItemContext.Provider value={store}>{children}</ItemContext.Provider >;
};
export { ItemContext, ItemProvider };
and I created a reducer in a separate file:
Reducer
export const itemsInitialState: ItemsState = {
items: [],
};
export const itemsReducer = (state: ItemsState, action: ItemsActionTypes) => {
const { type, payload } = action;
switch (type) {
case GET_ITEMS:
return {
...state,
items: payload.items,
};
default:
throw new Error(`Unsupported action type: ${type}`);
}
};
I created also a custom hook where I call the useContext() and a local state to get the params from the form:
custom hook
export const useItems = () => {
const context = useContext(ItemContext);
if (!context) {
throw new Error(`useItems must be used within a ItemsProvider`);
}
const [state, dispatch] = context;
const [email, setEmail] = useState<string>('');
const [title, setTitle] = useState<string>('');
const [description, setDescription] = useState<string>('');
const [price, setPrice] = useState<string>('');
const [itemsList, setItemsList] = useState<ItemType[]>([]);
const onChangeEmail = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>): void =>
setEmail(e.currentTarget.value);
const onChangeTitle = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>): void =>
setTitle(e.currentTarget.value);
const onChangePrice = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>): void =>
setPrice(e.currentTarget.value);
const onChangeDescription = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>): void =>
setDescription(e.currentTarget.value);
const handleSearch = useCallback(
async (event: React.SyntheticEvent) => {
event.preventDefault();
const searchParams = { email, title, price, description };
const { items } = await fetchItemsBatch({ searchParams });
if (items) {
setItemsList(items);
if (typeof dispatch === 'function') {
console.log('use effect');
dispatch({ type: GET_ITEMS, payload: { items } });
}
}
},
[email, title, price, description]
);
// useEffect(() => {
// // add a 'type guard' to prevent TS union type error
// if (typeof dispatch === 'function') {
// console.log('use effect');
// dispatch({ type: GET_ITEMS, payload: { items: itemsList } });
// }
// }, [itemsList]);
return {
state,
dispatch,
handleSearch,
onChangeEmail,
onChangeTitle,
onChangePrice,
onChangeDescription,
};
};
this is the index:
function ItemsManagerPageHome() {
const { handleSearch, onChangeEmail, onChangePrice, onChangeTitle, onChangeDescription } = useItems();
return (
<ItemProvider>
<Box>
<SearchComponent
handleSearch={handleSearch}
onChangeEmail={onChangeEmail}
onChangePrice={onChangePrice}
onChangeTitle={onChangeTitle}
onChangeDescription={onChangeDescription}
/>
<ListContainer />
</Box>
</ItemProvider>
);
}
The ListContainer should then do this to get values from the global state:
const { state } = useItems();
The issue is that when I try to dispatch the action after the list items are fetched the reducer is not called, and I cannot figure out why.
I try to put the dispatch in a useEffect() trying to trigger it only when a listItems state changes but I can see it called only at the beginning and not when the callback is fired.
What am I doing wrong?
Thank you for the help
You should use ItemsManagerPageHome component as a descendant component of the ItemProvider component. So that you can useContext(ItemContext) to get the context value from ItemContext.Provider.
Besides, I saw you validate that useItems must be used in ItemsProvider, but the if condition always is false because the defaultContext is an array and it's always a truth value. So, your validation doesn't work. You can use a null value as the default context.
The correct way is:
context.tsx:
import { createContext, useMemo, useReducer } from 'react';
import * as React from 'react';
type ItemProviderProps = any;
type ItemsActionTypes = any;
type ItemsState = any;
export const GET_ITEMS = 'GET_ITEMS';
export const itemsInitialState: ItemsState = {
items: [],
};
export const itemsReducer = (state: ItemsState, action: ItemsActionTypes) => {
const { type, payload } = action;
switch (type) {
case GET_ITEMS:
return {
...state,
items: payload.items,
};
default:
throw new Error(`Unsupported action type: ${type}`);
}
};
const ItemContext = createContext(null);
const ItemProvider = ({ children }: ItemProviderProps) => {
const [state, dispatch] = useReducer(itemsReducer, itemsInitialState);
const store = useMemo(() => [state, dispatch], [state]);
return <ItemContext.Provider value={store}>{children}</ItemContext.Provider>;
};
export { ItemContext, ItemProvider };
hooks.ts:
import { useCallback, useContext, useState } from 'react';
import { GET_ITEMS, ItemContext } from './context';
type ItemType = any;
const fetchItemsBatch = (): Promise<{ items: ItemType[] }> =>
new Promise((resolve) =>
setTimeout(() => resolve({ items: [1, 2, 3] }), 1_000)
);
export const useItems = () => {
const context = useContext(ItemContext);
if (!context) {
throw new Error(`useItems must be used within a ItemsProvider`);
}
const [state, dispatch] = context;
const handleSearch = useCallback(async (event: React.SyntheticEvent) => {
event.preventDefault();
const { items } = await fetchItemsBatch();
if (items) {
if (typeof dispatch === 'function') {
dispatch({ type: GET_ITEMS, payload: { items } });
}
}
}, []);
return {
state,
dispatch,
handleSearch,
};
};
ItemsManagerPageHome.tsx:
import React = require('react');
import { useItems } from './hooks';
export function ItemsManagerPageHome() {
const { handleSearch, state } = useItems();
console.log('state: ', state);
return <input onClick={handleSearch} type="button" value="search" />;
}
App.tsx:
import * as React from 'react';
import { ItemProvider } from './context';
import { ItemsManagerPageHome } from './ItemsManagerPageHome';
import './style.css';
export default function App() {
return (
<div>
<ItemProvider>
<ItemsManagerPageHome />
</ItemProvider>
</div>
);
}
Demo: stackblitz
Click the "search" button and see the logs in the console.

React : Value inside useEffect not defined

So I am building an e-commerce website checkout page with commerce.js. I have a context that allows me to use the cart globally. But on the checkout page when I generate the token inside useEffect , the cart variables have not been set until then.
My context is as below
import { createContext, useEffect, useContext, useReducer } from 'react';
import { commerce } from '../../lib/commerce';
//Provides a context for Cart to be used in every page
const CartStateContext = createContext();
const CartDispatchContext = createContext();
const SET_CART = 'SET_CART';
const initialState = {
id: '',
total_items: 0,
total_unique_items: 0,
subtotal: [],
line_items: [{}],
};
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, initialState);
const setCart = (payload) => dispatch({ type: SET_CART, payload });
useEffect(() => {
getCart();
}, []);
const getCart = async () => {
try {
const cart = await commerce.cart.retrieve();
setCart(cart);
} catch (error) {
console.log('error');
}
};
return (
<CartDispatchContext.Provider value={{ setCart }}>
<CartStateContext.Provider value={state}>
{children}
</CartStateContext.Provider>
</CartDispatchContext.Provider>
);
};
export const useCartState = () => useContext(CartStateContext);
export const useCartDispatch = () => useContext(CartDispatchContext);
Now on my checkout page
const CheckoutPage = () => {
const [open, setOpen] = useState(false);
const [selectedDeliveryMethod, setSelectedDeliveryMethod] = useState(
deliveryMethods[0]
);
const [checkoutToken, setCheckoutToken] = useState(null);
const { line_items, id } = useCartState();
useEffect(() => {
const generateToken = async () => {
try {
const token = await commerce.checkout.generateToken(id, {
type: 'cart',
});
setCheckoutToken(token);
} catch (error) {}
};
console.log(checkoutToken);
console.log(id);
generateToken();
}, []);
return <div> {id} </div>; //keeping it simple just to explain the issue
};
In the above code id is being rendered on the page, but the token is not generated since on page load the id is still blank. console.log(id) gives me blank but {id} gives the actual value of id
Because CheckoutPage is a child of CartProvider, it will be mounted before CartProvider and the useEffect will be called in CheckoutPage first, so the getCart method in CartProvider hasn't been yet called when you try to read the id inside the useEffect of CheckoutPage.
I'd suggest to try to call generateToken each time id changes and check if it's initialised first.
useEffect(() => {
if (!id) return;
const generateToken = async () => {
try{
const token = await commerce.checkout.generateToken(id, {type: 'cart'})
setCheckoutToken(token)
} catch(error){
}
}
console.log(checkoutToken)
console.log(id)
generateToken()
}, [id]);

How to set state to LocalStorage in React

I'm trying to save to localstorage this cart value
const addToCart = (payload) => {
setState({
...state,
cart: [...state.cart, payload ]
})
}
Using this code as initialState
import { useState } from "react"
import { useLocalStorage } from "./useLocalStorage"
const useInitialState = () => {
const [inc, setInc] = useLocalStorage("favCounter", false)
const initialState = {
favCounter: inc,
cart: []
}
const [state, setState] = useState(initialState)
const incrementFav = () => {
setState({
...state,
favCounter: state.favCounter + 1
})
setInc(state.favCounter + 1)
}
const decrementFav = () => {
setState({
...state,
favCounter: state.favCounter - 1
})
setInc(state.favCounter - 1)
}
const addToCart = (payload) => {
setState({
...state,
cart: [...state.cart, payload ]
})
}
return {
state,
incrementFav,
decrementFav,
addToCart
}
}
export default useInitialState
And this code as custom hook "useLocalStorage"
import { useState } from 'react'
export function useLocalStorage (key, initialValue) {
const [storedValue, setValue] = useState(() => {
try {
const item = window.localStorage.getItem(key)
return item !== null ? JSON.parse(item) : initialValue
} catch (e) {
return initialValue
}
})
const setLocalStorage = value => {
try {
window.localStorage.setItem(key, JSON.stringify(value))
setValue(value)
} catch (e) {
console.log(e)
}
}
return [storedValue, setLocalStorage]
}
And I don´t know how to store the cart (array) value to localstorage as I did it with "favcounter" stored value.
const [localCart, setLocalCart] = useLocalStorage("cartState", [])
const addToCart = (payload) => {
setState((prevState) => {
const newState = { ...prevState, cart: [...prevState.cart, payload ]}
setLocalCart(newState.cart)
return newState
})
}

How to detect that the state value of useSelector is changed?

I am using React as the recommended function.
But even if I put the state value received from useSelector into useEffect's dep, useEffect doesn't execute as intended.
When submitLike is executed, the detailPost of the state value is updated, but useEffect is not executed except for the first time.
Can you suggest me a solution ?
Below is my tsx file and reducer
post.tsx(page)
const Post = () => {
const dispatch = useDispatch();
const detailPost = useSelector((store: RootState) => store.post.detailPost);
const [post, setPost] = useState({ ...detailPost });
const [isLiked, setIsLiked] = useState(
{ ...detailPost }.liker?.split(',').filter((v: string) => +v === me.id).length || 0,
);
const submitLike = () => {
if (isLiked) dispatch(UNLIKE_POST_REQUEST({ userId: me.id, postId: detailPost.id }));
else dispatch(LIKE_POST_REQUEST({ userId: me.id, postId: detailPost.id }));
};
useEffect(() => {
loadPostAPI(window.location.href.split('/')[4])
.then((res) => {
setPost(res.data);
const currentLiked = res.data.liker?.split(',').filter((v: string) => +v === me.id).length || 0;
setIsLiked(currentLiked);
return currentLiked;
})
.catch((error) => console.log(error));
}, [detailPost]);
return (
...
post.User.nickname
post.like
...
);
};
export default Post;
post.ts(reducer)
const Post = (state = initialState, action: any) => {
switch (action.type) {
...
case LIKE_POST_REQUEST:
return { ...state, likePostLoading: true, likePostDone: false, likePostError: null };
case LIKE_POST_SUCCESS: {
const posts: any[] = [...state.mainPosts];
const post = posts.find((v) => v.id === action.data.postId);
if (post.liker) post.liker += `,${action.data.userId}`;
else post.liker = `${action.data.userId}`;
post.like += 1;
return { ...state, likePostLoading: false, likePostDone: true, likePostError: null, detailPost: post };
}
case UNLIKE_POST_SUCCESS: {
const posts: any[] = [...state.mainPosts];
const post = posts.find((v) => v.id === action.data.postId);
const liker = post.liker.split(',');
const idx = liker.find((v: string) => +v === action.data.userId);
liker.splice(idx, 1);
post.liker = liker.join('');
post.like -= 1;
return { ...state, unlikePostLoading: false, unlikePostDone: true, unlikePostError: null, detailPost: post };
}
default:
return state;
...
}
};
export default Post;
And when I click refresh, the post values ​​become undefined and an error occurs.
I also want to solve this problem with useEffect.

Resources