Test effect of Redux dispatch - reactjs

I have a React modal component that shows/hides depending on a variable that can be set/changed via a Redux dispatch.
I am at a bit of a loss of how to actually test it though.
Any help is appreciated.
Action
const HIDE_MODAL = 'HIDE_MODAL';
export const hideModal = () => ({
type: HIDE_MODAL
});
Reducer
// import HIDE_MODAL
export default (state = {}, action) => {
let newState = deepClone(state);
switch (action.type) {
case HIDE_MODAL:
newState.showModal = false;
break;
default:
return state;
}
return newState;
}
Component
const mapStateToProps = (state) => ({
showModal: state.showModal
});
const mapDispatchToProps = dispatch => ({
closeModal: () => dispatch(hideModal)
});
const MyComponent = ({closeModal, showModal}) => {
if (showModal) {
return <div>
<div>My Modal</div>
<button onClick={closeModal}/>Close</button>
</div>
}
);
Test
it('Should hide modal on click', () => {
const store = {showModal: true};
render(
<Provider store={mockStore(store)}>
<MyComponent />
</Provider>
);
expect(screen.queryByText('My modal')).toBeTruthy();
userEvent.click(screen.queryByText('Close'));
expect(screen.queryByText('My modal')).toBeFalsy();
});

first you should set your default value of showModal to true for testing purposes. If I'm not mistaken your function closeModal should be a little bit changes to this:
const mapDispatchToProps = dispatch => ({
closeModal: () => dispatch(hideModal()),
});
Because hideModal is a function you should call it too.
export const hideModal = () => ({
type: HIDE_MODAL
});
Otherwise you should pass an action directly to your dispatch Redux function(because dispatch takes an object with at least type key as an argument for proper work):
const mapDispatchToProps = dispatch => ({
closeModal: () => dispatch({
type: HIDE_MODAL
}),
});
Hope it will help.

Related

How to reset state when route changes?

