ReactJS Context > Reference latest state value from a state function - reactjs

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>

Related

How to run code on React.useReducer bailout

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>
);
}

How to wait for React useEffect hook finish before moving to next screen

I'm having issue where hook is being used in multiple files and it is being called twice for useEffect before the 1st one's async method finish (which should block the 2nd hook call, but it's not). See below 2 scenarios:
Stack Navigator
const { context, state } = useLobby(); // Hook is called here 1st, which will do the initial render and checks
return (
<LobbyContext.Provider value={context}>
<LobbyStack.Navigator>
{state.roomId
? <LobbyStack.Screen name="Lobby" component={LobbyScreen} />
: <LobbyStack.Screen name="Queue" component={QueueScreen} />
}
</LobbyStack.Navigator>
</LobbyContext.Provider>
)
Lobby Hooks
export const useLobby = () => {
const [state, dispatch] = React.useReducer(...)
//
// Scenario 1
// This get called twice (adds user to room twice)
//
React.useEffect(() => {
if (!state.isActive) assignRoom();
}, [state.isActive])
const assignRoom = async () => {
// dispatch room id
}
const context = React.useMemo(() => ({
join: () => { assignRoom(); }
})
}
Queue Screen
const { context, state } = useLobby(); // Hook is called here 2nd right after checking state from stack navigator
//
// Scenario 2
// Only does it once, however after state is changed to active
// the stack navigator didn't get re-render like it did in Scenario 1
//
React.useEffect(() => {
roomLobby.join();
}, []);
return (
...
{state.isActive
? "Show the room Id"
: "Check again"
...
)
In scenario 1, I guess while 1st hook is called and useEffect is doing async to add user to the room and set active to true. Meanwhile the conditional render part is moving straight to Queue screen which calls the hook again and doing the useEffect (since 1st haven't finished and isActive is still false).
How can I properly setup useReducer and useMemo so that it renders the screen base on the state.
Edited codes based on the answer
/* LobbyProvider */
const LobbyContext = React.createContext();
const lobbyReducer = (state, action) => {
switch (action.type) {
case 'SET_LOBBY':
return {
...state,
isActive: action.active,
lobby: action.lobby
};
case 'SET_ROOM':
return {
...state,
isQueued: action.queue,
roomId: action.roomId,
};
default:
return state;
}
}
const LobbyProvider = ({ children }) => {
const [state, dispatch] = React.useReducer(lobbyReducer, initialState);
React.useEffect(() => {
console.log("Provider:", state)
if (!state.isActive) joinRoom();
}, [])
// Using Firebase functions
const joinRoom = async () => {
try {
const response = await functions().httpsCallable('getActiveLobby')();
if (response) {
dispatch({ type: 'SET_LOBBY', active: true, lobby: response.data })
const room = await functions().httpsCallable('assignRoom')({ id: response.data.id });
dispatch({ type: 'SET_ROOM', queue: false, roomId: room.data.id })
}
} catch (e) {
console.error(e);
}
}
return (
<LobbyContext.Provider value={{state, dispatch}}>
{ children }
</LobbyContext.Provider>
)
}
/* StackNavigator */
const {state} = React.useContext(LobbyContext);
return (
<LobbyProvider>
// same as above <LobbyStack.Navigator>
// state doesn't seem to be updated here or to re-render
</LobbyProvider>
);
/* Queue Screen */
const {state} = React.useContext(LobbyContext);
// accessing state.isActive to do some conditional rendering
// which roomId does get rendered after dispatch
You must note that a custom hook will create a new instance of state everytime its called.
For example, you call the hook in StackNavigator component and then again in QueueScreen, so 2 different useReducers will be invoked instead of them sharing the states.
You should instead use useReducer in StackNavigator's parent and then utilize that as context within useLobby hook
const LobbyStateContext = React.createContext();
const Component = ({children}) => {
const [state, dispatch] = React.useReducer(...)
return (
<LobbyStateContext.Provider value={[state, dispatch]]>
{children}
</LobbyStateContext>
)
}
and use it like
<Component>
<StackNavigator />
</Component>
useLobby will then look like
export const useLobby = () => {
const [state, dispatch] = React.useContext(LobbyStateContext)
const assignRoom = async () => {
// dispatch room id
}
const context = React.useMemo(() => ({
join: () => { assignRoom(); }
})
return { context, assignRoom, state};
}
StackNavigator will utilize useLobby and have the useEFfect logic
const { context, state, assignRoom } = useLobby();
React.useEffect(() => {
if (!state.isActive) assignRoom();
}, [state.isActive])
return (
<LobbyContext.Provider value={context}>
<LobbyStack.Navigator>
{state.roomId
? <LobbyStack.Screen name="Lobby" component={LobbyScreen} />
: <LobbyStack.Screen name="Queue" component={QueueScreen} />
}
</LobbyStack.Navigator>
</LobbyContext.Provider>
)

React Hooks: useContext returning undefined in function

I have a small localization provider library that makes calls to a server to get translations based on the user's selected language. I've omitted a few helper functions for brevity. Here is the gist of it:
const LocaleContext = createContext();
const LocaleProvider = ({ children }) => {
const [state, setState] = useState(defaultValues);
if (state.phrases === null) {
changeLanguage(defaultLanguage);
}
return (
<LocaleContext.Provider value={[state, setState]}>
{children}
</LocaleContext.Provider>
);
};
const changeLanguage = (lang) => {
const [state, setState] = useContext(LocaleContext);
fetch(localizationEndpointForThatLanguage)
.then(response => response.json())
.then(data => {
setState({ ...state, phrases: data });
};
const translateString = (string) => {
const [state] = useContext(LocaleContext);
// if the string argument is found in state, return the translated value;
}
export { LocaleContext, LocaleProvider, changeLanguage, translateString };
Here is the component that uses it:
const Home = () => {
const [state, setState] = useContext(SiteContext);
useEffect(() => {
state.fetchContent(opts, state, setState);
}, []);
return (
<div>
{state.data && state.data.map((datum) => <div>{datum.title}</div>)}
<div onClick={() => changeLanguage('pl_PL')}>Change to Polish</div>
<div>{changeLanguage('Translate me')}</div>
</div>
);
};
The code gives me the error "TypeError: Cannot read property '0' of undefined" when trying to destructure the LocaleContext in the changeLanguage function. It works fine for the translateString function. It works if I manually pass state into the changeLanguage function.
const Home = () => {
const [state, setState] = useContext(SiteContext);
const [locale, setLocale] = useContext(LocaleContext);
useEffect(() => {
state.fetchContent(opts, state, setState);
}, []);
return (
<div>
{state.data && state.data.map((datum) => <div>{datum.title}</div>)}
<div onClick={() => changeLanguage('pl_PL')}>Change to Polish</div>
<div>{changeLanguage('Translate me', locale, setLocale)}</div>
</div>
);
};
and
const changeLanguage = (lang, state, setState) => {
fetch(localizationEndpointForThatLanguage)
.then(response => response.json())
.then(data => {
setState({ ...state, phrases: data });
});
};
I am fine with having to handoff context values throughout the app to make things work. I'd like help understanding why my translateString function does not require me to pass context values to it.

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