背景
我正在将一个内置于 React Native 的应用程序连接到 REST API。我正在通过 Axios 处理请求并使用 Redux 存储查询结果。我有一个用于我的 api 连接的 index.js 文件,其中包含充当需要更深层次授权的请求的处理程序的函数。我有一个返回访问令牌的简单函数,这是由当前位于应用程序“欢迎页面”中的以下代码触发的。
useEffect(() => {
dispatch(retrieveToken());
}, [dispatch]);
理想情况下,在浏览几个屏幕后,用户将进入主页并触发以下代码:
useEffect(() => {
dispatch(retrieveData());
}, [dispatch]);
到目前为止,一切都很好。这些是调度触发器的函数:
export const getToken = () =>
apiInstance
.request({
url: ENDPOINTS.TOKEN,
data: qs.stringify({
grant_type: 'some_credentials',
c_id: 'some_id',
c_secret: 'some_secret',
}),
headers: {
'content-type': 'some_content_type',
},
method: 'POST',
})
.then(response => {
return response.data;
})
.catch(error => {
return Promise.reject(error.message);
});
export const getData = () =>
apiInstance
.request({
url: ENDPOINTS.DATA,
method: 'POST',
data: qs.stringify({
timestamp: Date.now(),
c_id: 'some_id',
token: **this is the token we get from the previous function**,
}),
headers: {
'content-type': 'some_content_type',
},
})
.then(response => {
return response.data;
})
.catch(error => {
return Promise.reject(error.message);
});
问题
正如我之前提到的,这是一个 Redux/Axios 解决方案。这意味着状态是全局存储的,但有一个执行顺序。您应该注意到这两个函数存储在同一个文件中并且不会被调用,除非明确说明,例如我之前展示的两个调度调用。
事实是,如果我从 Home 记录令牌(在使用调度调用它之后),我可以清楚地看到它,但是如果我尝试从存储请求函数的文件中记录所述令牌,我会得到一个空数组。我尝试通过以下所有方式填充令牌字段:
问题
如何以特定顺序从另一个内部访问 axios 查询的结果?
<块引用>需要注意的是,API 中的数据只能通过 POST 访问,并且我在执行 getData() 时得到的错误代码是 401,不正确的凭据。 另外,请记住这是一个 Redux 应用程序。查询的结果与全局状态一起存储。但是,无法从外部组件访问此状态,并且我无法从执行查询的文件中调用它,因为令牌“在那个时间点不存在。”
操作代码
import keyMirror from 'keymirror';
import {createAction} from 'redux-actions';
import {getToken} from '../../api';
export const tokenActionTypes = keyMirror({
RETRIEVE_TOKEN_REQUEST: null,
RETRIEVE_TOKEN_SUCCESS: null,
RETRIEVE_TOKEN_FAILURE: null,
});
const tokenActionCreators = {
request: createAction(tokenActionTypes.RETRIEVE_TOKEN_REQUEST),
success: createAction(tokenActionTypes.RETRIEVE_TOKEN_SUCCESS),
failure: createAction(tokenActionTypes.RETRIEVE_TOKEN_FAILURE),
};
export const retrieveToken = () => dispatch => {
dispatch(tokenActionCreators.request());
getToken()
.then(token => dispatch(tokenActionCreators.success(token)))
.catch(error => dispatch(tokenActionCreators.failure(error)));
};
Reducer 代码
import {tokenActionTypes} from '../actions/token';
export const initialState = {
loadingToken: false,
token: [],
error: null,
};
const actionsMap = {
[tokenActionTypes.RETRIEVE_TOKEN_REQUEST]: state => ({
...state,
loadingToken: true,
}),
[tokenActionTypes.RETRIEVE_TOKEN_SUCCESS]: (state, action) => ({
...state,
loadingToken: false,
token: action.payload,
}),
[tokenActionTypes.RETRIEVE_TOKEN_FAILURE]: (state, action) => ({
...state,
loadingToken: false,
error: action.payload,
}),
};
export default (state = initialState, action) => {
const actionHandler = actionsMap[action.type];
if (!actionHandler) {
return state;
}
return actionHandler(state, action);
};
答案 0 :(得分:1)
您可以将一个 thunk 组合到另一个中,例如在获取数据中组合获取令牌:
export const retrieveToken = () => (dispatch, getState) => {
//you could use getState() to see if you need to fetch the token
// const tokenResult = selectToken(getState());
// if(token && !token expired) { return Promise.resolve() }
dispatch(tokenActionCreators.request());
//return a promise so you can wait for it
return getToken()
.then(token => dispatch(tokenActionCreators.success(token)))
.catch(error => dispatch(tokenActionCreators.failure(error)));
};
//in retrieve data you can wait for the token
export const retrieveData = () => dispatch => {
dispatch(retrieveToken()).then(
()=>{
//here return getting the data
}
)
};
该代码中可能存在的错误是一个渲染周期将分派多个 thunk,这些 thunk 将多次获取令牌。您可以通过 grouping 带有在解析时无效的缓存的retrieveToken 操作来解决该问题:
const invalidateOnResolveCache = (cache = new Map()) => {
return {
get: (key) => cache.get(key),
set: (key, value) => cache.set(key, value),
resolved: (x) => cache.delete(key),
};
};
或者你可以为所有需要令牌的 thunk 编写一个 wrap 函数:
//group retrieveToken in such a way that if it's called multiple times
// during a render cycle the token request will only be made once
//https://gist.github.com/amsterdamharu/2dde4a6f531251f3769206ee44458af7
export const needsToken =
(fn) =>
(...args) =>
(dispatch, getState) =>
dispatch(retrieveToken(...args)).then(() =>
//you could use getState to get the token and pass it to
// fn together with the other args
// for example: fn(...args.concat(selectToken(getState())))
fn(...args)
);
export const autoTokenRetrieveData = needsToken(retrieveData);
//use needsToken for any other thunk actions that need a token
示例:
const { Provider, useDispatch, useSelector } = ReactRedux;
const { createStore, applyMiddleware, compose } = Redux;
const { createSelector } = Reselect;
//grouping code to group your actions
//group promise returning function
const createGroup =
(cache) =>
(fn, getKey = (...x) => JSON.stringify(x)) =>
(...args) => {
const key = getKey(args);
let result = cache.get(key);
if (result) {
return result;
}
//no cache
result = Promise.resolve(fn.apply(null, args)).then(
(r) => {
cache.resolved(key); //tell cache promise is done
return r;
},
(e) => {
cache.resolve(key); //tell cache promise is done
return Promise.reject(e);
}
);
cache.set(key, result);
return result;
};
//thunk action creators are not (...args)=>result but
// (...args)=>(dispatch,getState)=>result
// so here is how we group thunk actions
const createGroupedThunkAction = (thunkAction, cache) => {
const group = createGroup(cache)(
(args, dispatch, getState) =>
thunkAction.apply(null, args)(dispatch, getState)
);
return (...args) =>
(dispatch, getState) => {
return group(args, dispatch, getState);
};
};
const createInvalidateOnResolveCache = (
cache = new Map()
) => {
return {
get: (key) => cache.get(key),
set: (key, value) => cache.set(key, value),
resolved: (key) => cache.delete(key),
};
};
//function that fetches token
const uniqueToken = (
(token) => () =>
token++
)(1);
const fetchToken = () => Promise.resolve(uniqueToken());
const initialState = {
data1: [],
data2: [],
token: null,
};
//action types
const DATA_SUCCESS = 'DATA_SUCCESS';
const GOT_TOKEN = 'GOT_TOKEN';
//action creators
const dataSuccess = (data, key) => ({
type: DATA_SUCCESS,
payload: { key, data },
});
const gotToken = (token) => ({
type: GOT_TOKEN,
payload: token,
});
const reducer = (state, { type, payload }) => {
if (type === DATA_SUCCESS) {
const { data, key } = payload;
return {
...state,
[key]: data,
};
}
if (type === GOT_TOKEN) {
return {
...state,
token: {
value: payload,
created: Date.now(),
},
};
}
return state;
};
//thunk getting the data
const getData1 = (token) => (dispatch) =>
Promise.resolve().then(() =>
dispatch(
dataSuccess(
`got data 1 with token: ${token}`,
'data1'
)
)
);
const getData2 = (token) => (dispatch) =>
Promise.resolve().then(() =>
dispatch(
dataSuccess(
`got data 2 with token: ${token}`,
'data2'
)
)
);
//thunk getting the token:
const getToken = () => (dispatch) =>
fetchToken().then((token) => dispatch(gotToken(token)));
//grouped thunk getting token
const getTokenGrouped = createGroupedThunkAction(
getToken,
createInvalidateOnResolveCache()
);
const needsToken =
(fn) =>
(...args) =>
(dispatch, getState) => {
let promise;
//only fetch token if it's older than 1 second
const tokenResult = selectToken(getState());
if (
tokenResult &&
Date.now() - new Date(tokenResult.created).getTime() <
1000
) {
promise = Promise.resolve();
} else {
promise = dispatch(getTokenGrouped(...args));
}
return promise.then(() =>
dispatch(
fn(...args.concat(selectTokenValue(getState())))
)
);
};
const getData1WithToken = needsToken(getData1);
const getData2WithToken = needsToken(getData2);
//selectors
const selectData1 = (state) => state.data1;
const selectData2 = (state) => state.data2;
const selectToken = (state) => state.token;
const selectTokenValue = createSelector(
[selectToken],
//SO snippet has no optional chaining, should just return token?.value
(token) => token && token.value
);
//creating store with redux dev tools
const composeEnhancers =
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(
reducer,
initialState,
composeEnhancers(
applyMiddleware(
//simple thunk middleware
({ dispatch, getState }) =>
(next) =>
(action) =>
typeof action === 'function'
? action(dispatch, getState)
: next(action)
)
)
);
const Data1 = React.memo(function Data1({ refresh }) {
const data = useSelector(selectData1);
const dispatch = useDispatch();
React.useEffect(() => {
dispatch(getData1WithToken());
}, [dispatch, refresh]);
return <div>{data}</div>;
});
const Data2 = React.memo(function Data2({ refresh }) {
const data = useSelector(selectData2);
const dispatch = useDispatch();
React.useEffect(() => {
dispatch(getData2WithToken());
}, [dispatch, refresh]);
return <div>{data}</div>;
});
const App = () => {
const [refresh, setRefresh] = React.useState({});
return (
<div>
{/* getting data in one render cycle many times */}
<Data1 refresh={refresh} />
<Data2 refresh={refresh} />
<Data1 refresh={refresh} />
<Data2 refresh={refresh} />
<Data1 refresh={refresh} />
<Data2 refresh={refresh} />
<button onClick={() => setRefresh({})}>
refresh
</button>
</div>
);
};
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
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>
<script src="https://cdnjs.cloudflare.com/ajax/libs/redux/4.0.5/redux.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-redux/7.2.0/react-redux.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/reselect/4.0.0/reselect.min.js"></script>
<script src="https://unpkg.com/immer@7.0.5/dist/immer.umd.production.min.js"></script>
<div id="root"></div>
说明:
在您看到 const
的任何地方添加 export
所以 export const
或 export default
,您可以从任何其他文件中导入它。
createGroupedThunkAction
接收 getToken
thunk 并返回存储在 getTokenGrouped
中的 thunk。
在一次渲染期间多次调用 getTokenGrouped
时(Data1 和 Data2 具有这样做的效果),它将共享获取该渲染的令牌,当它解析时,它将删除缓存,因为createInvalidateOnResolveCache
中使用的缓存类型。因此,无论您在渲染期间分派多少次,都不会在一次渲染期间获取多个令牌。
needsToken
函数将接收一个 thunk(getData1 和 getData2)并返回一个 thunk,如果没有当前令牌或令牌早于一个,它将通过调度 getTokenGrouped
自动获取一个令牌第二个(我编造的使令牌无效的逻辑)。此令牌存储在 state 中并传递给 getData1
和 getData2
,以便他们可以使用该令牌。
我建议在运行示例时打开 redux devtools,以便您可以看到正在调度的操作。通常使用 async 时,您会为异步操作调度 beforeFetch、afterFetch 或 faildFetch,但为了简单起见,我省略了这一点。
您可以尝试使用 createGroupedThunkAction
将 getData1
和 getData2
分组作为练习,这样也不会对这些进行不必要的提取。