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
})
}
Related
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.
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
Anybody has experience in AsyncStorage in React Native? It returns wired values something like this.
"_U": 0,
"_V": 1,
"_X": null,
"_W": {}
And here is Context, useReducer hook code.
const [localState, localDispatch] = useReducer(
local,
localInitialState,
async () => {
await AsyncStorage.removeItem(‘local’);
const storedLocalData = await AsyncStorage.getItem(‘local’);
console.log(‘LOCAL: ’, storedLocalData);
storedLocalData ? console.log(‘LOCAL-YES’) : console.log(‘LOCAL-NO’);
return storedLocalData ? JSON.parse(storedLocalData) : localInitialState;
},
);
const [themeState, themeDispatch] = useReducer(
themeReducer,
themeInitialState,
async () => {
await AsyncStorage.removeItem(‘theme’);
const storedThemeData = await AsyncStorage.getItem(‘theme’);
console.log(‘THEME: ’, storedThemeData);
storedThemeData ? console.log(‘THEME-YES’) : console.log(‘THEME-NO’);
return storedThemeData ? JSON.parse(storedThemeData) : themeInitialState;
},
);
Local state works well but theme sate which copied from local does not work...
And this is Console state.
Local state already stored in Asyncstorage. but Theme state returns null.. 😦
with the same code..
the State should be works like local state. not the theme state.
I hope any advise, Thanks.
Unfortunately there's no possibility for useReducer to have a function that returns a Promise as initializer for now! (which I think it's necessary for the next updates of React)
but here's my solution for now: (written in typescript)
import React from "react";
import { CommonActionTypes } from "context/common/CommonActions";
import useStorage from "./useStorage";
/**
* --- IMPORTANT ----
* if you're using this wrapper, your reducer must handle the ReplaceStateAction
* **Also** your state needs to have a property named `isPersistedDataReady` with `false` as default value
*/
export function usePersistedReducer<State, Action>(
reducer: (state: State, action: Action) => State,
initialState: State,
storageKey: string,
): [State, React.Dispatch<Action>] {
const { value, setValue, isReady } = useStorage<State>(storageKey, initialState);
const reducerLocalStorage = React.useCallback(
(state: State, action: Action): State => {
const newState = reducer(state, action);
setValue(newState);
return newState;
},
[value],
);
const [store, dispatch] = React.useReducer(reducerLocalStorage, value);
React.useEffect(() => {
isReady &&
// #ts-ignore here we need an extension of union type for Action
dispatch({
type: CommonActionTypes.ReplaceState,
state: { ...value, isPersistedDataReady: true },
});
}, [isReady]);
return [store, dispatch];
}
then in your views isPersistedDataReady value.
here's also the implementation of the hook useStorage
import AsyncStorage from "#react-native-async-storage/async-storage";
const useStorage = <T>(key: string, defaultValue: T) => {
type State = { value: T; isReady: boolean };
const [state, setState] = React.useState<State>({
value: defaultValue,
isReady: false,
});
React.useEffect(() => {
get()
.then((value) => {
setState({ value, isReady: true });
})
.catch(() => {
setState({ value: defaultValue, isReady: true });
});
}, []);
React.useEffect(() => {
state.value && state.isReady && save(state.value);
}, [state.value]);
const setValue = (value: T) => {
setState({ value, isReady: true });
};
const save = (value: T): Promise<void> => {
if (value) {
try {
const savingValue = JSON.stringify(value);
return AsyncStorage.setItem(key, savingValue);
} catch (er) {
return Promise.reject(er);
}
} else {
return Promise.reject(Error("No value provided"));
}
};
const get = (): Promise<T> => {
return AsyncStorage.getItem(key, () => defaultValue).then((value) => {
if (value === null) {
throw Error(`no value exsits for ${key} key in the storage`);
}
return JSON.parse(value);
});
};
const remove = (): Promise<void> => {
return AsyncStorage.removeItem(key);
};
return { ...state, setValue, clear: remove };
};
export default useStorage;
I need help. I don't understand why my dispatch action doesn't work. I've redux store currency list and current currency.
My reducer:
export const currencyReducer = (
state: typeState = initialState,
action: TypeActionCurrency
): typeState => {
switch (action.type) {
case types.CURRENCY_FILL_LIST:
return { ...state, list: action.payload }
case types.CURRENCY_SET_CURRENT:
return {
...state,
current:
state.list.find(currency => currency._id === action.payload) ||
({} as ICurrency),
}
default:
return state
}
}
My actions:
export const setCurrencyList = (currencies: ICurrency[]) => ({
type: types.CURRENCY_FILL_LIST,
payload: currencies,
})
export const setCurrentCurrency = (_id: string) => ({
type: types.CURRENCY_SET_CURRENT,
payload: _id,
})
My useEffect:
useEffect(() => {
if (!list.length) {
const fetchCurrencies = async () => {
try {
const data = await $apiClient<ICurrency[]>({ url: '/currencies' })
dispatch(setCurrencyList(data))
if (!current._id) dispatch(setCurrentCurrency(data[0]._id))
} catch (error) {
console.log(error)
}
}
fetchCurrencies()
}
}, [])
I want make request when load page and write currency list to Redux store, if we don't have current currency we write default currency from data.
There is one more strange thing, my redux extension shows that the state has changed, but when I receive it via the log or useSelector, it is empty
enter image description here
Thanks!
I am not 100% sure but it should work.
const [loader, setLoader] = useState(false);
const list = useSelector(state => state.list)
useEffect(() => {
if (!list.length) {
const fetchCurrencies = async () => {
try {
setLoader(true)
const data = await $apiClient<ICurrency[]>({ url: '/currencies' })
dispatch(setCurrencyList(data))
if (!current._id) dispatch(setCurrentCurrency(data[0]._id))
} catch (error) {
console.log(error)
} finally {
setLoader(false)
}
}
fetchCurrencies()
}
}, [])
useEffect(() => {
console.log(list);
}, [loader])
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.