I have a react-redux application, and I catch every error response from redux-saga, save it in the redux-store and render it in the component. The main problem is the ability to remove it when the component is updated or when the user has moved to another page. I tried to use the component lifecycle methods (componentWillUpdate), but it didn’t work correctly because I had other store parts connected to the component, and when they are updated, it clears the error before the user sees it.
My question is: is there a proper way to store/view/delete errors using the redux?
I handle errors in this way
export function* errorHandler(err: any): SagaIterator {
// put an error in the redux-store
}
// it's a wrapper for every saga
export const genericErrorHandler = (saga: (...args: any[]) => SagaIterator, ...args: any[]) =>
function* handleApp(action: any): any {
try {
yield call(saga, action, args);
} catch (err) {
yield call(errorHandler, err);
}
};
I could probably give a more specific suggestion if we had more details about the design of your app & how you're handling errors. But here are some things that might help:
split up errors by type, with separate actions/reducers/store locations for each. This will help you be specific about what kind of error has occurred and when/how to show each.
fire an action to clear the error state. This ensures you're being specific about what errors are no longer present, and when that change occurs. If possible, don't do this as a side effect of something else happening, be specific about when & why.
if the error state is specific to a UI component, and should be removed from the store when that component goes away, consider firing the error-clearing action in that component's componentWillUnmount method. This is called only when the component is being removed from the DOM.
Related
I am new to Redux and I have encountered this issue and cannot seem to find a solution for it.
I have several API calls inside of thunks and they all work fine. This one however fails. It is different to the others in the sense that I am implementing a search feature that uses payload. I have tried to debug it but can't seem to find where the error happens exactly.
this is the error that I get on the screen:
these are the action, reducer and code in the search component:
and this is the logic to connect the component to the store and access state and actions:
Surely it must be something I am not fully understanding, I hope you can make some sense out of this.
You should change action.loadSearchResults.result to action.searchResults.result. I am assuming there is a key called result you are getting in your response from HomePageApi.searchResults(searchInput).
I know this because in your action you have this code:
export function searchMoviewResultsSuccess(searchResults) {
return {
type: types.SEARCH_RESULTS_SUCCESS,
searchResults: searchResults // This is the key you should get in your reducer not `loadSearchResults`
}
}
Edit: SOLVED! Please see below.
I want my Blog component to fire the fetchBlog action creator every time the browser requests its URL, be it via a link or a refresh. I'd like to do it with the React useEffect Hook and with the React-Redux useDispatch and useSelector Hooks. However, my action only fires when following the link to the page; I do not understand why, even after reading several explanations (like the official docs).
Here is the code:
// Everything duly imported, or else VSC would yell at me
export default function Blog() {
const dispatch = useDispatch();
// slug is set here with useSelector, this always works
useEffect(() => {
dispatch(fetchBlog(slug))
}, [slug, dispatch]);
const blog = useSelector((state) => state.blogs[0]);
// return renders the blog information from the blog constant
// since the action does not fire, blog is undefined because state.blogs is an empty array
}
I know that, on refresh, fetchBlog does not fire because of Redux DevTools and also because I put a debugger there. (And the back-end logs don't show the request coming in.) The action creator itself and the reducer must be working; if they weren't, the page would not load correctly when visited through a link.
Edit: I have determined useSelector and useDispatch are not the root cause of the problem, as changing the code to use connect with mapStateToProps and mapDispatchToProps gives the same result. The issue seems to be with useEffect.
I think the problem is you are returning the call to dispatch. Functions returned from useEffect are clean up functions, so I don't think this would run on mount, or update - only before unmount. Try this:
export default function Blog() {
// ...
// Don't return from useEffect. Just call dispatch within the body.
useEffect(() => {
dispatch(fetchBlog(slug);
}, [slug, dispatch]);
// ...
}
https://reactjs.org/docs/hooks-reference.html#cleaning-up-an-effect
I'd like to clarify what the issue was, which #Trace guided me to finding.
useEffect wasn't being called on refresh because it gets called after the component renders/returns. When refreshing, the state - including the blog data - is lost; instead of returning, a TypeError is thrown because data.title doesn't exist. So useEffect never gets the chance of being called and fetch the blog's content.
The solution to that goes like this:
export default function Blog() {
// ...
useEffect(/* ... */)
const blog = useSelector((state) => state.blogs[0]);
if (!blog) {
return <p>Loading...</p>
}
// return actual blog contents here
}
So now fetchBlog does get called, updating blog and rendering the content.
It isn't clear to me where the slug comes from.
In theory useEffect runs after every render. In case of multiple parameters it will run the callback when one of the array parameters passed in the second argument changes.
Either create a useEffect with empty array as second argument to run it 'once' (e.g. when you refresh) or check the slug value.
Edits after checking the repo:
It's not going to work because useEffect is run AFTER the render (which was included in my answer although someone downvoted it). Dispatching the call will only happen after, or not at all if the exception was thrown before (in this case a nullpointer).
You can get the slug from react-router with match, may be handy for you to know.
export default function Blog({ match }) {
const slug = match.params.slug;
etc
The git repo shows how dispatch as is added as array parameter to useEffect, which is not necessary.
I have a login popup which maps a 'isLoggingIn' boolean to the redux store. When a login request action is dispatched a saga intercepts the action and sends another action that the login is processing, the reducer will take that in and set the 'isLoggingIn' boolean to true.
My store:
export interface AppState {
playerToken:string,
loginOpen: boolean,
loginProcessing: boolean
}
The login saga:
function* loginUser(action: any) {
yield put({ type: (LOGIN + PROCESSING) });
try {
const response = yield call(apiCall, 'api/token', 'POST', { username: action.payload.username, password: action.payload.password });
if (response)
{
yield put({ type: (LOGIN + SUCCESS), payload: response.data });
}
catch ({ statusCode }) {
if (statusCode === 401) {
yield put({ type: (LOGIN + FAIL), payload: { error: "Invalid username or password" } })
}
console.log(statusCode);
}
}
Once the saga is done with the login if there's an error it dispatches an action which the reducer sets to a 'loginError' string in the store and sets the isLoggingIn to false, otherwise isLoggingIn is set to false and the user login id is set which prompts the popup to hide itself (i.e. isVisible={this.props.playerToken == undefined).
This seems insanely complicated but I'm not sure how to break this down using Redux principles. I feel strongly the isProcessingLogin should be part of the components state, but the component has no real idea what's going on after it sends the login attempt event and there's no way for it to ever know unless it's listening on for something in the props.
It gets much worse with the various crud operations which need to happen and the various 'isCreatingXModel' booleans which have to be set to true/false in the store and mapped correctly in components.
Is this how redux is supposed to work or am I over using it in places it doesn't belong?
If this is how redux is supposed to be used what are its benefits exactly? I've read online a lot about things which make sense like having a single point of truth, but they can all be done without the crazy redux bloat, I've read people say not to use redux until you need it but that means I'm going to be doing api calls in two conceptually separate areas of code when redux is integrated whenever I 'need it', finally one of the biggest advantages I see purported by advocates is its ability to rewind and move forward in time, which is great but it won't work in any live application which connects to a database in the backend it manipulates unless as part of rewinding there's an undo last api call action.
Keep in mind that these are all entirely my opinions.
1. You might not need sagas (or thunk or other 'async' redux plugin)
Remember that redux is state management only. The API calls can be written in vanilla javascript with or without redux. For example: here's a basic replication of your flow without sagas:
e.g.
import { setLoadingStatus } from './actions'
import { store } from './reducers' // this is what is returned by a createStore call
export function myApiCall(myUrl, fetchOptions) {
store.dispatch(setLoadingStatus('loading'))
return fetch(myUrl, fetchOptions)
.then((response) => {
store.dispatch(setLoadingStatus('succeeded', data))
// do stuff with response data (maybe dispatch a different action to use it?)
})
.catch((error) => {
store.dispatch(setLoadingStatus('failed', error))
// do stuff
})
}
Note the use of store.dispatch. There's an interesting notion in React-Redux that you can only dispatch actions with mapDispatchToProps, but fortunately, that's not true.
I replaced your multiple actions with one that takes a state and optional data. This'll reduce the number of actions and reducers you need to write, but your action history will be harder to read. (Instead of three distinct actions, you'll only have one.)
2. You might not need redux.
The example function above could look basically identical if you weren't using redux -- imagine replacing the store.dispatch calls with this.setState calls. In the future, when you added it, you'd still have to write all the reducer, action, action creator boilerplate, but it would be only slightly more painful than doing it from the start.
As I said above, I usually go with Redux when working with React the built-in state management is has a bad mental map with any sort of large app.
There are two opposing rules of thumb:
use redux for state that needs to be shared between components
use redux for all state and get a single source of truth
I tend to lean to the second one. I hate hunting down errant pieces of state in the leaves of a large React component tree. This is definitely a question with no "correct" answer.
In my app.component.html I create a custom component (contact-table component) that should get a new value (Account) to present after I update the in the store the connected user to be someone else.
app.component.html:
<contact-table [contacts]="connectedAccount$ |async"></contact-table>
app.component.ts:
export class AppComponent implements OnInit
{
private connectedAccount$: Observable<Account>;
constructor(private store:Store<any>) {
this.connectedAccount$ = this.store
.select(state=> state.connectedAccountReducer.connectedAccount);
this.connectedAccount$
.subscribe(account1=> {
//the app prints any new account that comes up. It's working here.
console.log(account1);
});
}
public ngOnInit() {
this.store.dispatch({
type: updateConnectedAccount,
payload: {
connectedAccount: ACCOUNTS[0]
}
});
}
}
The subscribtion in AppComponent class works great and fires any update that comes up.
The problem is that the async pipe in app.component.html does not send the new account to the contact-table component. I think he doesn't get notified for any updates.
I tried to see what is being sent to the contact-table component,
and I saw he only get one value in the first creation of the contact-table component and its undefined. Thats not possibole becouse my reducer function creates an empty Account object for the initial state of my app.
Is there anything you notice that I missed?
Check and make sure you are using Object.assign to create the changes to your state. It shouldn't return a mutated state. Redux/ngrx and the async pipe detect changes when the object hash changes (or at least this is my understanding). I've run into this problem when I wasn't creating a brand new object but accidentally mutating the existing state and returning it.
To quote the redux site -> http://redux.js.org/docs/basics/Reducers.html
We don’t mutate the state. We create a copy with Object.assign(). Object.assign(state, { visibilityFilter: action.filter }) is also wrong: it will mutate the first argument. You must supply an empty object as the first parameter. You can also enable the object spread operator proposal to write { >...state, ...newState } instead.
I also had a async issue with RC2 so if you aren't using RC3 or above I'd recommend upgrading.
Not saying this is it but it is the most likely candidate from my experience. Hope this helps.
Try using the json pipe just to see if the view is getting anything from that store select.
<span>{{connectedAccount$ |async | json}}</span>
You can include ngrx-store-freeze - the problem with mutable state is easy to address.
Another way of how I often debug the dispatching of Actions is to introduce an Effect for logging purpose. This helps to identify problems as well.
UPDATE: I've narrowed down the issue quite a bit from this first post. please see the latest update. The problem appears to be to do with the size or complexity of the action payload rather than it being because the action is invoked following an async call.
I'm working on a react/redux application and am having a problem using the time travel feature in redux dev tools chrome extension.
When I replay the application in the slider monitor the first async call to a webapi action does not replay. All synchronous actions and async network calls except the first work just fine. Its just the first that doesn't render. I've tried using just redux-thunk for the async, but have also tried it with redux-saga (the current configuration). Im running the application in webpack-dev-server
The application itself is working function (all code is in typescript)
I've tried all kinds of configuration changes, but nothing seems to have any effect. Any ideas would be greatly appreciated.
Heres my configureStore file
function configureStore() {
const sagaMiddleware = createSagaMiddleware()
const store = createStore(rootreducer, compose(
applyMiddleware(invariant(), sagaMiddleware, thunk),
window.devToolsExtension ? window.devToolsExtension() : (f:any) => f
));
if (window.devToolsExtension) window.devToolsExtension.updateStore(store);
sagaMiddleware.run(logsSaga)
return store;
}
export default configureStore;
my saga
function* fetchLogs(logSearchParams: any) {
try {
const data = yield call(getLogTableData,
logSearchParams.params);
yield put({type: "ReceiveLogs",
data, logSearchParams:logSearchParams.params});
} catch (e) {
yield put({type: "LogsError", message: e.message});
}
}
export function* logsSaga() {
yield* takeEvery("RequestLogs", fetchLogs);
}
and the network call
return window.fetch('api/logs/gettable', {
method: 'post',
body: JSON.stringify(logSearchParams),
headers: headers
}).then(r => r.json());
Thanks for any help
EDIT: I'm using Redux-React and the connect decorator to connect Redux with the components. The action is called from an actionCreator
export let searchClicked = () => {
return (dispatch, getState) => {
let params = getSearchParms(getState());
return dispatch({type:'RequestLogs', params});
}
};
This is wired in to the components click handler using React-Redux mapDispatchToProps
Another two components receive the state via mapStateToProps, for example
function mapStateToProps(state) {
return state.logs;
}
When I debug this function isn't invoked when it should be (and is afterwards)
UPDATE:
I've tracked the problem down to a reducer for "ReceiveLogs", which is invoked by Redux-Saga. I have three reducers for this action. If I comment out this line
case "ReceiveLogs":
return {data:action.data.rows, selected:state.selected}
then other components which rely on reducers for this action work correctly and the dev tools replay works as expected. With this line, it fails. The problem appears to be "data:action.data.rows". rows is an array and if I change this to return an empty array, then replay works.
I think I'll give up for today.
UPDATE: It appears that the problem is possibly to do with the size of the array which is sent as part of the ReceiveLogs payload. if I restrict the size of the array by slicing e.g
return {data:action.data.rows.slice(0, 3), selected:state.selected}
then it works. If I include the 4th member of the array, it doesn't work. The 4th member of the array is significantly larger than the others since it has quite a large (and deep) and object included.
Is there some kind of size limit for action payloads and redux-dev-tools??? I'll carry on playing.
Check out Redux Devtools Excessive use of memory and CPU Troubleshooting:
That is happening due to serialization of some huge objects included in the state or action. The solution is to sanitize them.