How to properly catch HTTP error using react redux saga? - reactjs

When I am working with API call from my front end APP for example ReactJS I am always using a store, I love redux-saga, the good practice with saga is to call your API strictly in your effect handler, it will looks like this:
function mySaga*(){
yield put(loadingRequest);
try{
const response = yield call(axios.get, "https://*******.com");
yield put(saveResponseSomewhereInMyStore(response)
yield put(requestSuccess())
} catch(err){
yield put(requestFailure(err)
}
}
then my store State look like this
interface StoreState{
data:{....};
request:{
login:{
status:"LOADING" | "SUCCESS" | "FAILURE" | "NOT_FIRED",
error:HttpErrorResponse | null
}
}
}
With this approach I have to make a requestStatusSelector and use the hook useSelector every time I need to react to this status inside the view.
Is it the best strategy? For more complex situations I have to useEffect on this selector to trigger an other dispatch, for me it seems little bit dirty.
What are your strategies to handle API response status and error?

Related

Handling OAuth with React 18 useEffect hook running twice

Background
I have recently upgraded a fairly sizeable React app to React 18 and for the most part it has been great. One of the key changes is the new double mount in development causing useEffect hooks to all run twice, this is clearly documented in their docs.
I have read their new effect documentation https://beta.reactjs.org/learn/lifecycle-of-reactive-effects and although it is quite detailed there is a use case I believe I have found which is not very well covered.
The issue
Essentially the issue I have run into is I am implementing OAuth integration with a third-party product. The flow:
-> User clicks create integration -> Redirect to product login -> Gets redirected back to our app with authorisation code -> We hit our API to finalise the integration (HTTP POST request)
The problem comes now that the useEffect hook runs twice it means that we would hit this last POST request twice, first one would succeed and the second would fail because the integration is already setup.
This is not potentially a major issue but the user would see an error message even though the request worked and just feels like a bad pattern.
Considered solutions
Refactoring to use a button
I could potentially get the user to click a button on the redirect URL after they have logged into the third-party product. This would work and seems to be what the React guides recommend (Although different use case they suggested - https://beta.reactjs.org/learn/you-might-not-need-an-effect#sharing-logic-between-event-handlers).
The problem with this is that the user has already clicked a button to create the integration so it feels like a worse user experience.
Ignore the duplicate API call
This issue is only a problem in development however it is still a bit annoying and feels like an issue I want to explore further
Code setup
I have simplified the code for this example but hopefully this gives a rough idea of how the intended code is meant to function.
const IntegrationRedirect: React.FC = () => {
const navigate = useNavigate();
const organisationIntegrationsService = useOrganisationIntegrationsService();
// Make call on the mount of this component
useEffect(() => {
// Call the method
handleCreateIntegration();
}, []);
const handleCreateIntegration = async (): Promise<void> => {
// Setup request
const request: ICreateIntegration = {
authorisationCode: ''
};
try {
// Make service call
const setupIntegrationResponse = await organisationIntegrationsService.createIntegration(request);
// Handle error
if (setupIntegrationResponse.data.errors) {
throw 'Failed to setup integrations';
}
// Navigate away on success
routes.organisation.integrations.navigate(navigate);
}
catch (error) {
// Handle error
}
};
return ();
};
What I am after
I am after suggestions based on the React 18 changes that would handle this situation, I feel that although this is a little specific/niche it is still a viable use case. It would be good to have a clean way to handle this as OAuth integration is quite a common flow for integration between products.
You can use the useRef() together with useEffect() for a workaround
const effectRan = useRef(false)
useEffect(() => {
if (effectRan.current === false) {
// do the async data fetch here
handleCreateIntegration();
}
//cleanup function
return () => {
effectRan.current = true // this will be set to true on the initial unmount
}
}, []);
This is a workaround suggested by Dave Gray on his youtube channel https://www.youtube.com/watch?v=81faZzp18NM

React-boilerplate: Implementing JWT authentication. Attempting to dispatch action from service

Environment:
This is code I'm trying to implement on top of the tried and tested React-boilerplate.
While this boilerplate is wonderful, it doesn't contain authentication. I'm trying to implement my own authentication logic in a way that meshes with the existing logic of the boilerplate.
Context:
I have a service/utility function that is used to send the server requests with a JWT authentication header and return its response.
Goal:
I'm trying to implement logic where when a request is made using an expired token, the access token is automatically refreshed before the request is sent to the server.
What's stopping me:
I have a saga that handles the access token refresh. It works perfectly when it is called from within a container. But because this is a service, it is unaware of the redux store. For this reason I am unable to dispatch actions.
The react-boilerplate structure works by injecting reducers and sagas on the fly. This means I'd need to inject the authentication saga and reducer from within the service (kinda feels wrong no?). However, the saga and reducer injectors are React side-effects and can, therefore, only be used inside of React components.
I feel like the task I'm trying to achieve is quite trivial (and I'm sure it was implementing a million times already), and yet, I can't think of a solution that doesn't seem to go against the entire logic of why use Redux or Sagas to begin with.
Can anyone offer some insights? In the attached image, the red text is the part I'm struggling to implement
See code below:
/**
* Requests a URL, returning a promise
*
* #param {string} url The URL we want to request
* #param {object} [options] The options we want to pass to "fetch".
*
* #return {object} The response data
*/
export default function request( url, options ) {
const token = makeSelectAccessToken();
if (!token) throw new Error('No access token found.');
// Refresh access token if it's expired.
if (new Date(token.expires) - Date.now() <= 0) {
// TODO: Attempt to use refresh token to get new access token before continuing
/** dispatch(REFRESH_ACCESS_TOKEN) **/
// PROBLEM: can't dispatch actions because the store is not exposed to this service.
// Secondary challenge: Can't inject Saga or Reducer because the React-boilerplate injectors are React side-effects.
}
options = {
...options,
Authorization: `Bearer ${token.token}`, // Adding the JWT token to the request
};
return fetch(url, options)
}
There is multiple way to do that
1- Export the store
create new store.js
import { createStore } from 'redux';
import reducer from './reducer';
//you can add your middleware, sagas, ...
const store = createStore(reducer);
export default store;
import it to your service and use it everywhere but you’ll end up with a single store for all of your users
Cons: you’ll end up with a single store for all of your users(if
your app is using Server Side Rendering don't use this method)
Pros: Easy to use
2- You can add your func to middleware and intercept an action
edit your create store code and add new middleware for authorization
const refreshAuthToken = store => next => action => {
const token = makeSelectAccessToken();
if (!token) throw new Error('No access token found.');
if(new Date(token.expires) - Date.now() <= 0) {
// you can dispatch inside store
this.store.dispatch(REFRESH_ACCESS_TOKEN);
}
// continue processing this action
return next(action);
}
const store = createStore(
... //add to your middleware list
applyMiddleware(refreshAuthToken)
);
Cons: Maybe you can need redux-thunk library if this middleware not solve your problem (its easy job. You can't say this even a con)
Pros: Best and safer way for your need and it will work everytime you call an action. It will works like a charm on SSR apps
3- or you can send store as parameter to your request method from component
Cons: Not the best way it will run after your components initialize and you can make easily mistake or can forget to add to your component.
Pros: At least you can do what you want
Maybe a better approach would be to store the token on the localStorage so that you don't need to access the Redux store to obtain it. Just fetch the token and store it this way:
localStorage.setItem('token', JSON.stringify(token))
Later in your service, just retrieve like this:
const token = JSON.parse(localStorage.getItem('token'));
If you don’t feel safe storing the token in the local storage, there's also the secure-ls package, that allows encryption for the data saved to the local storage.

How do I correct official React.js testing recipe to work for React/Jest/Fetch asynchronous testing?

I am trying to learn the simplest way to mock fetch with jest. I am trying to start with the official React docs recipe for fetch but it doesnt actually work without some modification.
My aim is to use Jest and (only) native React to wait for component rendering to complete with useEffect[].fetch() call before running assertions and testing initialisation worked.
I have imported the Data fetching recipe from official docs:
https://reactjs.org/docs/testing-recipes.html#data-fetching
into codesandbox here
https://codesandbox.io/s/jest-data-fetching-ov8og
Result: test fail
expect(received).toContain(expected) // indexOf
Expected substring: "123, Charming Avenue"
Received string: "loading..."
Possible cause?: I suspect its failing because global.fetch is not being used in the component which appears to be using real fetch hence is stuck "loading".
UPDATE
I have managed to make the test work through trial and error. Changed the call to fetch in the actual component to global.fetch() to match the test. This is not desirable for working code to refer to global prefix everywhere fetch is used and is also not in the example code.
e.g.
export default function User(props) {
const [user, setUser] = useState(null);
async function fetchUserData(id) {
// this doesnt point to the correct mock function
// const response = await fetch("/" + id);
// this fixes the test by pointing to the correct mock function
const response = await global.fetch("/" + id);
const json = await response.json();
setUser(json);
}
...
any help or advice much appreciated.

Redux-thunk and redux-promise: empty payload in PENDING state

So I have redux-thunk set up and when I call the updateUser(userInfos) action, it returns a promise to update the server with those infos.
But sequentially I need to update the local state with those infos too, and if I wait for UPDATE_USER_FULFILLED, the effect of my action is not instantaneous, which feels weird for the user.
I thought of reducing the UPDATE_USER_PENDING action to update the local state before the server response arrives with up-to-date data (which should be the same so no re-rendering), but unfortunately, the payload of UPDATE_USER_PENDING action is null :(
Ain't there a way to load it with some data, which would allow instant UI response before server return?
Could be something like:
export function updateUser(user) {
return {
type: UPDATE_USER,
payload: {
promise: userServices.update(user),
localPayload: user,
}
}
}
but I'm guessing this is not available?
For now, the only solution I see is to create a UPDATE_USER_LOCAL action which would do the local action in parallel with the server call. But this is not very elegant and much heavier.
Thanks for the help

React Redux Thunk fetch: execute action based on result of dispatch / fetch

Using React & Redux & Redux-Thunk, trying to make this pseudocode:
// pseudocode
dispatch(makeServiceRequest)
if failed
dispatch(makeIdentityRequest)
if successful
dispatch(makeServiceRequest)
end
end
Want to avoid an infinite loop which would happen if the code was placed inside the .catch block of makeServiceRequest.
I'm using a fetch(url).then(...).catch(...) logic in the dispatch action. fetch does not reject on HTTP status errors.
How do I make this pseudocode happen? What is the correct pattern to handle a situation like this?
Just have a then that checks HTTP status and throws an error if it isn't a success status just like it says in your link:
function checkStatus(response) {
if (response.status >= 200 && response.status < 300) {
return response
} else {
var error = new Error(response.statusText)
error.response = response
throw error
}
}
fetch(url).then(checkStatus).then(...).catch(...);
Other than that, I'm not clear on what your Redux-specific question is.
Edit: based on your comments, I think you are asking how to manage a Redux actions that, when dispatched, can asynchronously dispatch other actions. One way to model that is as a "saga". There's a Redux library called redux-saga that lets you model this sort of thing.

Resources