React : retrieve info async with useReducer and useContext - reactjs

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>

Related

Infinity loop when using custom hook and reducer

I have infinity loop when using custom hook and reducer in React Hook. Custom hook has been infinity loop when moving dispatch setLoading into async function. But when I calling dispatch action out of async function scope, I have related issue with dispatch action. setLoading(false) will called before async function has been completed. Please help me clarify it
That is my code
reducer:
import { SET_ERROR, SET_LOADING } from "./common.type";
export const commonInitial = {
isLoading: false,
isError: {
message: "",
status: "",
},
};
const commonReducer = (state: any, action: any) => {
switch (action.type) {
case SET_LOADING:
return { ...state, isLoading: action.payload };
case SET_ERROR:
return { ...state, isError: action.payload };
default:
return { ...state };
}
};
export default commonReducer;
useFetch (custom hook)
export const useFetch = (url: string) => {
const [response, setResponse] = useState([]);
const [error, setErr] = useState({ message: "", status: "" });
//fetching state handler
const { dispatch } = useContext(CommonContext);
useEffect(() => {
const fetchDataFromUrl = async () => {
//show loading
dispatch(setLoading(true));
try {
const { data } = await axios.get(url);
setResponse(data);
dispatch(setLoading(false));
} catch (err: any) {
//handle error
dispatch(setLoading(false));
setErr(err);
}
};
fetchDataFromUrl();
}, [url]);
useEffect(() => {
dispatch(setError({ message: error.message }));
}, [error]);
return [response];
};
hook called
// using custom hook fetch data
const [data] = useFetch("https://jsonplaceholder.typicode.com/posts");
return (
<div>
<ul>
{!!data?.length &&
data.map((item: Post) => {
return (
<div key={item.id}>
<Link to={`/post/${item.id}`}>Moves to Post {item.id}</Link>
</div>
);
})}
</ul>
</div>
);
};
export default DemoHookA;
I am not sure, but the issue could be because of useEffect hook. As per React 18 in invoked multiple types. For getting rid of this rendering issue, you can create a ref that would check if the component is mounted or not and updating the ref once it is mounted.
You could achive this by following way.
import React, {
useEffect,
useRef
} from 'react';
function YourComponent() {
const isRendered = useRef(false);
// UseEffect
useEffect(() => {
if (!isRendered.current) {
// Your code goes here...
}
return () => {
// Update isRendered
isRendered.current = true;
}
})
}
export default YourComponent;
Put all the code that you have inside useEffect inside the if condition.
Hope it will work for you.

Using a callback with useReducer / Redux reducers

