我正在尝试使用新的react useReducer API来获取一些数据,并停留在需要异步获取的阶段。我只是不知道如何:/
如何将数据获取放置在switch语句中,或者这不是应该完成的方式?
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 }
我试图这样做,但是它不能与async一起使用;(
let reducer = async (state, action) => {
switch (action.type) {
case 'unload':
return initialState
case 'reload': {
return await { data: 2 }
}
}
}
答案 0 :(得分:7)
这是一个有趣的情况,useReducer
示例没有涉及。我认为减速器不是异步加载的正确位置。来自Redux的心态,您通常会将数据加载到其他位置,例如以thunk,可观察(例如redux-observable)或仅在生命周期事件(如componentDidMount
中)加载。使用新的useReducer
,我们可以使用componentDidMount
的{{1}}方法。您的效果可能类似于以下内容:
useEffect
另外,这里的工作示例:https://codesandbox.io/s/r4ml2x864m。
如果您需要将prop或状态传递给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>
);
}
函数,则可以通过将第二个参数调整为reloadProfile
(示例中的空数组)来实现,以使其仅运行需要的时候。您需要对照先前的值进行检查或实施某种缓存,以避免在不必要时进行提取。
如果您希望能够从子组件中重新加载,可以通过两种方法进行。第一种选择是将回调传递给子组件,该子组件将触发分派。这可以通过上下文提供程序或组件属性完成。由于您已经在使用上下文提供程序,因此下面是该方法的示例:
useEffect
如果您确实要使用分派功能而不是显式回调,则可以通过将分派包装在更高阶的函数中来进行处理,该函数处理由中间件处理的特殊操作在Redux世界中。这是一个例子。请注意,我们没有将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>
);
}
直接传递给上下文提供者,而是传递了一种充当中间件的自定义变量,它拦截了reducer无关的特殊动作。
profileR
答案 1 :(得分:4)
good practice是保持减速器纯净的。这将使useReducer
更具可预测性并简化可测试性。
dispatch
之前获取数据(简单方法)您可以用dispatch
包装原始的asyncDispatch
并通过上下文向下传递此功能:
const AppContextProvider = ({ children }) => {
const [state, dispatch] = React.useReducer(reducer, initState);
const asyncDispatch = () => {
dispatch({ type: "loading" });
fetchData().then(data => {
dispatch({ type: "finished", payload: data });
});
};
return (
<AppContext.Provider value={{ state, dispatch: asyncDispatch }}>
{children}
</AppContext.Provider>
);
};
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 (
<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(fetchData())}>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>
dispatch
要获得更大的灵活性和可重用性,可以使用middlewares增强dispatch
。要么自己编写,要么使用redux-thunk之类的Redux生态系统中的文件。
比方说,您想在调用thunk
之前使用dispatch
来获取异步数据并进行一些日志记录:
import thunk from "redux-thunk";
const middlewares = [thunk, logger]; // logger is our own one
然后,我们可以编写类似于applyMiddleware
的编写器,该编写器创建一个链取数据→日志状态→分派。实际上,我在Redux存储库how this is done中进行了查找。
useMiddlewareReducer
钩子const [state, dispatch] = useMiddlewareReducer(middlewares, reducer, initState);
API与useReducer
相同,您将中间件作为第一个参数传递。基本思想是,我们将中间状态存储在mutable refs中,因此每个中间件始终可以使用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 (
<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>
找到外部库链接(按星数排序):react-use
,react-hooks-global-state
,react-enhanced-reducer-hook
答案 2 :(得分:3)
我对问题和可能的解决方案进行了非常详细的说明。 Dan Abramov建议了解决方案3。
注意:要点中的示例提供了有关文件操作的示例,但是可以采用相同的方法进行数据提取。
https://gist.github.com/astoilkov/013c513e33fe95fa8846348038d8fe42
答案 3 :(得分:2)
我在调度方法上包裹了一层以解决异步动作问题。
这是初始状态。 loading
键记录应用程序当前的加载状态。当应用程序从服务器获取数据时,要显示加载页面很方便。
{
value: 0,
loading: false
}
有四种动作。
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);
}
};
}
假设有一个异步方法
async function asyncFetch(p) {
return new Promise(resolve => {
setTimeout(() => {
resolve(p);
}, 1000);
});
}
wrapperDispatch(dispatch)({
type: "click_async",
payload: asyncFetch(new Date().getTime())
});
完整的示例代码在这里:
答案 4 :(得分:2)
更新:
我在下面的网络链接中添加了另一条评论。这是一个名为useAsyncReducer
的自定义钩子,基于以下代码,该钩子使用与普通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>;
}
旧解决方案:
我刚刚发布了此回复here,并认为将其张贴在这里也可能会有所帮助。
我的解决方案是使用useReducer
+异步函数来模拟useState
:
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' })} />
}
答案 5 :(得分:0)
您可以使用 useAsync 软件包:https://github.com/sonofjavascript/use-async,该软件包基本上是useReducer
钩子的扩展,该钩子允许通过http请求管理应用程序状态上的异步操作。>
通过ClientStore
设置客户端代理(您可以使用自己的http客户端):
import React from 'react'
import { ClientStore } from '@sonofjs/use-async'
import axios from 'axios'
import Component from './Component.jsx'
const ViewContainer = () => (
<ClientStore.Provider agent={axios}>
<Component />
</ClientStore.Provider>
)
export default ViewContainer
定义并使用您的操作:
import React, { useEffect } from 'react'
import useAsync from '@sonofjs/use-async'
const actions = {
FETCH_DATA: (state) => ({
...state,
loading: true,
request: {
method: 'GET',
url: '/api/data'
}
}),
FETCH_DATA_SUCCESS: (state, response) => ({
...state,
loading: false,
data: response
}),
FETCH_DATA_ERROR: (state, error) => ({
...state,
loading: false,
error
})
}
const initialState = {
loading: false,
data: {}
}
const Component = () => {
const [state, dispatch] = useAsync(actions, initialState)
useEffect(() => {
dispatch({ type: 'DATA' })
}, [])
return (
<>
{state.loading ? <span>Loading...</span> : null}
{<span>{JSON.stringify(state.data)}</span>}
{state.error ? <span>Error: {JSON.stringify(state.error)}</span> : null}
<>
)
}
export default Component
答案 6 :(得分:0)
很简单 您可以在异步函数结果后更改 useEffect 中的状态
为获取结果定义useState
const [resultFetch, setResultFetch] = useState(null);
和 useEffect
用于收听 setResultFetch
获取异步 API 调用后 setResultFetch(result of response)
useEffect(() => {
if (resultFetch) {
const user = resultFetch;
dispatch({ type: AC_USER_LOGIN, userId: user.ID})
}}, [resultFetch])