I am having a problem with redux state. The state won't reset after route change.
I have a component with "ForgottenPassword ", when I type in email and click on "reminde password" there is alert with text "Email have been send" (if success) or "Error occured" (if email is incorrect). When I go to login component and then comeback to "ForgottenPassword" component the alert with text (success or error) is still there cause the state did not reset.
Is there a way to listen to route change and set state to initial so the success or error message would dissapear after route change?
const ForgottenPassword = (props: Props) => {
const { loginUserLoading = false, forgottenPasswordAsync } = props;
const t = useTranslationById();
const formik = useFormik({
initialValues: {
email: "",
},
onSubmit: (values: ForgottenPasswordParams) => {
forgottenPasswordAsync(values);
},
});
React.useEffect(() => {
if (props.forgotPasswordLoadingSuccess) {
formik.resetForm();
}
}, [props.forgotPasswordLoadingSuccess]);
const handleSubmitButton = React.useCallback(() => formik.handleSubmit(), [formik]);
return (
<div className={styles["forgotten-password-form"]}>
<div className={styles["forgotten-password-form__title"]}>{t("user-forgotten-password__title")}</div>
<form onSubmit={formik.handleSubmit}>
<Input
label={t("user-login__email-label")}
placeholder={t("user-login__email-label")}
name="email"
type="text"
/>
{props.forgotPasswordLoadingError && <div className={styles["forgotten-password-form__error"]}><FormattedMessage id="user-recover-password-error" /></div>}
{props.forgotPasswordLoadingSuccess && <div className={styles["forgotten-password-form__success"]}><FormattedMessage id="user-recover-password-email-send" /></div>}
<Button>
Remind password
</Button>
</form>
</div>
);
};
export default ForgottenPassword;
export const forgottenPassword = createAsyncAction(
"FORGOTTEN_PASSWORD_REQUEST",
"FORGOTTEN_PASSWORD_SUCCESS",
"FORGOTTEN_PASSWORD_FAILURE"
)<void, any, ApiError>();
const reducer = (state: UserState = {}, action: UserAction) => {
return produce(state, (draft) => {
switch (action.type) {
case getType(forgottenPassword.request):
draft.forgottenPasswordLoading = true;
break;
case getType(forgottenPassword.success):
draft.forgottenPasswordLoading = false;
draft.forgottenPasswordLoadingSuccess = true;
break;
case getType(forgottenPassword.failure):
draft.forgottenPasswordLoading = false;
draft.forgottenPasswordLoadingError = action.payload;
break;
}
});
};
const mapStateToProps = (state: RootState) => ({
forgotPasswordLoading: selectForgotPasswordLoading(state),
forgotPasswordLoadingSuccess: selectForgotPasswordLoadingSuccess(state),
forgotPasswordLoadingError: selectForgotPasswordLoadingError(state),
loginUserLoading: selectLoginUserLoading(state),
loginUserLoadingError: selectLoginUserLoadingError(state),
});
const mapDispatchToProps = (dispatch: Dispatch) =>
bindActionCreators(
{
forgottenPasswordAsync,
},
dispatch
);
export default connect(mapStateToProps, mapDispatchToProps)(ForgottenPassword);
export const selectForgotPasswordLoading = createSelector(selectState, (state) => state.forgottenPasswordLoading);
export const selectForgotPasswordLoadingSuccess = createSelector(selectState, (state) => state.forgottenPasswordLoadingSuccess);
export const selectForgotPasswordLoadingError = createSelector(selectState, (state) => state.forgottenPasswordLoadingError);
I am new here so sorry if I ask incorrectly.
The code is very complex and it is hard to paste only a sample of the code.
I was trying to do if else statemnt in Forgotten component but I figure out it won't work cause it is a problem that lies in redux, which I am starting to learn.
If you want to reset the state when the route is changed you can use the useEffect hook in the role of a componentWillUnmount in the page that is getting destroyed (in this case I suppose it is ForgottenPassword).
You can manage the reset as you prefer. A solution can be adding an action to your redux state like FORGOTTEN_PASSWORD_RESET which resets the state, and then dispatching the action from the hook.
You can write:
//ForgottenPassword.jsx
const ForgottenPassword = () => {
//existing code...
React.useEffect(()=>{
return () => {
//This will be called only when the page is destroyed.
reset() //reset your redux state here...
}
},[])
return (
//component's code...
);
}
const mapDispatchToProps = (dispatch) => {
return ({
reset: () => dispatch({ type: "FORGOTTEN_PASSWORD_RESET"}),
//OTHER ACTIONS....
})
}
export default connect(mapStateToProps, mapDispatchToProps)(ForgottenPassword);
TWO NOTES on the code above:
Update your reducer to manage the new action, otherwise is useless. I've not fully understood how it works otherwise I would have updated myself
Usually, to keep the code "clean", the action should be stored in a separate file from the actual component and then imported.

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>

How to cover lines in mapDispatchToProps with Jest?