I've been looking for a solution to "promisify" useReducer do something once I'm sure that the state has been changed as per my dispatched action. I found some promising stuff, such as this feature request and a few solutions similar to this one that's based on combining useReducer with useEffect. So instead of using a promise, I've tried to use a callback instead and I want to note here that this implementation works. But I'm unsure if there are any drawbacks to this.
**Note, the use case here isn't to call a function per every time the state changes, but rather the option to do something when the reducer finishes processing an action.
As per Redux rules, my reducer does not mutate state.
const emptyState: IState = {
str: '',
obj: {
propA: 0,
propB: 0,
}
}
interface ReducerActions {
type: 'changeStr' | 'changeObj';
callback?: (newState: IState) => any;
}
const reducer = (state: IState, action: ReducerActions): IState => {
let newState = {...state};
switch(action.type) {
case 'changeStr':
newState.str = action.newStr;
break;
case 'changeObj':
newState.obj = action.newObj;
break;
if (action.callback) {
action.callback(newState);
}
return newState;
}
I did notice that this works in reverse of the traditional flow, where the callback or promise is executed after the state has changed, but should it matter when the callback is called using the value of the new state anyways?
And, are there any drawbacks or side-effects of using this method (whether here or in a Redux implementation)?
the action changeStr replaces the string of state.str with a new string, using 2 buttons, one makes it longer, the other shorter. If I use useEffect, I can of course check the new value of the string and get the length, but I would not be able to get the length of the previous string without storing the previous value. If I pass a callback to the action implemented in the buttons, I know which button makes it longer and which one makes it shorter.
Still not sure what your needs are but if you need the previous and current value to perform some logic you can write a custom hook:
const { useReducer, useState, useRef } = React;
const init = { value: 'A' };
const TOGGLE = 'TOGGLE';
const toggle = () => ({ type: TOGGLE });
const reducer = (state, { type }) => {
//toggle state.value between A and B
if (type === TOGGLE) {
return { value: state.value === 'A' ? 'B' : 'A' };
}
return state;
};
const selectValue = (state) => state.value;
const NONE = {};
//custom hook to detect changes between renders
const useChange = (value, callback) => {
const ref = useRef(NONE);
if (ref.current !== value) {
if (ref.current !== NONE) {
callback(ref.current, value);
}
ref.current = value;
}
};
const App = () => {
const [state, dispatch] = useReducer(reducer, init);
const [message, setMessage] = useState('');
const value = selectValue(state);
useChange(value, (pref, current) =>
setMessage(`value changed from ${pref} to ${current}`)
);
return (
<div>
<button onClick={() => dispatch(toggle())}>
toggle
</button>
<div>{value}</div>
<div>{message}</div>
</div>
);
};
ReactDOM.render(<App />, 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>
UPDATE
Example of using thunk with useReducer:
const { useReducer, useRef } = React;
const init = { value: 'A' };
const TOGGLE = 'TOGGLE';
const thunkToggle = () => (dispatch, getState) => {
const value = getState().value;
//you can do async dispatch
setTimeout(() => {
dispatch({ type: TOGGLE });
console.log(
`action dispatched value was ${value} is now ${
getState().value
}`
);
}, 10);
console.log(`value is now (nothing dispatched) ${value}`);
};
const reducer = (state, { type }) => {
console.log(`in reducer action type: ${type}`);
//toggle state.value between A and B
if (type === TOGGLE) {
return { value: state.value === 'A' ? 'B' : 'A' };
}
return state;
};
const SET_STATE = Date.now();
//custom hook to detect changes between renders
const useThunkReducer = (reducer, initialState) => {
const state = useRef(initialState);
const thunkReducer = (state, action) => {
if (action.type === SET_STATE) {
return action.payload;
}
return reducer(state, action);
};
const [rState, dispatch] = useReducer(thunkReducer, init);
const thunkDispatch = (action) => {
if (typeof action === 'function') {
return action(thunkDispatch, () => state.current);
}
state.current = thunkReducer(state.current, action);
dispatch({ type: SET_STATE, payload: state.current });
};
return [rState, thunkDispatch];
};
const App = () => {
const [state, dispatch] = useThunkReducer(reducer, init);
return (
<div>
<button onClick={() => dispatch(thunkToggle())}>
toggle
</button>
<pre>{JSON.stringify(state, undefined, 2)}</pre>
</div>
);
};
ReactDOM.render(<App />, 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>
For completion; here is an example using middleware so you can add several middleware functions and not only thunk:
const { useRef, useState } = React;
const compose = (...fns) =>
fns.reduce((result, fn) => (...args) =>
fn(result(...args))
);
const mw = () => (next) => (action) => next(action);
const createMiddleware = (...middlewareFunctions) => (
store
) =>
compose(
...middlewareFunctions
.concat(mw)
.reverse()
.map((fn) => fn(store))
);
const useMiddlewareReducer = (
reducer,
initialState,
middleware = () => (b) => (c) => b(c)
) => {
const stateContainer = useRef(initialState);
const [state, setState] = useState(initialState);
const dispatch = (action) => {
const next = (action) => {
stateContainer.current = reducer(
stateContainer.current,
action
);
return setState(stateContainer.current);
};
const store = {
dispatch,
getState: () => stateContainer.current,
};
return middleware(store)(next)(action);
};
return [state, dispatch];
};
//middleware
const thunkMiddleWare = ({ getState, dispatch }) => (
next
) => (action) =>
typeof action === 'function'
? action(dispatch, getState)
: next(action);
const logMiddleware = ({ getState }) => (next) => (
action
) => {
console.log('in log middleware', action, getState());
Promise.resolve().then(() =>
console.log('after action:', action.type, getState())
);
return next(action);
};
const init = { value: 'A' };
const TOGGLE = 'TOGGLE';
const thunkToggle = () => (dispatch) => {
setTimeout(() => {
dispatch({ type: TOGGLE });
}, 500);
};
const reducer = (state, { type }) => {
console.log(`in reducer action type: ${type}`);
//toggle state.value between A and B
if (type === TOGGLE) {
return { value: state.value === 'A' ? 'B' : 'A' };
}
return state;
};
const middleware = createMiddleware(
thunkMiddleWare,
logMiddleware
);
const App = () => {
const [state, dispatch] = useMiddlewareReducer(
reducer,
init,
middleware
);
return (
<div>
<button onClick={() => dispatch(thunkToggle())}>
toggle
</button>
<pre>{JSON.stringify(state, undefined, 2)}</pre>
</div>
);
};
ReactDOM.render(<App />, 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>

react-redux-spinner won't render

I was searching for a loading spinner for react+redux and came across the react-redux-spinner library. I included it in my project, added the reducer, called [pendingTask]: begin/end in my actions, added the Spinner component to render, but it just won't show at all, even though in the redux logs I can see that pending tasks in the store are incremented and decremented accordingly to the action called. Here is some of my code:
store:
const rootReducer = combineReducers({
pendingTasks: pendingTasksReducer
// other reducers
});
const store = createStore(rootReducer, /* middlewares */);
export default store;
actions
export const fetchData = params => {
const request = params => ({
type: 'FETCH_REQUEST',
[pendingTask]: begin,
payload: { params }
});
const success = data => ({
type: 'FETCH_SUCCESS',
[pendingTask]: end,
payload: { data }
});
const failure = error => ({
type: 'FETCH_FAILURE',
[pendingTask]: end,
payload: { error }
});
return async dispatch => {
dispatch(request(params));
try {
const res = await service.fetchData(params);
dispatch(success(res.data));
return res.data;
} catch (e) {
const msg = e.toString();
dispatch(failure(msg));
return Promise.reject(msg);
}
}
}
page
const Page = props => {
const { data } = props;
useEffect(() => {
async function fetchData(params) {
try {
await props.fetchData(params);
} catch (e) {
console.log(e);
}
}
fetchData(data.params);
}
return (
<div className="wrapper">
{
data.map(({ field1, field2 }, key) => ({
<div>{field1}: {field2}</div>
}));
}
</div>
);
};
const mapStateToProps = state => {
const { data } = state;
return { data };
};
const actionCreators = {
fetchData: actions.fetchData
};
export default connect(mapStateToProps, actionCreators)(Page);
app component
export const App = props => {
return (
<main className="App">
<Spinner config={{ trickeRate: 0.02 }} />
<Page/>
</main>
);
}
I've double-checked that I use the correct names for the store and for the actions, and they do fire up - but the spinner itself never gets rendered on the page at all, even though with each action the pendingTasks value change. What could I possibly do wrong or miss here? Infinitely grateful in advance for pointing out!

React-Redux Functional Component Multiple Renders

I created a very simple React-Redux App and fetching Users and Posts from https://jsonplaceholder.typicode.com/
In my components I am logging Users and Posts data into the console. As far as I see, in the network tab there is one request for Users and 10 requests for Posts. That's correct but in the console, I see 10 Posts requests for each User.
Does it mean ReactJS renders the component 100 times? What is my mistake in this code?
Any help will be greatly appreciated!
My code and codepen link are below
Please check the code in codepen
const { useEffect } = React;
const { connect, Provider } = ReactRedux;
const { createStore, applyMiddleware, combineReducers } = Redux;
const thunk = ReduxThunk.default;
//-- REDUCERS START -- //
const userReducer = (state = [], action) => {
if (action.type === 'fetch_users') return [...action.payload];
return state;
};
const postReducer = (state = [], action) => {
if (action.type === 'fetch_posts') return [...action.payload];
return state;
};
//-- REDUCERS END -- //
//-- ACTIONS START -- //
const fetchUsers = () => async dispatch => {
const response = await axios.get(
'https://jsonplaceholder.typicode.com/users'
);
dispatch({ type: 'fetch_users', payload: response.data });
};
const fetchPosts = userId => async dispatch => {
const response = await axios.get(
`https://jsonplaceholder.typicode.com/users/${userId}/posts`
);
dispatch({ type: 'fetch_posts', payload: response.data });
};
//-- ACTIONS END -- //
const reducer = combineReducers({ users: userReducer, posts: postReducer });
const store = createStore(reducer, applyMiddleware(thunk));
const mapStateToProps = state => {
return { users: state.users, posts: state.posts };
};
const mapDispatchToProps = dispatch => {
return {
getUsers: () => dispatch(fetchUsers()),
getPosts: (id) => dispatch(fetchPosts(id))
};
};
const Users = props => {
console.log('users', props.users);
const { getUsers } = props;
useEffect(() => {
getUsers();
}, [getUsers]);
const renderUsers = () =>
props.users.map(user => {
return (
<div>
<div>{user.name}</div>
<div>
<PostsContainer userId={user.id} />
</div>
</div>
);
});
return <div style={{backgroundColor:'green'}}>{renderUsers()}</div>;
};
const UserContainer = connect(mapStateToProps, mapDispatchToProps)(Users);
const Posts = props => {
console.log('posts' , props.posts);
const { getPosts, userId } = props;
useEffect(() => {
getPosts(userId);
}, [getPosts, userId]);
const renderPosts = () =>
props.posts.map(post => {
return (
<div>
<div>{post.title}</div>
</div>
);
});
return <div style={{backgroundColor:'yellow'}}>{renderPosts()}</div>;
};
const PostsContainer = connect(mapStateToProps, mapDispatchToProps)(Posts);
const App = props => {
return (
<div>
<UserContainer />
</div>
);
};
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
Does it mean ReactJS renders the component 100 times? What is my mistake in this code?
you have a UserContainer, that renders and requests for users;
once fetched users, you have an update state. UserContainer rerenders, and now you have 10 PostContainers;
each PostContainer makes a request to fetch posts, 10 on total;
it results in 10 state updates. UserContainer rerenders 10 times, and each PostContainer rerenders 10 times;
The component doesn't renders 100 times, each PostContainer renders the initial mount then rerenders 10 times. since there are 10 PostContainers and each rerenders 10 times that's why you might think that renders 100 times.
you have some issues. the dependency issue, which was pointed out is the first. getUsers useEffect should have an empty dependency, and userId useEffect, should depend on userId.
to solve the 10 rerenders on UserContainer due to posts, you need to have a different mapStateToProps to each. for UserContainer you will map only users, otherwise you will get 10 updates due to 10 posts requests:
const mapUserStateToProps = state => {
return { users: state.users };
};
with that it solves UserContainer 10 rerenders.
now about PostContainer there is something that needs to be fixed first, your reducer. your reducer replaces last posts with the current call. in the end you will have only the posts that arrived last, not all posts. to fix that you need to spread your state.
const postReducer = (state = [], action) => {
if (action.type === 'fetch_posts') return [...state, ...action.payload];
return state;
};
eventually, if in your project you could have a repeated request to same userId than it would be necessary to have some validation for not adding the same posts again
now it leads us to mapping props to PostContainer. you would need to have a filter on posts based on userId. mapStateToProps takes props as second argument, which enables us to accomplish that:
const mapPostStateToProps = (state, { userId }) => {
return { posts: state.posts.filter(post => post.userId === userId) };
};
this looks the end to solve the issue, but each PostContainer still rerenders 10 times. why does this happens since posts will be the same? that happens because filter will return a new array reference, no matter if its content didn't change.
to solve this issue you can use React.memo. you need to provide the component and a equality function to memo. to compare an array of objects there are some solutions, also few libs that provide some deepEqual function. here I use JSON.stringify to compare, but you are free to use some other one:
const areEqual = (prevProps, nextProps) => {
return JSON.stringify(prevProps.posts) === JSON.stringify(nextProps.posts)
}
you would validate also other props that could change but that's not the case
now apply React.memo to posts:
const PostsContainer = connect(mapPostStateToProps, mapDispatchToProps)(React.memo(Posts, areEqual));
After all that applied, UserContainer will rerender one once, and each PostContainer will rerender only once as well.
here follows link with working solution:
https://codepen.io/rbuzatto/pen/BaLYmNK?editors=0010
final code:
const { useEffect } = React;
const { connect, Provider } = ReactRedux;
const { createStore, applyMiddleware, combineReducers } = Redux;
const thunk = ReduxThunk.default;
//-- REDUCERS START -- //
const userReducer = (state = [], action) => {
if (action.type === 'fetch_users') return [...action.payload];
return state;
};
const postReducer = (state = [], action) => {
if (action.type === 'fetch_posts') return [...state, ...action.payload];
return state;
};
//-- REDUCERS END -- //
//-- ACTIONS START -- //
const fetchUsers = () => async dispatch => {
const response = await axios.get(
'https://jsonplaceholder.typicode.com/users'
);
dispatch({ type: 'fetch_users', payload: response.data });
};
const fetchPosts = userId => async dispatch => {
const response = await axios.get(
`https://jsonplaceholder.typicode.com/users/${userId}/posts`
);
dispatch({ type: 'fetch_posts', payload: response.data });
};
//-- ACTIONS END -- //
const reducer = combineReducers({ users: userReducer, posts: postReducer });
const store = createStore(reducer, applyMiddleware(thunk));
const mapUserStateToProps = state => {
return { users: state.users };
};
const mapPostStateToProps = (state, { userId }) => {
return { posts: state.posts.filter(post => post.userId === userId) };
};
const mapDispatchToProps = dispatch => {
return {
getUsers: () => dispatch(fetchUsers()),
getPosts: (id) => dispatch(fetchPosts(id))
};
};
const Users = props => {
console.log('users', props.users);
const { getUsers } = props;
useEffect(() => {
getUsers();
}, []);
const renderUsers = () =>
props.users.map(user => {
return (
<div key={user.id}>
<div>{user.name}</div>
<div>
<PostsContainer userId={user.id} />
</div>
</div>
);
});
return <div style={{backgroundColor:'green'}}>{renderUsers()}</div>;
};
const UserContainer = connect(mapUserStateToProps, mapDispatchToProps)(Users);
const Posts = props => {
console.log('posts');
const { getPosts, userId } = props;
useEffect(() => {
getPosts(userId);
}, [userId]);
const renderPosts = () =>
props.posts.map(post => {
return (
<div>
<div>{post.title}</div>
</div>
);
});
return <div style={{backgroundColor:'yellow'}}>{renderPosts()}</div>;
};
const areEqual = (prevProps, nextProps) => {
return JSON.stringify(prevProps.posts) === JSON.stringify(nextProps.posts)
}
const PostsContainer = connect(mapPostStateToProps, mapDispatchToProps)(React.memo(Posts, areEqual));
const App = props => {
return (
<div>
<UserContainer />
</div>
);
};
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
useEffect() renders the component every time something is changed in the dependencies you provided.
Ideally, you should change your components to re-render only when something changes in props. getUser and getPost change on each render. So, it is better to change it to monitor users and posts from state.
In Users:
const { users, getUsers } = props;
useEffect(() => {
getUsers();
}, []); -- Leaving this empty makes it load only on mount.
In Posts:
const { getPosts, userId } = props;
useEffect(() => {
getPosts(userId);
}, [userId]);

React useReducer async data fetch

I'am trying to fetch some data with new react useReducer API and stuck on stage where i need to fetch it async. I just don't know how :/
How to place data fetching in switch statement or it's not a way how it's should be done?
import React from 'react'
const ProfileContext = React.createContext()
const initialState = {
data: false
}
let reducer = async (state, action) => {
switch (action.type) {
case 'unload':
return initialState
case 'reload':
return { data: reloadProfile() } //how to do it???
}
}
const reloadProfile = async () => {
try {
let profileData = await fetch('/profile')
profileData = await profileData.json()
return profileData
} catch (error) {
console.log(error)
}
}
function ProfileContextProvider(props) {
let [profile, profileR] = React.useReducer(reducer, initialState)
return (
<ProfileContext.Provider value={{ profile, profileR }}>
{props.children}
</ProfileContext.Provider>
)
}
export { ProfileContext, ProfileContextProvider }
I was trying to do it like this, but it's not working with async ;(
let reducer = async (state, action) => {
switch (action.type) {
case 'unload':
return initialState
case 'reload': {
return await { data: 2 }
}
}
}
This is an interesting case that the useReducer examples don't touch on. I don't think the reducer is the right place to load asynchronously. Coming from a Redux mindset, you would typically load the data elsewhere, either in a thunk, an observable (ex. redux-observable), or just in a lifecycle event like componentDidMount. With the new useReducer we could use the componentDidMount approach using useEffect. Your effect can be something like the following:
function ProfileContextProvider(props) {
let [profile, profileR] = React.useReducer(reducer, initialState);
useEffect(() => {
reloadProfile().then((profileData) => {
profileR({
type: "profileReady",
payload: profileData
});
});
}, []); // The empty array causes this effect to only run on mount
return (
<ProfileContext.Provider value={{ profile, profileR }}>
{props.children}
</ProfileContext.Provider>
);
}
Also, working example here: https://codesandbox.io/s/r4ml2x864m.
If you need to pass a prop or state through to your reloadProfile function, you could do so by adjusting the second argument to useEffect (the empty array in the example) so that it runs only when needed. You would need to either check against the previous value or implement some sort of cache to avoid fetching when unnecessary.
Update - Reload from child
If you want to be able to reload from a child component, there are a couple of ways you can do that. The first option is passing a callback to the child component that will trigger the dispatch. This can be done through the context provider or a component prop. Since you are using context provider already, here is an example of that method:
function ProfileContextProvider(props) {
let [profile, profileR] = React.useReducer(reducer, initialState);
const onReloadNeeded = useCallback(async () => {
const profileData = await reloadProfile();
profileR({
type: "profileReady",
payload: profileData
});
}, []); // The empty array causes this callback to only be created once per component instance
useEffect(() => {
onReloadNeeded();
}, []); // The empty array causes this effect to only run on mount
return (
<ProfileContext.Provider value={{ onReloadNeeded, profile }}>
{props.children}
</ProfileContext.Provider>
);
}
If you really want to use the dispatch function instead of an explicit callback, you can do so by wrapping the dispatch in a higher order function that handles the special actions that would have been handled by middleware in the Redux world. Here is an example of that. Notice that instead of passing profileR directly into the context provider, we pass the custom one that acts like a middleware, intercepting special actions that the reducer doesn't care about.
function ProfileContextProvider(props) {
let [profile, profileR] = React.useReducer(reducer, initialState);
const customDispatch= useCallback(async (action) => {
switch (action.type) {
case "reload": {
const profileData = await reloadProfile();
profileR({
type: "profileReady",
payload: profileData
});
break;
}
default:
// Not a special case, dispatch the action
profileR(action);
}
}, []); // The empty array causes this callback to only be created once per component instance
return (
<ProfileContext.Provider value={{ profile, profileR: customDispatch }}>
{props.children}
</ProfileContext.Provider>
);
}
It is a good practice to keep reducers pure. It will make useReducer more predictable and ease up testability. Subsequent approaches both combine async operations with pure reducers:
1. Fetch data before dispatch (simple)
Wrap the original dispatch with asyncDispatch and let context pass this function down:
const AppContextProvider = ({ children }) => {
const [state, dispatch] = useReducer(reducer, initState);
const asyncDispatch = () => { // adjust args to your needs
dispatch({ type: "loading" });
fetchData().then(data => {
dispatch({ type: "finished", payload: data });
});
};
return (
<AppContext.Provider value={{ state, dispatch: asyncDispatch }}>
{children}
</AppContext.Provider>
);
// Note: memoize the context value, if Provider gets re-rendered more often
};
const reducer = (state, { type, payload }) => {
if (type === "loading") return { status: "loading" };
if (type === "finished") return { status: "finished", data: payload };
return state;
};
const initState = {
status: "idle"
};
const AppContext = React.createContext();
const AppContextProvider = ({ children }) => {
const [state, dispatch] = React.useReducer(reducer, initState);
const asyncDispatch = () => { // adjust args to your needs
dispatch({ type: "loading" });
fetchData().then(data => {
dispatch({ type: "finished", payload: data });
});
};
return (
<AppContext.Provider value={{ state, dispatch: asyncDispatch }}>
{children}
</AppContext.Provider>
);
};
function App() {
return (
<AppContextProvider>
<Child />
</AppContextProvider>
);
}
const Child = () => {
const val = React.useContext(AppContext);
const {
state: { status, data },
dispatch
} = val;
return (
<div>
<p>Status: {status}</p>
<p>Data: {data || "-"}</p>
<button onClick={dispatch}>Fetch data</button>
</div>
);
};
function fetchData() {
return new Promise(resolve => {
setTimeout(() => {
resolve(42);
}, 2000);
});
}
ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.0/umd/react.production.min.js" integrity="sha256-32Gmw5rBDXyMjg/73FgpukoTZdMrxuYW7tj8adbN8z4=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.0/umd/react-dom.production.min.js" integrity="sha256-bjQ42ac3EN0GqK40pC9gGi/YixvKyZ24qMP/9HiGW7w=" crossorigin="anonymous"></script>
<div id="root"></div>
2. Use middleware for dispatch (generic)
dispatch might be enhanced with middlewares like redux-thunk, redux-observable, redux-saga for more flexibility and reusability. Or write your own one.
Let's say, we want to 1.) fetch async data with redux-thunk 2.) do some logging 3.) invoke dispatch with the final result. First define middlewares:
import thunk from "redux-thunk";
const middlewares = [thunk, logger]; // logger is our own implementation
Then write a custom useMiddlewareReducer Hook, which you can see here as useReducer bundled with additional middlewares, akin to Redux applyMiddleware:
const [state, dispatch] = useMiddlewareReducer(middlewares, reducer, initState);
Middlewares are passed as first argument, otherwise API is the same as useReducer. For the implementation, we take applyMiddleware source code and carry it over to React Hooks.
const middlewares = [ReduxThunk, logger];
const reducer = (state, { type, payload }) => {
if (type === "loading") return { ...state, status: "loading" };
if (type === "finished") return { status: "finished", data: payload };
return state;
};
const initState = {
status: "idle"
};
const AppContext = React.createContext();
const AppContextProvider = ({ children }) => {
const [state, dispatch] = useMiddlewareReducer(
middlewares,
reducer,
initState
);
return (
<AppContext.Provider value={{ state, dispatch }}>
{children}
</AppContext.Provider>
);
};
function App() {
return (
<AppContextProvider>
<Child />
</AppContextProvider>
);
}
const Child = () => {
const val = React.useContext(AppContext);
const {
state: { status, data },
dispatch
} = val;
return (
<div>
<p>Status: {status}</p>
<p>Data: {data || "-"}</p>
<button onClick={() => dispatch(fetchData())}>Fetch data</button>
</div>
);
};
function fetchData() {
return (dispatch, getState) => {
dispatch({ type: "loading" });
setTimeout(() => {
// fake async loading
dispatch({ type: "finished", payload: (getState().data || 0) + 42 });
}, 2000);
};
}
function logger({ getState }) {
return next => action => {
console.log("state:", JSON.stringify(getState()), "action:", JSON.stringify(action));
return next(action);
};
}
// same API as useReducer, with middlewares as first argument
function useMiddlewareReducer(
middlewares,
reducer,
initState,
initializer = s => s
) {
const [state, setState] = React.useState(initializer(initState));
const stateRef = React.useRef(state); // stores most recent state
const dispatch = React.useMemo(
() =>
enhanceDispatch({
getState: () => stateRef.current, // access most recent state
stateDispatch: action => {
stateRef.current = reducer(stateRef.current, action); // makes getState() possible
setState(stateRef.current); // trigger re-render
return action;
}
})(...middlewares),
[middlewares, reducer]
);
return [state, dispatch];
}
// | dispatch fn |
// A middleware has type (dispatch, getState) => nextMw => action => action
function enhanceDispatch({ getState, stateDispatch }) {
return (...middlewares) => {
let dispatch;
const middlewareAPI = {
getState,
dispatch: action => dispatch(action)
};
dispatch = middlewares
.map(m => m(middlewareAPI))
.reduceRight((next, mw) => mw(next), stateDispatch);
return dispatch;
};
}
ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.0/umd/react.production.min.js" integrity="sha256-32Gmw5rBDXyMjg/73FgpukoTZdMrxuYW7tj8adbN8z4=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.0/umd/react-dom.production.min.js" integrity="sha256-bjQ42ac3EN0GqK40pC9gGi/YixvKyZ24qMP/9HiGW7w=" crossorigin="anonymous"></script>
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/redux-thunk/2.3.0/redux-thunk.min.js" integrity="sha256-2xw5MpPcdu82/nmW2XQ6Ise9hKxziLWV2GupkS9knuw=" crossorigin="anonymous"></script>
<script>var ReduxThunk = window.ReduxThunk.default</script>
Note: we store intermediate state in mutable refs - stateRef.current = reducer(...), so each middleware can access current, most recent state at the time of its invocation with getState.
To have the exact API as useReducer, you can create the Hook dynamically:
const useMiddlewareReducer = createUseMiddlewareReducer(middlewares); //init Hook
const MyComp = () => { // later on in several components
// ...
const [state, dispatch] = useMiddlewareReducer(reducer, initState);
}
const middlewares = [ReduxThunk, logger];
const reducer = (state, { type, payload }) => {
if (type === "loading") return { ...state, status: "loading" };
if (type === "finished") return { status: "finished", data: payload };
return state;
};
const initState = {
status: "idle"
};
const AppContext = React.createContext();
const useMiddlewareReducer = createUseMiddlewareReducer(middlewares);
const AppContextProvider = ({ children }) => {
const [state, dispatch] = useMiddlewareReducer(
reducer,
initState
);
return (
<AppContext.Provider value={{ state, dispatch }}>
{children}
</AppContext.Provider>
);
};
function App() {
return (
<AppContextProvider>
<Child />
</AppContextProvider>
);
}
const Child = () => {
const val = React.useContext(AppContext);
const {
state: { status, data },
dispatch
} = val;
return (
<div>
<p>Status: {status}</p>
<p>Data: {data || "-"}</p>
<button onClick={() => dispatch(fetchData())}>Fetch data</button>
</div>
);
};
function fetchData() {
return (dispatch, getState) => {
dispatch({ type: "loading" });
setTimeout(() => {
// fake async loading
dispatch({ type: "finished", payload: (getState().data || 0) + 42 });
}, 2000);
};
}
function logger({ getState }) {
return next => action => {
console.log("state:", JSON.stringify(getState()), "action:", JSON.stringify(action));
return next(action);
};
}
function createUseMiddlewareReducer(middlewares) {
return (reducer, initState, initializer = s => s) => {
const [state, setState] = React.useState(initializer(initState));
const stateRef = React.useRef(state); // stores most recent state
const dispatch = React.useMemo(
() =>
enhanceDispatch({
getState: () => stateRef.current, // access most recent state
stateDispatch: action => {
stateRef.current = reducer(stateRef.current, action); // makes getState() possible
setState(stateRef.current); // trigger re-render
return action;
}
})(...middlewares),
[middlewares, reducer]
);
return [state, dispatch];
}
}
// | dispatch fn |
// A middleware has type (dispatch, getState) => nextMw => action => action
function enhanceDispatch({ getState, stateDispatch }) {
return (...middlewares) => {
let dispatch;
const middlewareAPI = {
getState,
dispatch: action => dispatch(action)
};
dispatch = middlewares
.map(m => m(middlewareAPI))
.reduceRight((next, mw) => mw(next), stateDispatch);
return dispatch;
};
}
ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.0/umd/react.production.min.js" integrity="sha256-32Gmw5rBDXyMjg/73FgpukoTZdMrxuYW7tj8adbN8z4=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.0/umd/react-dom.production.min.js" integrity="sha256-bjQ42ac3EN0GqK40pC9gGi/YixvKyZ24qMP/9HiGW7w=" crossorigin="anonymous"></script>
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/redux-thunk/2.3.0/redux-thunk.min.js" integrity="sha256-2xw5MpPcdu82/nmW2XQ6Ise9hKxziLWV2GupkS9knuw=" crossorigin="anonymous"></script>
<script>var ReduxThunk = window.ReduxThunk.default</script>
More infos - external libraries: react-use, react-hooks-global-state, react-enhanced-reducer-hook
I wrote a very detailed explanation of the problem and possible solutions. Dan Abramov suggested Solution 3.
Note: The examples in the gist provide examples with file operations but the same approach could be implemented for data fetching.
https://gist.github.com/astoilkov/013c513e33fe95fa8846348038d8fe42
Update:
I’ve added another comment in the weblink below. It’s a custom hook called useAsyncReducer based on the code below that uses the exact same signature as a normal useReducer.
function useAsyncReducer(reducer, initState) {
const [state, setState] = useState(initState),
dispatchState = async (action) => setState(await reducer(state, action));
return [state, dispatchState];
}
async function reducer(state, action) {
switch (action.type) {
case 'switch1':
// Do async code here
return 'newState';
}
}
function App() {
const [state, dispatchState] = useAsyncReducer(reducer, 'initState');
return <ExampleComponent dispatchState={dispatchState} />;
}
function ExampleComponent({ dispatchState }) {
return <button onClick={() => dispatchState({ type: 'switch1' })}>button</button>;
}
Old solution:
I just posted this reply here and thought it may be good to post here as well in case it helps anyone.
My solution was to emulate useReducer using useState + an async function:
async function updateFunction(action) {
switch (action.type) {
case 'switch1':
// Do async code here (access current state with 'action.state')
action.setState('newState');
break;
}
}
function App() {
const [state, setState] = useState(),
callUpdateFunction = (vars) => updateFunction({ ...vars, state, setState });
return <ExampleComponent callUpdateFunction={callUpdateFunction} />;
}
function ExampleComponent({ callUpdateFunction }) {
return <button onClick={() => callUpdateFunction({ type: 'switch1' })} />
}
I wrapped the dispatch method with a layer to solve the asynchronous action problem.
Here is initial state. The loading key record the application current loading status, It's convenient when you want to show loading page when the application is fetching data from server.
{
value: 0,
loading: false
}
There are four kinds of actions.
function reducer(state, action) {
switch (action.type) {
case "click_async":
case "click_sync":
return { ...state, value: action.payload };
case "loading_start":
return { ...state, loading: true };
case "loading_end":
return { ...state, loading: false };
default:
throw new Error();
}
}
function isPromise(obj) {
return (
!!obj &&
(typeof obj === "object" || typeof obj === "function") &&
typeof obj.then === "function"
);
}
function wrapperDispatch(dispatch) {
return function(action) {
if (isPromise(action.payload)) {
dispatch({ type: "loading_start" });
action.payload.then(v => {
dispatch({ type: action.type, payload: v });
dispatch({ type: "loading_end" });
});
} else {
dispatch(action);
}
};
}
Suppose there is an asynchronous method
async function asyncFetch(p) {
return new Promise(resolve => {
setTimeout(() => {
resolve(p);
}, 1000);
});
}
wrapperDispatch(dispatch)({
type: "click_async",
payload: asyncFetch(new Date().getTime())
});
The full example code is here:
https://codesandbox.io/s/13qnv8ml7q
it is very simple
you can change state in useEffect after async Fuction result
define useState for result of fetch
const [resultFetch, setResultFetch] = useState(null);
and useEffect for listen to setResultFetch
after fetch async API call setResultFetch(result of response)
useEffect(() => {
if (resultFetch) {
const user = resultFetch;
dispatch({ type: AC_USER_LOGIN, userId: user.ID})
}}, [resultFetch])

Resources