How to translate errors which occurs in redux-saga? - react-redux

I have a question, how to translate errors which are catch in redux-saga. How are you doing it in React-Redux?
I am trying to do it like this:
catch (error) {
if(error.response.payload.error==='Unauthorized'){
Alert.error(<FormattedMessage {...messages.unauthorizedMessage} />, {
html: false,
});
}

In the saga catch block, dispatch an action:
// saga:
catch(error) {
if(error.response.payload.error==='Unauthorized'){
yield put({
type: 'UNAUTHORIZED_REQUEST',
message: messages.unauthorizedMessage
})
}
}
Then in a reducer, update state based on the action:
const requestError = (state = null, { type, message }) => {
case 'UNAUTHORIZED_REQUEST':
return message;
default:
return state;
}
Then, have a component render the error message to reflect the updated state:
let MyComponent = ({ requestError }) => {
return (
<div>
{requestError &&
<MyErrorMessage message={requestError} />
}
{/* ... */}
</div>
)
}
MyComponent = connect(
state => ({
requestError: state.requestError
})
)(MyComponent)

Related

Vue 3 components not awaiting for state to be loaded

I am having some trouble using fetch in vuex to build state before rendering my page's components.
Here is the page component code:
async beforeCreate() {
await this.$store.dispatch('projects/getProjects');
},
And this is the state code it's dispatching:
async getProjects(context: any, parms: any) {
context.commit("loadingStatus", true, { root: true });
console.log("1");
await fetch(`${process.env.VUE_APP_API}/projects?`, {
method: "get",
headers: {
Authorization: `Bearer ${localStorage.getItem("token")}`,
},
})
.then((response) => {
console.log("2");
if (!response.ok) {
throw new Error(response.status.toString());
} else {
return response.json();
}
})
.catch((error) => {
// todo: tratamento de erros na UI
console.error("There was an error!", error);
})
.then((data) => {
context.commit("setProjects", { data });
console.log("3");
// sets the active project based on local storage
if (
localStorage.getItem(
`activeProjectId_${context.rootState.auth.operator.accountId}`
)
) {
console.log("setting project to storage");
context.dispatch("selectProject", {
projectId: localStorage.getItem(
`activeProjectId_${context.rootState.auth.operator.accountId}`
),
});
} else {
//or based on the first item in the list
console.log("setting project to default");
if (data.length > 0) {
context.dispatch("selectProject", {
projectId: data[0].id,
});
}
}
context.commit("loadingStatus", false, { root: true });
});
},
async selectProject(context: any, parms: any) {
console.log("4");
context.commit("loadingStatus", true, { root: true });
const pjt = context.state.projects.filter(
(project: any) => project.id === parms.projectId
);
if (pjt.length > 0) {
console.log("Project found");
await context.commit("setActiveProject", pjt[0]);
} else if (context.state.projects.length > 0) {
console.log("Project not found setting first on the list");
await context.commit("setActiveProject", context.state.projects[0]);
} else {
await context.commit("resetActiveProject");
}
await context.commit("loadingStatus", false, { root: true });
},
I've added this console.log (1, 2, 3, 4) to help me debug what's going on.
Right after console.logging "1", it starts to mount the components. And I only get logs 2, 3 and 4 after all components have been loaded.
How can I make it so that my components will only load after the whole process is done (i.e. after I log "4") ?
If your beforeCreate hook (or any client hooks) contains async code, Vue will NOT wait to it then render and mount the component.
The right choice here should be showing a loader when your data is fetching from the server. It will provide better UX:
<template>
<div v-if="!data"> Loading... </div>
<div v-else> Put all your logic with data here </div>
</template>
<script>
export default {
data() {
return {
data: null
}
},
async beforeCreate() {
this.data = await this.$store.dispatch('projects/getProjects');
},
}
</script>

Providing two combined Reducers for my redux saga store prevents my websocket channel message from triggering, but only one does not?

Configured my store this way with redux toolkit for sure
const rootReducer = combineReducers({
someReducer,
systemsConfigs
});
const store = return configureStore({
devTools: true,
reducer: rootReducer ,
// middleware: [middleware, logger],
middleware: (getDefaultMiddleware) => getDefaultMiddleware({ thunk: false }).concat(middleware),
});
middleware.run(sagaRoot)
And thats my channel i am connecting to it
export function createSocketChannel(
productId: ProductId,
pair: string,
createSocket = () => new WebSocket('wss://somewebsocket')
) {
return eventChannel<SocketEvent>((emitter) => {
const socket_OrderBook = createSocket();
socket_OrderBook.addEventListener('open', () => {
emitter({
type: 'connection-established',
payload: true,
});
socket_OrderBook.send(
`subscribe-asdqwe`
);
});
socket_OrderBook.addEventListener('message', (event) => {
if (event.data?.includes('bids')) {
emitter({
type: 'message',
payload: JSON.parse(event.data),
});
//
}
});
socket_OrderBook.addEventListener('close', (event: any) => {
emitter(new SocketClosedByServer());
});
return () => {
if (socket_OrderBook.readyState === WebSocket.OPEN) {
socket_OrderBook.send(
`unsubscribe-order-book-${pair}`
);
}
if (socket_OrderBook.readyState === WebSocket.OPEN || socket_OrderBook.readyState === WebSocket.CONNECTING) {
socket_OrderBook.close();
}
};
}, buffers.expanding<SocketEvent>());
}
And here's how my saga connecting handlers looks like
export function* handleConnectingSocket(ctx: SagaContext) {
try {
const productId = yield select((state: State) => state.productId);
const requested_pair = yield select((state: State) => state.requested_pair);
if (ctx.socketChannel === null) {
ctx.socketChannel = yield call(createSocketChannel, productId, requested_pair);
}
//
const message: SocketEvent = yield take(ctx.socketChannel!);
if (message.type !== 'connection-established') {
throw new SocketUnexpectedResponseError();
}
yield put(connectedSocket());
} catch (error: any) {
reportError(error);
yield put(
disconnectedSocket({
reason: SocketStateReasons.BAD_CONNECTION,
})
);
}
}
export function* handleConnectedSocket(ctx: SagaContext) {
try {
while (true) {
if (ctx.socketChannel === null) {
break;
}
const events = yield flush(ctx.socketChannel);
const startedExecutingAt = performance.now();
if (Array.isArray(events)) {
const deltas = events.reduce(
(patch, event) => {
if (event.type === 'message') {
patch.bids.push(...event.payload.data?.bids);
patch.asks.push(...event.payload.data?.asks);
//
}
//
return patch;
},
{ bids: [], asks: [] } as SocketMessage
);
if (deltas.bids.length || deltas.asks.length) {
yield putResolve(receivedDeltas(deltas));
}
}
yield call(delayNextDispatch, startedExecutingAt);
}
} catch (error: any) {
reportError(error);
yield put(
disconnectedSocket({
reason: SocketStateReasons.UNKNOWN,
})
);
}
}
After Debugging I got the following:
The Thing is that when I Provide one Reducer to my store the channel works well and data is fetched where as when providing combinedReducers I am getting
an established connection from my handleConnectingSocket generator function
and an empty event array [] from
const events = yield flush(ctx.socketChannel) written in handleConnectedSocket
Tried to clarify as much as possible
ok so I start refactoring my typescript by changing the types, then saw all the places that break, there was a problem in my sagas.tsx.
Ping me if someone faced such an issue in the future

State updates but Component doesn't re-render

I'm creating a simple react-redux chat application. I managed to display some dummy data from my redux state in my Message component. I succeed to push a new 'message' to the redux state from my Submit component. But the new item doesn´t render in the Message component.
So I tried to console log the previous state and the new state from the messageReducer and it seems to work. I get the state array with all the dummy data + the new pushed object.
Here is the Github repo if needed: https://github.com/MichalK98/Chat.V.2
// Message Component
import React, { Component } from 'react';
import { connect } from 'react-redux';
class Message extends Component {
render() {
return (
<ul id="chatroom">
{this.props.messages.map((msg) => (
<li className={(msg.username == 'You' ? "chat-me" : "")} key={msg.id}>
<p>{msg.message}</p>
<small>{msg.username}</small>
</li>
)).reverse()}
</ul>
)
}
}
const mapStateToProps = (state) => {
return {
messages: state.message.messages
}
}
export default connect(mapStateToProps)(Message);
// Submit Component
...
import { connect } from 'react-redux';
...
class Submit extends Component {
state = {
message: []
}
clear = async () => {
await this.setState({
message: ''
});
}
handleChange = async (e) => {
await this.setState({
message: e.target.value
});
}
handleSubmit = (e) => {
e.preventDefault();
this.props.writeMessage(this.state.message);
this.clear();
}
render() {
return (
<div className="chat-footer">
<form onSubmit={this.handleSubmit} autoComplete="off">
<input onChange={this.handleChange} value={this.state.message} type="text" placeholder="Skriv något..."/>
<button id="btnSend"><SendSvg/></button>
</form>
</div>
)
}
}
const mapStateToProps = (state) => {
return {
messages: state.messages
}
}
const mapDispatchToProps = (dispatch) => {
return {
writeMessage: (message) => { dispatch({type: 'WRITE_MESSAGE', messages: {id: Math.random(), username: 'You', message: message}})}
}
}
export default connect(mapStateToProps, mapDispatchToProps)(Submit);
// messageReducer
const initState = {
messages: [
{id: 1, username: 'You', message: 'Hi, data from reducer!'},
{id: 2, username: 'Mattias', message: 'Wow..'},
{id: 3, username: 'Alien', message: 'Awesome!'}
]
}
const messageReducer = (state = initState, action) => {
if (action.type === 'WRITE_MESSAGE') {
state.messages.push(action.messages);
console.log('Action ',action.messages);
console.log('State ',state.messages);
}
return state;
}
export default messageReducer;
I expect that the new data will render in my Message component when I add a new object to the state array in messageReducer.
first of all in your Message Component you should Change mapStateToProps :
const mapStateToProps = (state) => {
return {
messages : state.messages
}
}
And then in your message reducer you should change reducer. this is better way for reducer. you shouldn't directly change state :
const messageReducer = (state = initState, action) => {
switch (action.type) {
case "WRITE_MESSAGE":
return { ...state, messages: [...state.messages, { ...action.messages }] }
default:
return state;
}
}
if you need any help i can help you to complete your project :-)

