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.