私はReduxのドキュメントから適応した基本的なサンクアクションクリエーターとレデューサーを持っています: http://redux.js.org/docs/advanced/AsyncActions.html
_// action creator
function fetchPosts () {
return dispatch => {
dispatch({ type: 'FETCH_POSTS_REQUEST' })
return fetch('http://jsonplaceholder.typicode.com/posts')
.then(response => response.json())
.then(json => dispatch({ type: 'FETCH_POSTS_SUCCESS', items: json }))
// THIS CATCHES FETCH REQUEST ERRORS, AND COMPONENT LEVEL ERRORS
.catch(error => dispatch({ type: 'FETCH_POSTS_FAILURE', error: error.message }))
}
}
// reducer
function reducer (state = { isFetching: false, items: [] }, action) {
switch (action.type) {
case 'FETCH_POSTS_REQUEST':
return Object.assign({}, state, { isFetching: true })
case 'FETCH_POSTS_SUCCESS':
return Object.assign({}, state, { isFetching: false, items: action.items })
case 'FETCH_POSTS_FAILURE':
return Object.assign({}, state, { isFetching: false })
default:
return state
}
}
_
小道具として状態が渡されるReactコンポーネントで、投稿アイテムの存在を確認し、存在する場合はコンポーネントレベルのエラーを強制します。
_const Test = props => {
if (!props.items.length) return null
throw new Error('Error!')
}
_
アプリを起動するとき:
fetchPosts
アクションクリエーターが呼び出されますprops.invalidProperty.error
_の読み取りを試みます。Cannot read property 'error' of undefined
_ここまでは順調ですね。
問題は、コンポーネントからのJS例外がコンソールに出力されないことです。代わりに、フェッチプロミスのcatch()
ブロックがエラーをキャッチし、FETCH_POSTS_FAILUREアクションをディスパッチします。
これには、ストアの更新によって影響を受けたコンポーネントのすべてのエラーを飲み込む効果があります。 FETCH_POSTS_FAILURE状態変更がディスパッチされますが、これは正しくないと感じます-実際に投稿をフェッチする際のエラーはありませんでしたが、それらの投稿を使用するコンポーネントのダウンストリームでエラーが発生しました。
非同期リクエストのエラーを、ディスパッチを介して状態を変更した結果として発生する他のランダムエラーから分離するのに役立つパターンを探しています。
編集:
Redux githubリポジトリの非同期の例の例: https://github.com/nandastone/redux/commit/88ab48040ce41c39d8daba8cc0c13a6f32c38adf#diff-eeb827d44ad03655e63b7e9319a03dd4R6
_Promise.catch
_ハンドラーは、解決ハンドラーまたは拒否ハンドラーからスローされたエラーもキャッチします。
_fetch('http://jsonplaceholder.typicode.com/posts').then(res => {
throw new Error();
}).catch(err => {
//will handle errors from both the fetch call and the error from the resolution handler
});
_
fetch
からのエラーのみを処理し、解決ハンドラーでdispatch({ type: 'FETCH_POSTS_SUCCESS', items: json })
の呼び出しによってスローされたエラーが、catch
ハンドラーでキャッチされないようにするには、拒否を添付しますfetch
へのハンドラー。
_return fetch('http://jsonplaceholder.typicode.com/posts').then(response => response.json, error => {
dispatch({ type: 'FETCH_POSTS_FAILURE', error: error.message });
}).then(json => dispatch({ type: 'FETCH_POSTS_SUCCESS', items: json }), error => {
//response body couldn't be parsed as JSON
});
_
fetch
はステータスコード> = 400をエラーとして処理しないため、上記の呼び出しはネットワークエラーまたはCORSエラーがある場合にのみ拒否されます。そのため、解決ハンドラでステータスコードを確認する必要があります。
_function fetchHandler(res) {
if (res.status >= 400 && res.status < 600) {
return Promise.reject(res);
}
return res.json();
}
return fetch('http://jsonplaceholder.typicode.com/posts').then(fetchHandler, error => {
//network error
dispatch({ type: 'NETWORK_FAILURE', error });
}).then(json => dispatch({ type: 'FETCH_POSTS_SUCCESS', items: json }), error => {
dispatch({ type: 'FETCH_POSTS_FAILURE', error: error.message });
});
_
Reactコンポーネントでスローされたエラーは、Reactが矛盾した状態のままになる可能性があるため、後続のrender
sを防ぎ、アプリケーションが応答しなくなることに注意してください。 UIイベント React Fiber は、エラーの境界でこの問題に対処します。
エラーハンドラーを前のthen
ブロックに移動することを検討できます。
私は原理の簡単なデモンストレーションを書きました: https://codepen.io/anon/pen/gWzOVX?editors=0011
const fetch = () => new Promise((resolve) => {
setTimeout(resolve, 100);
});
const fetchError = () => new Promise((resolve, reject) => {
setTimeout(reject, 200)
});
fetch()
.then(() => { throw new Error("error") })
.catch(() => { console.log("error in handler caught") })
fetch()
.then(() => { throw new Error("error") },
() => { console.log("error in handler not caught") })
fetchError()
.then(() => { throw new Error("error") })
.catch(() => { console.log("error in fetch caught 1") })
fetchError()
.then(() => { throw new Error("error") },
() => { console.log("error in fetch caught 2") })
これは、私が作成したフェッチラッパーからのコードのブロックです。 checkStatus
プロミスチェーンにexecuteRequest
が表示されます。ここで、_response.ok
_を使用して2xx以外の応答がないかチェックしています。私のAPIエラーはJSON
を返すため、2xx以外の応答をparseResponse
に渡し、次にreject()
に解析されたエラーデータを拒否してエラーとして返します。 executeRequest
:
_ /**
* Parse a reponse based on the type
* @param {Response} response
* @returns {Promise} <resolve: *, reject: Error>
*/
const parseResponse = (response) => {
const contentType = (response.headers.get('content-type') || '').split(';')[0];
if (contentType === 'application/json') {
return response.json();
} else if (contentType === 'multipart/form-data') {
return response.formData();
} else if (contentType === 'text/html') {
return response.text();
} else if (contentType === 'application/octet-stream') {
return response.blob();
}
};
/**
* Check for API-level errors
* @param {Response} response
* @returns {Promise} <resolve: Response, reject: Error>
*/
const checkStatus = (response) =>
new Promise((resolve, reject) => {
if (response.ok) {
return resolve(response);
}
parseResponse(response)
.then(reject)
.catch(reject);
});
/**
* Create a new Request object
* @param {String} method
* @param {String} route
* @param {*} [data]
* @param {Object} [options]
* @returns {Request}
*/
const buildRequest = (method, route, data = null, definedOptions = {}) => {
const options = Object.assign({}, defaultOptions, validateOptions(definedOptions));
const body = () => data ? { body: options.json ? JSON.stringify(data) : data } : {};
const baseOptions = {
method: method.toUpperCase(),
mode: options.mode,
headers: new Headers(headers(options.headers)),
};
const requestOptions = Object.assign({}, baseOptions, body());
return new Request(getURL(route), requestOptions);
};
/**
* Execute a request using fetch
* @param {String} method
* @param {String} route
* @param {*} [body]
* @param {Object} [options]
*/
const executeRequest = (method, route, body, options) =>
new Promise((resolve, reject) => {
fetch(buildRequest(method, route, body, options))
.then(checkStatus)
.then(parseResponse)
.then(resolve)
.catch(reject);
_