How to run code on React.useReducer bailout - reactjs

I am trying to write a library that processes dispatched useReducer actions. However, they need to be processed only once I have the new react state.
The idea in the following example is to buffer all the actions as they happen and then after the component rerenders get the new state and send all the buffered actions with the new state into the processor (simple logger function in this case). Problem is, that won't work if the component bails out of rendering, because the component won't rerender and so the useEffect callback won't get called either.
function logger(action, state) {
// this should log every action immediately once its processed by react
// state must be up-to-date (after all batched state changes are processed)
console.log({action, state});
}
const buffer = [];
const Component = () => {
const [state, dispatch] = useReducer(
(state, action) => {
// bail out if there is no change
if (action.value === state.someState) return state;
return {someState: action.value};
},
{someState: 1}
);
const loggedDispatch = (action) => {
dispatch(action);
// Can't run the logger here as I don't have the new state yet
// so I am buffering the actions instead for later
buffer.push(action);
};
useEffect(() => {
// Now I have the new state, so I can run the logger
// Problem is that this won't run if the reducer bailed out of render
while (buffer.length) {
logger(buffer.shift(), state);
}
});
return (
<div>
<div onClick={() => loggedDispatch({value: 1})}>Set to 1</div>
<div onClick={() => loggedDispatch({value: 2})}>Set to 2</div>
</div>
);
};
Any idea how to fix this with react hooks?
With class components it was possible to use
this.setState(state => state, function () {
console.log('This runs regardless of bailout');
})
But react hooks such as useState or useReducer don't support the second parameter.
Codesandbox: https://codesandbox.io/s/friendly-pine-lebrb?file=/src/App.js

I think it is related to initial value that in {someState: 1} equal to value of first div button because when I click the second div button everything is going to be good.
this when I click first button
and this when click the second
Bailing out of a dispatch
If you return the same value from a Reducer Hook as the current state, React will bail out without rendering the children or firing effects. (React uses the Object.is comparison algorithm.)
this is the quote of react useReducer about return same value witch maybe your problem.
I hope this useful.

You can create a custom hook that will use useReducer internally. In addition to the default functionality of useReducer it'll pass the previous state, next state and the action dispatched to your middleware.
Codesandbox: https://codesandbox.io/s/use-reducer-with-middleware-y2hvb?file=/src/index.js
const {
useCallback,
useReducer
} = React;
const {
render
} = ReactDOM
const useReducerWithMiddleware = (reducer, initialState, ...middlewares) => {
const [state, dispatch] = useReducer((prevState, action) => {
const nextState = reducer(prevState, action);
middlewares.forEach((middleware) =>
middleware(prevState, nextState, action)
);
return nextState;
}, initialState);
return [state, dispatch];
};
const reducer = (state, action) => {
if (action.type === CHANGE_TEXT_ACTION_TYPE) {
return { ...state,
text: action.payload
};
}
return state;
};
const initialState = {
text: "foo"
};
const loggerMiddleware = (prevState, nextState, action) => {
console.log(`${action.type} is fired with payload = ${action.payload}.`);
console.log(`prev state is equal to \n${JSON.stringify(prevState, null, 2)}`);
console.log(`next state is equal to \n${JSON.stringify(nextState, null, 2)}`);
};
const CHANGE_TEXT_ACTION_TYPE = "changeText";
const App = () => {
const [state, dispatch] = useReducerWithMiddleware(
reducer,
initialState,
loggerMiddleware
);
const handleButtonClick = useCallback(() => {
dispatch({
type: CHANGE_TEXT_ACTION_TYPE,
payload: state.text === "foo" ? "bar" : "foo"
});
}, [dispatch, state]);
return <button onClick={handleButtonClick}>{state.text}</button>;
};
render(<App /> , document.getElementById("app"));
<script crossorigin src="https://unpkg.com/react#17/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#17/umd/react-dom.development.js"></script>
<body>
<div id="app"></div>
</body>

