the useReducer dispatch is not called in a callback - reactjs

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.

Related

How to add an item to an array within context? (React typescript)

I created a context search in my application, where I have an array called "searchPosts". My goal is to send an object from a component into this array in context and thus be able to use it in other components. I would like to create a global state where my object is stored
context
import { createContext } from "react";
export type SearchContextType = {
searchPost: (text: string) => void;
};
export const SearchContext = createContext<SearchContextType>(null!);
provider
import React, { useState } from "react";
import { SearchContext } from "./SearchContext"
export const SearchProvider = ({ children }: { children: JSX.Element }) => {
const [searchPosts, setSearchPosts] = useState([]);
const searchPost = (text: string) => {
}
return (
<SearchContext.Provider value={{searchPost}}>
{ children }
</SearchContext.Provider>
);
}
I created this search function because in theory it should be a function for me to add the item to the array, but I don't know how I could do that.
This is the state that I have in my component called "searchPosts" that I get the object that I would like to pass to my global array. I want to pass the information from this array in this component to my global array in context
const navigate = useNavigate();
const api = useApi();
const [searchText, setSearchText] = useState('');
const [searchPost, setSearchPost] = useState([]);
const handleSearch = async () => {
const posts = await api.getAllPosts();
const mapPosts = posts.filter(post => post.title.toLowerCase().includes(searchText));
setSearchPost(mapPosts);
}
In the component searchPosts, try to import SearchContext and import searchPost function from context component using useContext hook.
import {SearchContext} from './SearchContext'
const {searchPost} = useContext(SearchContext);;
Now, inside your handleSearch function, pass the mapPosts array to searchPost function that you imported from useContext hook, like this:
const handleSearch = async () => {
const posts = await api.getAllPosts();
const mapPosts = posts.filter(post => post.title.toLowerCase().includes(searchText));
setSearchPost(mapPosts);
searchPost(mapPosts);
}
Now, inside your searchPost function inside your provider component, add following code:
const searchPost = (posts: any[]) => {
setSearchPosts(prev => {
return [...prev, ...posts];
})
}
Add the searchPost[] to SearchContextType
import { createContext } from "react";
export type SearchContextType = {
searchPost: (text: string) => void;
searchResult: string[];
};
export const SearchContext = createContext<SearchContextType>(null!);
Create a reducer to manage your dispatch
//const SEARCH_POST = "SEARCH_POST"; in constants.ts
import { SEARCH_POST } from 'constants';
// reducer
export const reducer = (state, action) => {
switch (action.type) {
case SEARCH_POST: {
return { ...state, searchResult: action.value };
}
default: {
throw new Error(`Unhandled action type: ${action.type}`);
}
}
}
Create the provider
interface InitState {
searchResult: string[];
}
export const SearchProvider = ({ children }: { children: JSX.Element }) => {
const initialState: InitState = {
searchResult: [],
};
const [controller, dispatch] = useReducer(reducer, initialState);
const value = useMemo(() => [controller, dispatch], [controller, dispatch]);
return <SearchContext.Provider value={value}>{children}</SearchContext.Provider>;
};
export const searchPost = (dispatch, value) => dispatch({ type: SEARCH_POST, value });
Now create a custom hook to access the context
export const useSearchState = () => {
const context = useContext(SearchContext);
if (!context) {
throw new Error(
"useSearchState should be used inside the SearchProvider."
);
}
return context;
};
In your component, you can use the above to access the state.
const [controller, dispatch] = useSearchState();
const {searchResult} = controller;
// to update the post you can call searchPost
// import it from search provider
searchPost(dispatch, posts)

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]);

React Native Asyncstorage / useReducer returns null value

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;

react-redux: infinite loop on dispatch

