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.
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'm currently working on an app that fetches data from an API and displays it on the front-end. I've got the data coming up correctly when I print it to the console (so I know fetching the data isn't an issue) and I want to be able to use this data globally across different components. I'm aiming to use createContext to do this but haven't figured it out yet. Here's my code so far:
useFetch.ts
export const useFetch = (url: string) => {
const [fetchedData, setFetchedData] = useState(null);
const [loaded, setLoaded] = useState(false);
useEffect(() => {
getAllFetchedData()
}, []);
const getAllFetchedData = async () => {
await axios.get(url).then((response) => {
const data = response.data;
setFetchedData(data)
setLoaded(true)
})
.catch(error => console.error(`Error: ${error}`));
}
return [loaded, fetchedData];
}
DataProvider.tsx
type PostProviderProps = {
children: React.ReactNode
}
export default function DataProvider({ children }: PostProviderProps) {
const [loaded, data] = useFetch(`https://jsonplaceholder.typicode.com/posts`)
return (
<PostContext.Provider value = {[loaded, data]}>
{children}
</PostContext.Provider>
)
}
DataContext.ts
type PostContextType = {
userId: number;
id: number;
title: string;
body: string;
}
const PostContext = createContext<PostContextType>({} as PostContextType)
export default PostContext
If anybody could help it'd be really appreciated 🙂
The PostContext is typed to be an object with userId, id, title, and body properties, but the value you are trying to provide is an array of the loaded and data values returned from the useFetch hook. It appears that you are fetching an array of posts.
I suggest the following updates to the code.
First, update the useFetch hook to take a generic type for the fetched return value.
const useFetch = <T extends unknown>(url: string): [boolean, T | null] => {
const [fetchedData, setFetchedData] = useState<T | null>(null);
const [loaded, setLoaded] = useState(false);
useEffect(() => {
const getAllFetchedData = async () => {
await axios
.get(url)
.then((response) => {
const data = response.data;
setFetchedData(data);
setLoaded(true);
})
.catch((error) => console.error(`Error: ${error}`));
};
getAllFetchedData();
}, []);
return [loaded, fetchedData];
};
Update the context to use a better interface that aligns with (1) what you are fetching and (2) what you want to provide to the app as a context value.
interface Post {
userId: number;
id: number;
title: string;
body: string;
}
interface PostContextType {
loaded: boolean;
posts: Post[] | null;
}
const PostContext = createContext<PostContextType>({
loaded: false,
posts: []
});
Create the DataProvider component and a custom hook to access the context.
const usePosts = () => React.useContext(PostContext);
function DataProvider({ children }: React.PropsWithChildren<{}>) {
const [loaded, data] = useFetch<Post[]>(
`https://jsonplaceholder.typicode.com/posts`
);
const value = {
loaded,
posts: data
};
return (
<PostContext.Provider value={value}>
{children}
</PostContext.Provider>
);
}
Wrap the app code with the provider.
function App() {
return (
<DataProvider>
....
</DataProvider>
);
}
Consume the context in a descendent component.
const MyComponent = () => {
const { loaded, posts } = usePosts();
return loaded ? (
posts?.map((post) => (
<div key={post.id}>
<h2>{post.title}</h2>
<h3>{post.userId}</h3>
<p>{post.body}</p>
</div>
))
) : (
<>"Loading"</>
);
};
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]);
I dont understant what is wrong with this code, im passing in ID and filtering out state, but it just wont remove, cant figure it out, would love some help.
slice:
import { createSlice } from "#reduxjs/toolkit";
const movieSlice = createSlice({
name: "movie",
initialState: { favoriteMovies: [] },
reducers: {
addMovie: (state, action) => {
state.favoriteMovies = [...state.favoriteMovies, action.payload];
},
removeMovie: (state, action) => {
state.favoriteMovies = state.favoriteMovies.filter(
(movie) => movie.imdbID !== action.payload
);
},
},
});
export const { addMovie, removeMovie } = movieSlice.actions;
export const selectMovie = (state) => state.movie.favoriteMovies;
export default movieSlice.reducer;
dispatching:
const MovieDetail = ({ movie }) => {
const [isFavorite, setIsFavorite] = useState(false);
const dispatch = useDispatch();
const imdbID = movie.imdbID;
const handleAddFavorite = () => {
dispatch(addMovie({ movie }));
setIsFavorite(true);
};
const handleRemoveFavorite = () => {
dispatch(removeMovie({ imdbID }));
console.log(imdbID);
setIsFavorite(false);
};
it does nothing when should remove, and then add it again. The ids i pass in are correct.
The way you are dispatching with
dispatch(removeMovie({ imdbID }));
imdbID will end up as action.payload.imdbID.
So you need to either access that, or dispatch like
dispatch(removeMovie(imdbID));
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>