Redux async action triggered after request finished. Why?

I have problem with my async action. I would like to set 'loading' state to true when action fetchPosts() is called and 'loading' state to false when action fetchPostsSuccess() or fetchPostsFailiure().
With my current code it works almost fine except 'loading' state change when fetchPosts() receive response from server and I would like to change this state at the beginning of request.
Here is simple code which shows my steps.
I'm using axios and redux-promise (https://github.com/acdlite/redux-promise).
// actions
export function fetchPosts() {
const request = axios.get(`${API_URL}/posts/`);
return {
type: 'FETCH_POSTS',
payload: request,
};
}
export function fetchPostsSuccess(posts) {
return {
type: 'FETCH_POSTS_SUCCESS',
payload: posts,
};
}
export function fetchPostsFailure(error) {
return {
type: 'FETCH_POSTS_FAILURE',
payload: error,
};
}
// reducer
const INITIAL_STATE = {
posts: [],
loading: false,
error: null,
}
const postsReducer = (state = INITIAL_STATE, action) => {
switch (action.type) {
case 'FETCH_POSTS':
return { ...state, loading: true, error: null };
case 'FETCH_POSTS_SUCCESS':
return { ...state, posts: action.payload, loading: false };
case 'FETCH_POSTS_FAILURE':
return { ...state, posts: [], loading: false, error: action.payload };
default:
return state;
}
}
const rootReducer = combineReducers({
postsList: postsReducer,
});
// store
function configureStore(initialState) {
return createStore(
rootReducer,
applyMiddleware(
promise,
),
);
}
const store = configureStore();
// simple Posts app
class Posts extends Component {
componentWillMount() {
this.props.fetchPosts();
}
render() {
const { posts, loading } = this.props.postsList;
return (
<div>
{loading && <p>Loading...</p>}
<ul>
{posts.map(post => <li key={post.id}>{post.title}</li>)}
</ul>
</div>
);
}
}
const mapStateToProps = state => ({
postsList: state.postsList,
});
const mapDispatchToProps = dispatch => ({
fetchPosts: (params = {}) => {
dispatch(fetchPosts())
.then((response) => {
if (!response.error) {
dispatch(fetchPostsSuccess(response.payload.data));
} else {
dispatch(fetchPostsFailure(response.payload.data));
}
});
},
});
const PostsContainer = connect(mapStateToProps, mapDispatchToProps)(Posts);
// main
ReactDOM.render((
<Provider store={store}>
<Router history={browserHistory}>
<Route path="posts" component={PostsContainer} />
</Router>
</Provider>
), document.getElementById('appRoot'));
Can someone guide me what I'm doing wrong ?
It's turned out the problem is with 'redux-promise' package. This async middleware has no such thing like 'pending' state of promise (called 'optimistic update') .
It changes the state only when promise has been resolved or rejected.
I should use different middleware which allow for 'optimistic updates'
Your problem ís with redux-promise. You should use redux-thunk instead that allows you to return a function and dispatch multiple times. Have a look at it ;)!