I have a sample application that loads entries from a Spring boot backend. However, my approach leads to an infinite loop that I cannot explain to myself.
api.ts
class CommonApi extends BaseApi {
public loadEntries = () => this.get('http://localhost:8080/radars/development/entries') as Promise<any>;
}
entriesSlice.ts
interface EntriesState {
map: {}
}
const initialState: EntriesState = {
map: {}
};
export const entriesSlice = createSlice({
name: 'entries',
initialState,
reducers: {
getEntries: (state, action: PayloadAction<any>) => {
state.map = action.payload;
},
},
});
export const { getEntries } = entriesSlice.actions;
export const getEntriesAction = (): AppThunk => dispatch => {
return commonApi.loadEntries().then(payload => {
const newPayload: any[] = [];
payload.map((entry: any) => {
return newPayload.push({
label: entry.label,
quadrant: toSegment(entry.category),
ring: toRing(entry.status)
})
})
dispatch(getEntries(newPayload));
}).catch(err => {
console.error('error: ', err)
})
};
export const entriesObject = (state: RootState) => state.entries.map;
export default entriesSlice.reducer;
I think I've found out that this line in entriesSlice.ts causes the error, but I dont know why:
state.map = action.payload;
App.tsx
import { entriesObject, getEntriesAction } from "../../features/entries/entriesSlice";
import { config1Object, getConfig1Action } from "../../features/config1/config1Slice";
function App() {
const config1 = useSelector(config1Object) as any;
const entries = useSelector(entriesObject) as any;
const dispatch = useDispatch();
const [value, setValue] = useState(0);
useEffect(() => {
dispatch(getConfig1Action());
dispatch(getEntriesAction());
}, [config1, entries, dispatch]);
return (
<Container>
<TabPanel value={value} index={0}>
<Chart config={config1} entries={entries} />
</TabPanel>
</Container>
);
}
What am I doing wrong?
You have entries as a dependency to your useEffect - every time getEntriesAction is dispatched it fetches entries and creates a new object in state, which tells react that entries has been updated (it's a new object with a new reference), which reruns the useEffect, which dispatches getEntriesAction again, which... leads to an infinite loop.

React : retrieve info async with useReducer and useContext

I am trying to reproduce something I was doing with Reactjs/ Redux/ redux-thunk:
Show a spinner (during loading time)
Retrieve information from remote server
display information and remove spinner
The approach was to use useReducer and useContext for simulating redux as explained in this tutorial. For the async part, I was relying on redux-thunk, but I don't know if there is any alternative to it for useReducer. Here is my code:
The component itself :
const SearchForm: React.FC<unknown> = () => {
const { dispatch } = React.useContext(context);
// Fetch information when clickin on button
const getAgentsInfo = (event: React.MouseEvent<HTMLElement>) => {
const fetchData:() => Promise<void> = async () => {
fetchAgentsInfoBegin(dispatch); //show the spinner
const users = await fetchAgentsInfo(); // retrieve info
fetchAgentsInfoSuccess(dispatch, users); // show info and remove spinner
};
fetchData();
}
return (
...
)
The data fetcher file :
export const fetchAgentsInfo:any = () => {
const data = await fetch('xxxx');
return await data.json();
};
The Actions files:
export const fetchAgentsInfoBegin = (dispatch:any) => {
return dispatch({ type: 'FETCH_AGENTS_INFO_BEGIN'});
};
export const fetchAgentsInfoSuccess = (dispatch:any, users:any) => {
return dispatch({
type: 'FETCH_AGENTS_INFO_SUCCESS',
payload: users,
});
};
export const fetchAgentsInfoFailure = (dispatch:any) => {
return dispatch({
type: 'FETCH_AGENTS_INFO_FAILURE'
})
};
And my store itself :
import React, { createContext, useReducer } from 'react';
import {
ContextArgs,
ContextState,
ContextAction
} from './types';
// Reducer for updating the store based on the 'action.type'
const Reducer = (state: ContextState, action: ContextAction) => {
switch (action.type) {
case 'FETCH_AGENTS_INFO_BEGIN':
return {
...state,
isLoading:true,
};
case 'FETCH_AGENTS_INFO_SUCCESS':
return {
...state,
isLoading:false,
agentsList: action.payload,
};
case 'FETCH_AGENTS_INFO_FAILURE':
return {
...state,
isLoading:false,
agentsList: [] };
default:
return state;
}
};
const Context = createContext({} as ContextArgs);
// Initial state for the store
const initialState = {
agentsList: [],
selectedAgentId: 0,
isLoading:false,
};
export const ContextProvider: React.FC = ({ children }) => {
const [state, dispatch] = useReducer(Reducer, initialState);
const value = { state, dispatch };
Context.displayName = 'Context';
return (
<Context.Provider value={value}>{children}</Context.Provider>
);
};
export default Context;
I tried to partially reuse logic from this article but the spinner is never displayed (data are properly retrieved and displayed).
Your help will be appreciated !
Thanks
I don't see anything in the code you posted that could cause the problem you describe, maybe do console.log in the reducer to see what happends.
I do have a suggestion to change the code and move logic out of the component and into the action by using a sort of thunk action and replacing magic strings with constants:
//action types
const BEGIN = 'BEGIN',
SUCCESS = 'SUCCESS';
//kind of thunk action (cannot have getState)
const getData = () => (dispatch) => {
dispatch({ type: BEGIN });
setTimeout(() => dispatch({ type: SUCCESS }), 2000);
};
const reducer = (state, { type }) => {
if (type === BEGIN) {
return { ...state, loading: true };
}
if (type === SUCCESS) {
return { ...state, loading: false };
}
return state;
};
const DataContext = React.createContext();
const DataProvider = ({ children }) => {
const [state, dispatch] = React.useReducer(reducer, {
loading: false,
});
//redux-thunk action would receive getState but
// cannot do that because it'll change thunkDispatch
// when state changes and could cause problems when
// used in effects as a dependency
const thunkDispatch = React.useCallback(
(action) =>
typeof action === 'function'
? action(dispatch)
: action,
[]
);
return (
<DataContext.Provider
value={{ state, dispatch: thunkDispatch }}
>
{children}
</DataContext.Provider>
);
};
const App = () => {
const { state, dispatch } = React.useContext(DataContext);
return (
<div>
<button
onClick={() => dispatch(getData())}
disabled={state.loading}
>
get data
</button>
<pre>{JSON.stringify(state, undefined, 2)}</pre>
</div>
);
};
ReactDOM.render(
<DataProvider>
<App />
</DataProvider>,
document.getElementById('root')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>

Resources