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
It is a good practice to keep reducers pure. It will make useReducer more predictable and ease up testability.
dispatch (simple approach)You can wrap the original dispatch with asyncDispatch and pass this function down via context:
const AppContextProvider = ({ children }) => {
const [state, dispatch] = React.useReducer(reducer, initState);
const asyncDispatch = () => {
dispatch({ type: "loading" });
fetchData().then(data => {
dispatch({ type: "finished", payload: data });
});
};
return (
{children}
);
};
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 = () => {
dispatch({ type: "loading" });
fetchData().then(data => {
dispatch({ type: "finished", payload: data });
});
};
return (
{children}
);
};
function App() {
return (
);
}
const Child = () => {
const val = React.useContext(AppContext);
const {
state: { status, data },
dispatch
} = val;
return (
Status: {status}
Data: {data || "-"}
);
};
function fetchData() {
return new Promise(resolve => {
setTimeout(() => {
resolve(42);
}, 2000);
});
}
ReactDOM.render( , document.getElementById("root"));
dispatchFor more flexibility and reusability, you can enhance dispatch with middlewares. Either write your own or use the ones from Redux ecosystem like redux-thunk.
Let's say, you want to fetch async data with thunk and do some logging, before dispatch is invoked:
import thunk from "redux-thunk";
const middlewares = [thunk, logger]; // logger is our own one
We can then write a composer similar to applyMiddleware, which creates a chain fetch data → log state → dispatch. In fact I looked up in the Redux repository, how this is done.
useMiddlewareReducer Hookconst [state, dispatch] = useMiddlewareReducer(middlewares, reducer, initState);
The API is the same as useReducer and you pass middlewares as first argument. Basic idea is, that we store intermediate state in mutable refs, so each middleware always can access the most recent state with getState.
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 (
{children}
);
};
function App() {
return (
);
}
const Child = () => {
const val = React.useContext(AppContext);
const {
state: { status, data },
dispatch
} = val;
return (
Status: {status}
Data: {data || "-"}
);
};
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( , document.getElementById("root"));
External library links found (ordered by star count): react-use, react-hooks-global-state, react-enhanced-reducer-hook