Proper way to clear asynchronous work in redux middleware

I have the following middleware that I use to call similar async calls:
import { callApi } from '../utils/Api';
import generateUUID from '../utils/UUID';
import { assign } from 'lodash';
export const CALL_API = Symbol('Call API');
export default store => next => action => {
const callAsync = action[CALL_API];
if(typeof callAsync === 'undefined') {
return next(action);
}
const { endpoint, types, data, authentication, method, authenticated } = callAsync;
if (!types.REQUEST || !types.SUCCESS || !types.FAILURE) {
throw new Error('types must be an object with REQUEST, SUCCESS and FAILURE');
}
function actionWith(data) {
const finalAction = assign({}, action, data);
delete finalAction[CALL_API];
return finalAction;
}
next(actionWith({ type: types.REQUEST }));
return callApi(endpoint, method, data, authenticated).then(response => {
return next(actionWith({
type: types.SUCCESS,
payload: {
response
}
}))
}).catch(error => {
return next(actionWith({
type: types.FAILURE,
error: true,
payload: {
error: error,
id: generateUUID()
}
}))
});
};
I am then making the following calls in componentWillMount of a component:
componentWillMount() {
this.props.fetchResults();
this.props.fetchTeams();
}
fetchTeams for example will dispatch an action that is handled by the middleware, that looks like this:
export function fetchTeams() {
return (dispatch, getState) => {
return dispatch({
type: 'CALL_API',
[CALL_API]: {
types: TEAMS,
endpoint: '/admin/teams',
method: 'GET',
authenticated: true
}
});
};
}
Both the success actions are dispatched and the new state is returned from the reducer. Both reducers look the same and below is the Teams reducer:
export const initialState = Map({
isFetching: false,
teams: List()
});
export default createReducer(initialState, {
[ActionTypes.TEAMS.REQUEST]: (state, action) => {
return state.merge({isFetching: true});
},
[ActionTypes.TEAMS.SUCCESS]: (state, action) => {
return state.merge({
isFetching: false,
teams: action.payload.response
});
},
[ActionTypes.TEAMS.FAILURE]: (state, action) => {
return state.merge({isFetching: false});
}
});
The component then renders another component that dispatches another action:
render() {
<div>
<Autocomplete items={teams}/>
</div>
}
Autocomplete then dispatches an action in its componentWillMount:
class Autocomplete extends Component{
componentWillMount() {
this.props.dispatch(actions.init({ props: this.exportProps() }));
}
if an error happens in the autocomplete reducer that is invoked after the SUCCESS reducers have been invoked for fetchTeams and fetchResults from the original calls in componentWillMount of the parent component and the error will be handled in the Promise.catch of the callApi method that happens in the middleware.
return callApi(endpoint, method, data, authenticated).then(response => {
return next(actionWith({
type: types.SUCCESS,
payload: {
response
}
}))
}).catch(error => {
return next(actionWith({
type: types.FAILURE,
error: true,
payload: {
error: error,
id: generateUUID()
}
}))
});
};
This is because it is happening with in the same tick of the event loop. If I introduce some asynchronicity in the Autcomplete componentWIllMount function then the error is not handled in the Promise catch handler of the middleware
class Autocomplete extends Component{
componentWillMount() {
setTimeout(() => {
this.props.dispatch(actions.init({ props: this.exportProps() }));
});
}
Should I have the callApi function execute on a separate event loop tick?

Resources