My question is how do we cover these lines in jest?
export const mapDispatchToProps = dispatch => {
return {
submitClaimsForm: form => {
dispatch(submitClaimsForm(form));
}
};
};
In my component this is what the redux connected area looks like:
export function mapStateToProps(state) {
return {
formNonMember: state.form,
submissionSuccess: state.claimSubmission.submissionSuccess
};
}
export const mapDispatchToProps = dispatch => {
return {
submitClaimsForm: form => {
dispatch(submitClaimsForm(form));
}
};
};
let AdditionalDetailsFormConnect = reduxForm({
form: 'AdditionalDetails',
destroyOnUnmount: false
})(AdditionalDetailsForm);
export default connect(
mapStateToProps,
mapDispatchToProps
)(AdditionalDetailsFormConnect);
And this is how the dispatched action is used:
onSubmit() {
this.props.submitClaimsForm(this.props.formattedForm);
}
Next this is what the actual action looks like:
import {postClaimsForm} from '../shared/services/api';
export const Actions = {
SET_SUBMISSION_STATUS: 'SET_SUBMISSION_STATUS'
};
export const submitClaimsForm = form => dispatch => {
return postClaimsForm(form)
.then(res => {
// console.log('promise returned:', res);
return dispatch({
type: Actions.SET_SUBMISSION_STATUS,
submissionSuccess: true
});
})
.catch(error => {
// console.log('error returned:', error);
return dispatch({
type: Actions.SET_SUBMISSION_STATUS,
submissionSuccess: false
});
});
};
What I've tried so far:
it('mapDispatchToProps works as expected', () => {
const actionProps = mapDispatchToProps({
submitClaimsForm: jest.fn()
});
actionProps.submitClaimsForm();
expect(submitClaimsForm).toHaveBeenCalled();
});
But this errors and tells me that dispatch is undefined.
I also have this test, which passes, it tells me that submitClaimsForm has been called, but it just covers the lines for onSubmit:
it('onSubmit is called on submit', function() {
const spyOnSubmit = jest.spyOn(wrapper.instance(), 'onSubmit');
const mockHandleSubmit = jest.fn(wrapper.instance().onSubmit);
const submitClaimsForm = jest.fn(wrapper.instance().submitClaimsForm);
wrapper.setProps({
handleSubmit: mockHandleSubmit,
submitClaimsForm
});
wrapper.find('MyForm').simulate('submit');
expect(mockHandleSubmit).toHaveBeenCalled();
expect(spyOnSubmit).toHaveBeenCalled();
expect(submitClaimsForm).toHaveBeenCalled(); // <--
});
The reason your mapDispatchToProps works as expected test fails is because mapDispatchToProps expects a dispatch function to be passed in, not the map itself (that's what mapDispatchToProps returns).
This should work:
jest.mock('./actions');
import * as actions from './actions';
it('mapDispatchToProps calls the appropriate action', async () => {
// mock the 'dispatch' object
const dispatch = jest.fn();
const actionProps = mapDispatchToProps(dispatch);
const formData = { ... };
actionProps.submitClaimsForm(formData);
// verify the appropriate action was called
expect(actions.submitClaimsForm).toHaveBeenCalled(formData);
});

React Redux - dispatch action with react hooks

To clearify I'm pretty newbie with the concept of react-redux. I try to dispatch an async action in the presentational comp. but this does not seem to work out.
Container Component
const store = configureStore();
const Root: React.FC = () => (
<Provider store={store}>
<App />
</Provider>
);
render(<Root/>, document.getElementById('root'));
Presentational Component
interface AppProps {
system: SystemState,
updateSession: typeof updateSession,
getLanguageThunk: any
}
const App: React.FC<AppProps> = ({system, updateSession, getLanguageThunk}) => {
useEffect(() => {
getLanguageThunk().then((res: any) => {
console.log(res);
i18n.init().then(
() => i18n.changeLanguage(res.language));
});
}, []
);
return (
<div>
<div className="app">
<TabBar/>
</div>
</div>
);
};
const mapStateToProps = (state: AppState) => ({
system: state.system
});
export default connect(mapStateToProps, { updateSession, getLanguageThunk })(App);
But the console everytime logs undefined. So I am doint something wrong here. Maybe some of u can help me out on here.
Redux middleware
export const getLanguageThunk = (): ThunkAction<void, AppState, null, Action<string>> => async dispatch => {
const language = await getLanguage();
dispatch(
updateSession({
disableSwipe: false,
language
})
)
};
async function getLanguage() {
try {
const response = await fetch('http://localhost:3000/language');
return response.json();
} catch {
return { language: 'en_GB' }
}
}
You need to return the language from getLanguageThunk, to be able to use it from promise in the useEffect method
export const getLanguageThunk = (): ThunkAction<void, AppState, null, Action<string>> => async dispatch => {
const language = await getLanguage();
dispatch(
updateSession({
disableSwipe: false,
language
})
)
return language;
};

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