You can create a custom hook and use that,
I have created a sample, check this out this might help
https://stackblitz.com/edit/react-dispatcher
I have given suggestion in GitHub issue as well
https://github.com/facebook/react/issues/15344#issuecomment-868204394
import { useReducer, useEffect } from "react";
let cb = () => {};
function useDispatch() {
const [state, dispatch] = useReducer(
(state, action) => {
if (action.value === state.someState) return state;
return { someState: action.value };
},
{ someState: 0 }
);
useEffect(() => {
cb(state);
}, [state]);
return [
state,
(action, callback) => {
cb = callback;
dispatch(action);
},
];
}
const Component = () => {
const [state, dispatch] = useDispatch();
const loggedDispatch = (action) => {
console.log("pre-state", state);
dispatch(action, (newState) => {
console.log("new-state", newState);
});
};
return (
<div>
<div onClick={() => loggedDispatch({ value: 1 })}>Set to 1</div>
<div onClick={() => loggedDispatch({ value: 2 })}>Set to 2</div>
</div>
);
};
export default function App() {
return (
<div className="App">
<Component />
</div>
);
}

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.

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>

ReactJS Context > Reference latest state value from a state function

I'm currently using a common Context pattern I've seen, which allows child components to update a parent's state (i.e. the Provider) by passing a modifier function down through the shared Context.
The problem I'm having, is that the modifier functions only reference the original state, and don't reference the latest state...
Is there a way to get the latest state values (i.e. state.user) from the modifier function (i.e. modUser)?
Below is the minimum code to reproduce:
Expected output: Nick ... then Bob ... then BOB.
Actual output: Nick ... then Bob ... then NICK.
import React, { useState, useContext, useEffect } from "react"
const defaultState = {
user: null,
setUser: () => { },
modUser: () => { }
}
const Context = React.createContext(defaultState)
const Test = () => {
const setUser = user => setState(prevState => ({ ...prevState, user }))
const modUser = () => setUser(state.user.toUpperCase()) // `state.user` never changes to updated value!
// user is 'Nick' at start...
const initState = {
user: 'Nick',
setUser,
modUser
}
// user changes to 'Bob' after 1 second...
useEffect(() => { setTimeout(() => setUser('Bob'), 1000) }, [])
const [state, setState] = useState(initState)
return <Context.Provider value={state}>
<div>Parent user is {state.user}</div>
<TestInner />
</Context.Provider>
}
const TestInner = () => {
const state = useContext(Context)
// user (should) change to uppercase 'BOB' after 2 seconds...
useEffect(() => { setTimeout(() => state.modUser(), 2000) }, [])
return <div>Child user is {state.user}</div>
}
export default Test
You can pass a callback to the state setter. Looking a little closer at your code there is no need to wrap setUser or modUser in a useCallback because after const [state, setState] = useState(initState); you never change them. The following can work and initializes the state once so mod and setUser don't need to be re created on re render of Test (because they are only used on first render and then ignored.
const { useState, useContext, useEffect } = React;
const Context = React.createContext();
const Test = () => {
//use callback to set state with initial value, after
// first render the callback will not be called again
// until component is unmounted and re mounted
const [state, setState] = useState(() => ({
setUser: user =>
setState(prevState => ({ ...prevState, user })),
modUser: () =>
setState(state => ({
...state,
user: state.user.toUpperCase(),
})),
user: 'Nick',
}));
//get the setUser function to pass it to useEffect as a dependency
const { setUser } = state;
useEffect(() => {
setTimeout(() => setUser('Bob'), 1000);
}, [setUser]);
return (
<Context.Provider value={state}>
<div>Parent user is {state.user}</div>
<TestInner />
</Context.Provider>
);
};
const TestInner = () => {
const { modUser, user } = useContext(Context);
useEffect(() => {
setTimeout(() => modUser(), 2000);
}, [modUser]);
return <div>Child user is {user}</div>;
};
ReactDOM.render(<Test />, 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 Hooks: updating multiple hook states atomically [duplicate]

I'm trying React hooks for the first time and all seemed good until I realised that when I get data and update two different state variables (data and loading flag), my component (a data table) is rendered twice, even though both calls to the state updater are happening in the same function. Here is my api function which is returning both variables to my component.
const getData = url => {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(async () => {
const test = await api.get('/people')
if(test.ok){
setLoading(false);
setData(test.data.results);
}
}, []);
return { data, loading };
};
In a normal class component you'd make a single call to update the state which can be a complex object but the "hooks way" seems to be to split the state into smaller units, a side effect of which seems to be multiple re-renders when they are updated separately. Any ideas how to mitigate this?
You could combine the loading state and data state into one state object and then you could do one setState call and there will only be one render.
Note: Unlike the setState in class components, the setState returned from useState doesn't merge objects with existing state, it replaces the object entirely. If you want to do a merge, you would need to read the previous state and merge it with the new values yourself. Refer to the docs.
I wouldn't worry too much about calling renders excessively until you have determined you have a performance problem. Rendering (in the React context) and committing the virtual DOM updates to the real DOM are different matters. The rendering here is referring to generating virtual DOMs, and not about updating the browser DOM. React may batch the setState calls and update the browser DOM with the final new state.
const {useState, useEffect} = React;
function App() {
const [userRequest, setUserRequest] = useState({
loading: false,
user: null,
});
useEffect(() => {
// Note that this replaces the entire object and deletes user key!
setUserRequest({ loading: true });
fetch('https://randomuser.me/api/')
.then(results => results.json())
.then(data => {
setUserRequest({
loading: false,
user: data.results[0],
});
});
}, []);
const { loading, user } = userRequest;
return (
<div>
{loading && 'Loading...'}
{user && user.name.first}
</div>
);
}
ReactDOM.render(<App />, document.querySelector('#app'));
<script src="https://unpkg.com/react#16.7.0-alpha.0/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom#16.7.0-alpha.0/umd/react-dom.development.js"></script>
<div id="app"></div>
Alternative - write your own state merger hook
const {useState, useEffect} = React;
function useMergeState(initialState) {
const [state, setState] = useState(initialState);
const setMergedState = newState =>
setState(prevState => Object.assign({}, prevState, newState)
);
return [state, setMergedState];
}
function App() {
const [userRequest, setUserRequest] = useMergeState({
loading: false,
user: null,
});
useEffect(() => {
setUserRequest({ loading: true });
fetch('https://randomuser.me/api/')
.then(results => results.json())
.then(data => {
setUserRequest({
loading: false,
user: data.results[0],
});
});
}, []);
const { loading, user } = userRequest;
return (
<div>
{loading && 'Loading...'}
{user && user.name.first}
</div>
);
}
ReactDOM.render(<App />, document.querySelector('#app'));
<script src="https://unpkg.com/react#16.7.0-alpha.0/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom#16.7.0-alpha.0/umd/react-dom.development.js"></script>
<div id="app"></div>
This also has another solution using useReducer! first we define our new setState.
const [state, setState] = useReducer(
(state, newState) => ({...state, ...newState}),
{loading: true, data: null, something: ''}
)
after that we can simply use it like the good old classes this.setState, only without the this!
setState({loading: false, data: test.data.results})
As you may noticed in our new setState (just like as what we previously had with this.setState), we don't need to update all the states together! for example I can change one of our states like this (and it doesn't alter other states!):
setState({loading: false})
Awesome, Ha?!
So let's put all the pieces together:
import {useReducer} from 'react'
const getData = url => {
const [state, setState] = useReducer(
(state, newState) => ({...state, ...newState}),
{loading: true, data: null}
)
useEffect(async () => {
const test = await api.get('/people')
if(test.ok){
setState({loading: false, data: test.data.results})
}
}, [])
return state
}
Typescript Support.
Thanks to P. Galbraith who replied this solution,
Those using typescript can use this:
useReducer<Reducer<MyState, Partial<MyState>>>(...)
where MyState is the type of your state object.
e.g. In our case it'll be like this:
interface MyState {
loading: boolean;
data: any;
something: string;
}
const [state, setState] = useReducer<Reducer<MyState, Partial<MyState>>>(
(state, newState) => ({...state, ...newState}),
{loading: true, data: null, something: ''}
)
Previous State Support.
In comments user2420374 asked for a way to have access to the prevState inside our setState, so here's a way to achieve this goal:
const [state, setState] = useReducer(
(state, newState) => {
newWithPrevState = isFunction(newState) ? newState(state) : newState
return (
{...state, ...newWithPrevState}
)
},
initialState
)
// And then use it like this...
setState(prevState => {...})
isFunction checks whether the passed argument is a function (which means you're trying to access the prevState) or a plain object. You can find this implementation of isFunction by Alex Grande here.
Notice. For those who want to use this answer a lot, I decided to turn it into a library. You can find it here:
Github: https://github.com/thevahidal/react-use-setstate
NPM: https://www.npmjs.com/package/react-use-setstate
Batching update in react-hooks https://github.com/facebook/react/issues/14259
React currently will batch state updates if they're triggered from within a React-based event, like a button click or input change. It will not batch updates if they're triggered outside of a React event handler, like an async call.
This will do:
const [state, setState] = useState({ username: '', password: ''});
// later
setState({
...state,
username: 'John'
});
To replicate this.setState merge behavior from class components,
React docs recommend to use the functional form of useState with object spread - no need for useReducer:
setState(prevState => {
return {...prevState, loading, data};
});
The two states are now consolidated into one, which will save you a render cycle.
There is another advantage with one state object: loading and data are dependent states. Invalid state changes get more apparent, when state is put together:
setState({ loading: true, data }); // ups... loading, but we already set data
You can even better ensure consistent states by 1.) making the status - loading, success, error, etc. - explicit in your state and 2.) using useReducer to encapsulate state logic in a reducer:
const useData = () => {
const [state, dispatch] = useReducer(reducer, /*...*/);
useEffect(() => {
api.get('/people').then(test => {
if (test.ok) dispatch(["success", test.data.results]);
});
}, []);
};
const reducer = (state, [status, payload]) => {
if (status === "success") return { ...state, data: payload, status };
// keep state consistent, e.g. reset data, if loading
else if (status === "loading") return { ...state, data: undefined, status };
return state;
};
const App = () => {
const { data, status } = useData();
return status === "loading" ? <div> Loading... </div> : (
// success, display data
)
}
const useData = () => {
const [state, dispatch] = useReducer(reducer, {
data: undefined,
status: "loading"
});
useEffect(() => {
fetchData_fakeApi().then(test => {
if (test.ok) dispatch(["success", test.data.results]);
});
}, []);
return state;
};
const reducer = (state, [status, payload]) => {
if (status === "success") return { ...state, data: payload, status };
// e.g. make sure to reset data, when loading.
else if (status === "loading") return { ...state, data: undefined, status };
else return state;
};
const App = () => {
const { data, status } = useData();
const count = useRenderCount();
const countStr = `Re-rendered ${count.current} times`;
return status === "loading" ? (
<div> Loading (3 sec)... {countStr} </div>
) : (
<div>
Finished. Data: {JSON.stringify(data)}, {countStr}
</div>
);
}
//
// helpers
//
const useRenderCount = () => {
const renderCount = useRef(0);
useEffect(() => {
renderCount.current += 1;
});
return renderCount;
};
const fetchData_fakeApi = () =>
new Promise(resolve =>
setTimeout(() => resolve({ ok: true, data: { results: [1, 2, 3] } }), 3000)
);
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>var { useReducer, useEffect, useState, useRef } = React</script>
PS: Make sure to prefix custom Hooks with use (useData instead of getData). Also passed callback to useEffect cannot be async.
If you are using third-party hooks and can't merge the state into one object or use useReducer, then the solution is to use :
ReactDOM.unstable_batchedUpdates(() => { ... })
Recommended by Dan Abramov here
See this example
A little addition to answer https://stackoverflow.com/a/53575023/121143
Cool! For those who are planning to use this hook, it could be written in a bit robust way to work with function as argument, such as this:
const useMergedState = initial => {
const [state, setState] = React.useState(initial);
const setMergedState = newState =>
typeof newState == "function"
? setState(prevState => ({ ...prevState, ...newState(prevState) }))
: setState(prevState => ({ ...prevState, ...newState }));
return [state, setMergedState];
};
Update: optimized version, state won't be modified when incoming partial state was not changed.
const shallowPartialCompare = (obj, partialObj) =>
Object.keys(partialObj).every(
key =>
obj.hasOwnProperty(key) &&
obj[key] === partialObj[key]
);
const useMergedState = initial => {
const [state, setState] = React.useState(initial);
const setMergedState = newIncomingState =>
setState(prevState => {
const newState =
typeof newIncomingState == "function"
? newIncomingState(prevState)
: newIncomingState;
return shallowPartialCompare(prevState, newState)
? prevState
: { ...prevState, ...newState };
});
return [state, setMergedState];
};
In addition to Yangshun Tay's answer you'll better to memoize setMergedState function, so it will return the same reference each render instead of new function. This can be crucial if TypeScript linter forces you to pass setMergedState as a dependency in useCallback or useEffect in parent component.
import {useCallback, useState} from "react";
export const useMergeState = <T>(initialState: T): [T, (newState: Partial<T>) => void] => {
const [state, setState] = useState(initialState);
const setMergedState = useCallback((newState: Partial<T>) =>
setState(prevState => ({
...prevState,
...newState
})), [setState]);
return [state, setMergedState];
};
You can also use useEffect to detect a state change, and update other state values accordingly

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