I am really struggling to understand why there are so many use**Async hook libraries in react when hooks cannot be used in event handlers.
If I look at this code:
import { useEffect, useReducer } from 'react';
const initialState = {
started: false,
pending: true,
error: null,
result: null,
start: null,
abort: null,
};
const reducer = (state, action) => {
switch (action.type) {
case 'init':
return initialState;
case 'ready':
return {
...state,
start: action.start,
abort: action.abort,
};
case 'start':
if (state.started) return state; // to bail out just in case
return {
...state,
started: true,
};
case 'result':
if (!state.pending) return state; // to bail out just in case
return {
...state,
pending: false,
result: action.result,
};
case 'error':
if (!state.pending) return state; // to bail out just in case
return {
...state,
pending: false,
error: action.error,
};
default:
throw new Error(`unexpected action type: ${action.type}`);
}
};
export const useAsyncTask = (func) => {
const [state, dispatch] = useReducer(reducer, initialState);
useEffect(() => {
let dispatchSafe = action => dispatch(action);
let abortController = null;
const start = async () => {
if (abortController) return;
abortController = new AbortController();
dispatchSafe({ type: 'start' });
try {
const result = await func(abortController);
dispatchSafe({ type: 'result', result });
} catch (e) {
dispatchSafe({ type: 'error', error: e });
}
};
const abort = () => {
if (abortController) {
abortController.abort();
}
};
dispatch({ type: 'ready', start, abort });
const cleanup = () => {
dispatchSafe = () => null; // avoid to dispatch after stopped
dispatch({ type: 'init' });
};
return cleanup;
}, [func]);
return state;
};
I really like that it has all the loading and error states returned but I cannot use this in an event handler.
Are there any options to use this in an event handler or how else can I get my loading and error states generically?
The README to the package I got the code from gives this example:
import React, { useState, useCallback } from 'react';
import {
useAsyncCombineSeq,
useAsyncRun,
useAsyncTaskDelay,
useAsyncTaskFetch,
} from 'react-hooks-async';
const Err = ({ error }) => <div>Error: {error.name} {error.message}</div>;
const Loading = ({ abort }) => <div>Loading...<button onClick={abort}>Abort</button></div>;
const GitHubSearch = ({ query }) => {
const url = `https://api.github.com/search/repositories?q=${query}`;
const delayTask = useAsyncTaskDelay(useCallback(() => 500, [query]));
const fetchTask = useAsyncTaskFetch(url);
const combinedTask = useAsyncCombineSeq(delayTask, fetchTask);
useAsyncRun(combinedTask);
if (delayTask.pending) return <div>Waiting...</div>;
if (fetchTask.error) return <Err error={fetchTask.error} />;
if (fetchTask.pending) return <Loading abort={fetchTask.abort} />;
if (!fetchTask.result) return <div>No result</div>;
return (
<ul>
{fetchTask.result.items.map(({ id, name, html_url }) => (
<li key={id}><a target="_blank" href={html_url}>{name}</a></li>
))}
</ul>
);
};
const App = () => {
const [query, setQuery] = useState('');
return (
<div>
Query:
<input value={query} onChange={e => setQuery(e.target.value)} />
{query && <GitHubSearch query={query} />}
</div>
);
};
This seems very inefficient, is there a better way?
Related
I am pretty new to react and redux. I am trying to call get method API in useEffect hook but the function I am calling isn't getting invoked at all. the same function is invoked when I tried calling outside the useEffect and the state is also updated but I still got that error Cannot read property 'map' of null. so in both the cases, the common thing is getting the error and my code seems good to me. all other functions and states are working except this and I am not able to figure out what I am missing. any help very much is appreciated. thank you.
Orders.js
const orders = (props) => {
// getting the orders
useEffect(() => {
props.fetchOrders();
}, []);
let orders;
if(props.loading || props.error) {
orders = <Loading />
}
console.log(props)
orders = props.orders.map((order) => <Order
key={order.id}
ingredients={order.ingredients}
price={order.price}
customer={order.customerDetails}
/>);
return(
<div>
{orders}
</div>
)
}
const mapStateToProps = state => {
return {
orders: state.orders,
error: state.error,
loading: state.loading
}
}
const mapDispatchToProps = dispatch => {
return {
fetchOrders: () => dispatch(getOrders())
}
}
export default connect(mapStateToProps, mapDispatchToProps) (errorHandler(orders, axiosInstance));
action.js
// not invoking
const getOrders = () => {
return dispatch => {
axiosInstance.get('/orders.json').then(res => {
const ordersData = transformData(res);
dispatch(fetchOrders(ordersData));
}).catch(err => {
console.log(err)
})
}
}
const transformData = (response) => {
// simplified logic
const ordersData = [];
if(response.data) {
for (let key in response.data) {
ordersData.unshift({
...response.data[key],
id: key
})
}
}
return ordersData;
}
const fetchOrders = (ordersData) => {
return {
type: actionTypes.GET_ORDERS,
orders: ordersData
}
}
export { getOrders }
reducer.js
const reducer = (state = intialState, action) => {
switch (action.type) {
.
.
case actionType.GET_ORDERS:
return setOrders(state, action);
.
.
default:
return state;
}
}
const setOrders = (state, action) => {
return {
...state,
orders: action.orders.concat(),
error: false,
loading: false
}
}
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.
In the categories component, I render a random image from each category. I also added a onClick event to each image. When the image is clicked, it will dispatch the action getCategory(target.alt) and the DOM will render the products from the clicked category. The problem I got is that every time I clicked a random category image, the DOM will re-render and new random images will appear on the DOM. How do I prevent this re-render? Below is my codes.
const Categories = ({selectedCategory}) => {
const isLoading = useSelector(state => state.productsReducer.isLoading);
const productsByCategory = useSelector(state =>
state.productsReducer.productsByCategories);
const getRandomProductsByCategory = () => {
const randomProducts = []
for(let categories in productsByCategory) {
const randomCategory = productsByCategory[categories][getRandomIndex(productsByCategory[categories].length)];
productsByCategory[categories].map(category => {
if(category === randomCategory) {
randomProducts.push(category)
}
})
}
return randomProducts;
}
return (
<div class='categories-container'>
{getRandomProductsByCategory().map(randomProduct => (
<img onClick={selectedCategory} src={randomProduct.image} />}
</div>
)
}
function App() {
const dispatch = useDispatch();
const category = useSelector(state => state.productsReducer.category)
useEffect(() => {
dispatch(getProducts())
}, [dispatch])
const handleCategoryClick = ({target}) => {
return dispatch(getCategory(target.alt))
}
return (
<>
{/* <ProductsList /> */}
<Categories selectedCategory={handleCategoryClick} />
{category.map(product => <img src={product.image} />)}
</>
)
}
const populateProductsStarted = () => ({
type: 'POPULATE_PRODUCTS/fetchStarted'
})
const populateProductsSuccess = products => ({
type: 'POPULATE_PRODUCTS/fetchSuccess',
payload: products
})
const populateProductsFailed = error => ({
type: 'POPULATE_PRODUCTS/fetchFailed',
error
})
export const getCategory = (category) => ({
type: 'GET_CATEGORY',
category
})
const getProducts = () => async dispatch => {
dispatch(populateProductsStarted())
try {
const response = await fetch(url)
if(response.ok) {
let jsonResponse = await response.json();
return dispatch(populateProductsSuccess(jsonResponse))
}
} catch (err) {
dispatch(populateProductsFailed(err.toString()))
}
}
const initialState = {
isLoading: false,
isError: null,
allProducts: [],
productsByCategories: {},
category: []
}
const productsReducer = (state=initialState, action) => {
switch(action.type) {
case 'POPULATE_PRODUCTS/fetchStarted':
return {
...state,
isLoading: true
}
case 'POPULATE_PRODUCTS/fetchSuccess':
return {
...state,
isLoading: false,
allProducts: action.payload,
productsByCategories: action.payload.reduce((accumulatedProduct, currentProduct) => {
accumulatedProduct[currentProduct.category] = accumulatedProduct[currentProduct.category] || [];
accumulatedProduct[currentProduct.category].push(currentProduct);
return accumulatedProduct;
}, {})
}
case 'POPULATE_PRODUCTS/fetchFailed':
return {
...state,
isError: action.error
}
case 'GET_CATEGORY':
return {
...state,
category: state.allProducts.filter(product => product.category === action.category)
}
default:
return state
}
}
One way to achieve this is through memoization provided by React's useMemo.
const images = React.useMemo(getRandomProductsByCategory().map(randomProduct => (
<img onClick={selectedCategory} src={randomProduct.image} />, [productsByCategory])
return (
<div class='categories-container'>
{images}
</div>
)
This will keep the srcs consistent across re-renders.
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>
As long as I know, child component is being re-rendered when the parent component's state or props change.
But I have no idea with the case of vice-versa.
Here is a code.
usePromise.js (custom made hooks)
import { useEffect, useReducer } from 'react';
const reducer = (state, action) => {
switch (action.type) {
case 'RESOLVED':
return { ...state, resolved: action.diff };
case 'LOADING':
return { ...state, loading: action.diff };
case 'ERROR':
return { ...state, resolved: action.diff };
default:
return state;
}
};
export default function usePromise(promiseCreator, deps = []) {
const [state, dispatch] = useReducer(reducer, {
resolved: null,
loading: false,
error: null
});
const process = async () => {
dispatch({ type: 'LOADING', diff: true });
try {
const result = await promiseCreator();
dispatch({ type: 'RESOLVED', diff: result });
} catch (e) {
dispatch({ type: 'ERROR', diff: e });
}
dispatch({ type: 'LOADING', diff: false });
};
useEffect(() => {
process();
}, deps);
return state;
}
usePromiseSample.js
import React from 'react';
import usePromise from './usePromise';
const wait = () => {
return new Promise(resolve =>
setTimeout(() => resolve('Hello hooks!'), 3000)
);
};
const UsePromiseSample = () => {
const { resolved, loading, error } = usePromise(wait);
console.log('test')
if (loading) return <div>loading...</div>;
if (error) return <div>error happened!</div>;
if (!resolved) return null;
return <div>{resolved}</div>;
};
export default UsePromiseSample;
As you can see above the code, child(usePromise.js) component's state is changing four times.
But it seems that parent(usePromiseSample.js) is also being re-rendered four times since test is logged four times.
How can I understand this situation easily?
usePromise is not a child component, but a custom hook. The hook itself it not being re-rendered when an action is dispatched inside usePromise, but the component that uses it is.
If you render UsePromiseSample inside another component, you will see that the parent is not re-rendering when UsePromiseSample is.
const { useEffect, useReducer } = React;
const reducer = (state, action) => {
switch (action.type) {
case 'RESOLVED':
return { ...state, resolved: action.diff, loading: false };
case 'ERROR':
return { ...state, resolved: action.diff, loading: false };
default:
return state;
}
};
function usePromise(promiseCreator, deps = []) {
const [state, dispatch] = useReducer(reducer, {
resolved: null,
loading: true,
error: null
});
const process = () => {
promiseCreator()
.then(result => {
dispatch({ type: 'RESOLVED', diff: result });
})
.catch(e => {
dispatch({ type: 'ERROR', diff: e });
});
};
useEffect(() => {
process();
}, deps);
return state;
}
const wait = () => {
return new Promise(resolve =>
setTimeout(() => resolve('Hello hooks!'), 3000)
);
};
const UsePromiseSample = () => {
const { resolved, loading, error } = usePromise(wait);
console.log('UsePromiseSample rendered')
if (loading) return <div>loading...</div>;
if (error) return <div>error happened!</div>;
if (!resolved) return null;
return <div>{resolved}</div>;
};
const App = () => {
console.log('App rendered')
return <UsePromiseSample />
}
ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://unpkg.com/react#16/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom#16/umd/react-dom.development.js"></script>
<div id="root"></div>