How to "bubble up" errors from thrown from redux connect functions? - reactjs

I want my react native app to display error messages with a toast. I want to detect errors within the root level App component using componentDidCatch so I can handle all errors in the same manner.
Currently, if one of my async actions throw an error, mapDispatchToProps can catch it. How do I "bubble" up these errors to my App component?
Alternatively, I could add a redux state for errors and set it on every async error. I can then check for this state in App. It would be cleaner, however, if I could catch all errors in componentDidCatch

Well, here's what I did in my project. I use https://github.com/fkhadra/react-toastify
App.js
import Toaster from './components/Toaster/Toaster';
class App extends Component {
render() {
return (
<div>
<Toaster/>
<Routes />
</div>
);
}
}
export default (App);
Toaster.js
import React, { Component } from 'react';
import { connect } from "react-redux";
import { toast, ToastContainer } from 'react-toastify';
import PropTypes from 'prop-types';
import { toastConstants } from '../../_constants';
const Message = ({ type, content }) => {
let icon = '';
switch(type){
case 'success':
icon = <i className="fa fa-check-circle"></i>;
break;
case 'error':
icon = <i className="fa fa-times-circle"></i>;
break;
case 'info':
icon = <i className="fa fa-info-circle"></i>;
break;
case 'warning':
icon = <i className="fa fa-exclamation-circle"></i>;
break;
default:
icon = '';
break;
}
return (
<div>
{icon} {content}
</div>
);
};
class Toaster extends Component {
componentWillReceiveProps(nextProps) {
if (nextProps.toast.message && nextProps.toast.type) {
toast.dismiss();
switch (nextProps.toast.type) {
case toastConstants.SUCCESS:
toast.success(<Message content={nextProps.toast.message} type="success" />);
break;
case toastConstants.INFO:
toast.info(<Message content={nextProps.toast.message} type="info" />);
break;
case toastConstants.WARN:
toast.warn(<Message content={nextProps.toast.message} type="warning" />);
break;
case toastConstants.ERROR:
toast.error(<Message content={nextProps.toast.message} type="error" />);
break;
default:
break;
}
}
}
render() {
return (
<ToastContainer autoClose={5000} />
);
}
}
function mapStateToProps(state) {
const { toast } = state;
return {
toast
};
}
Message.propTypes = {
type: PropTypes.string,
content: PropTypes.string
};
export default connect(mapStateToProps)(Toaster);
SomeActions.js
function getAll(){
return dispatch => {
dispatch(request());
companyService.getAll()
.then(
response => {
if(response.status === 'fail'){
dispatch(failure(response));
dispatch(toastActions.error(response.message));
}else{
dispatch(success(response));
}
},
error => {
dispatch(toastActions.error(error.toString()));
dispatch(failure(error.toString()));
}
);
}
function request() { return { type: companyConstants.LIST_REQUEST } }
function success(data) { return { type: companyConstants.LIST_SUCCESS, data } }
function failure(error) { return { type: companyConstants.LIST_FAILURE, error } }
}
toastActions.js
import { toastConstants } from '../_constants';
export const toastActions = {
success,
error,
clear
};
function success(message) {
return { type: toastConstants.SUCCESS, message };
}
function error(message) {
return { type: toastConstants.ERROR, message };
}
function clear() {
return { type: toastConstants.CLEAR };
}
toastReducer.js
import { toastConstants } from '../_constants';
const initialState = {
type: toastConstants.CLEAR,
message: null
};
export function toast(state = initialState, action) {
switch (action.type) {
case toastConstants.SUCCESS:
return {
type: toastConstants.SUCCESS,
message: action.message
};
case toastConstants.ERROR:
return {
type: toastConstants.ERROR,
message: action.message
};
case toastConstants.CLEAR:
return {};
default:
return initialState
}
}
Hope its of any use for you!
Cheers.

so the issue is not specific to redux connector functions. in fact, all errors thrown from event handlers will not trigger componentDidCatch. see https://reactjs.org/docs/error-boundaries.html#how-about-event-handlers.
I did not want use some redux error state to capture these errors as that would require more boilerplate. ex: it would force me to connect all my components to redux for error handling, even for components that did not need redux state or would not update state (except for error state). not the idea solution, but to get around this I created a separate function for all my event handlers to use.
//componentEventHandler.js
export function handleEvent () {
const args = Array.from(arguments);
const fn = args.shift();
fn(...args).catch(e => this.setState(() => { throw e }));
}
I then import this function in my components and use as follows.
onPress={handleEvent.bind(this, this.props.signIn, this.state.email, this.state.password)}
now all my child components of App.js will event errors up to App.js

Related

Redux not working on React Native (Invariant Violation)

Error Message:
https://imgur.com/LBNyl2M
App.js
import React, { Component } from 'react';
import { StyleSheet } from 'react-native';
import { Provider } from 'react-redux';
import Layout from './containers/Layout/Layout';
import store from './store/index';
export default function App() {
return (
<Provider store={store}><Layout/></Provider>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
alignItems: 'center',
justifyContent: 'center',
},
});
index.js
import { createStore } from 'redux';
import reducer from './reducer';
const store = createStore(reducer);
export default store;
reducer.js
import * as actionTypes from './actions';
import moment from 'moment';
const initialState = {
itemList: [],
idCount: 0,
text: 'Write your to do item!',
chosenDate: 'no-date',
activeItems: 0,
completedItems: 0,
showCompletedList: false
}
const reducer = (state = initialState, action) => {
switch(action.type){
case actionTypes.ADDTOLIST:
const objItem = { 'id': state.idCount+1, 'text': state.text, 'date': state.chosenDate, 'completed': false, 'displayTextInput': false, 'pickerVisible': false };
return {
...state,
itemList: [...state.itemList, objItem],
idCount: state.idCount+1,
activeItems: state.activeItems+1
}
case actionTypes.REMOVEFROMLIST:
let oldItemList = [...state.itemList];
let index = oldItemList.indexOf(action.item);
if( index !== -1) oldItemList.splice(index, 1);
return {
...state,
itemList: [...oldItemList],
activeItems: action.item.completed ? state.activeItems : state.activeItems-1,
completedItems: action.item.completed ? state.completedItems-1 : state.completedItems
}
case actionTypes.EDITITEMDATE:
oldItemList = [...state.itemList];
index = oldItemList.indexOf(action.item);
if(index !== -1){
oldItemList[index].date = state.chosenDate;
return {
...state,
itemList: [...oldItemList]
}
}
return state;
case actionTypes.EDITITEMSTATUS:
oldItemList = [...state.itemList];
index = oldItemList.indexOf(action.item);
if(index !== -1){
oldItemList[index].completed = !oldItemList[index].completed;
return {
...state,
itemList: [...oldItemList],
activeItems: action.item.completed ? state.activeItems+1 : state.activeItems-1,
completedItems: action.item.completed ? state.completedItems-1 : state.completedItems+1
}
}
return state;
case actionTypes.EDITITEMTEXT:
oldItemList = [...state.itemList];
index = oldItemList.indexOf(action.item);
if(index !== -1){
oldItemList[index] = state.text;
return {
...state,
itemList: [...oldItemList]
}
}
return state;
case actionTypes.TOGGLETEXTINPUT:
oldItemList = [...oldItemList];
index = oldItemList[index].indexOf(action.item);
if(index !== -1){
oldItemList[index],displayTextInput = !oldItemList[index],displayTextInput;
return {
...state,
itemList: [...oldItemList]
}
}
return state;
case actionTypes.FILTERACTIVEITEMS:
return {
...state,
showCompletedList: false
}
case actionTypes.FILTERCOMPLETEDITEMS:
return {
...state,
showCompletedList: true
}
case actionTypes.HANDLECHANGETEXT:
return {
...state,
text: action.text
}
case actionTypes.HIDEPICKERINITEM:
oldItemList = [...state.itemList];
index = oldItemList[index].indexOf(item);
if(index !== -1){
oldItemList[index].isVisible = false;
return {
...state,
itemList: [...oldItemList]
}
}
case actionTypes.SHOWPICKERINITEM:
oldItemList = [...state.itemList];
index = oldItemList[index].indexOf(item);
if(index !== -1){
oldItemList[index].isVisible = true;
return {
...state,
itemList: [...oldItemList]
}
}
return state;
case actionTypes.HANDLEPICKER:
return{
...state,
chosenDate: moment(action.datetime).format('MMM, Do YYYY HH:mm')
}
}
}
export default reducer;
Layout.js
import React, { Component } from 'react';
import { Button, KeyboardAvoidingView } from 'react-native';
import AddScreen from '../../components/AddScreen/AddScreen';
import TodoList from '../../components/TodoList/TodoList';
import Header from '../../components/UI/Header/Header';
class Layout extends Component {
state = {
displayItems: false,
pickerVisible: false
}
toggleItems = () => {
this.setState({ displayItems: !this.state.displayItems });
}
showPicker = () => {
this.setState({ pickerVisible: true });
}
hidePicker = () => {
this.setState({ pickerVisible: false });
}
render () {
let childComponent = <AddScreen
toggleItems={this.props.toggleItems}
showPicker={this.props.showPicker}
hidePicker={this.props.hidePicker}
pickerVisible={this.props.pickerVisible}
/>;
if(this.props.displayItems){
childComponent = <TodoList
itemList={this.state.itemList}
showCompletedList={this.state.showCompletedList}
/>;
}
return (
<KeyboardAvoidingView style={{flex:1}} behavior="padding">
<Header />
{childComponent}
<Button title='Toggle Items' onPress={this.toggleItems} />
</KeyboardAvoidingView>
);
}
}
export default Layout;
my complete project on github: https://github.com/rvmelo/todolist-redux
Is it an error in the code or is it related to some package update?
I get the following error: "Invariant Violation: Element type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: undefined. You likely forgot to export your component from the file it's defined in, or you might have mixed up default and named imports."
You're going to have to do some tracing down on your end. That error typically comes when you try to render a component that doesn't exist (or doesn't exist at the path you've told it it does), which is impossible to tell without having access to most of the project.
In AddScreen, TodoList, and Header make sure they all export default. And if they render any outside components you will need to check the same for them if imported the same way.
One way to debug this would be to remove each component one at a time and see which one makes it break.
-------UPDATE---------
Looking at your project on GitHub, I see that two of the components are named exports.
Change Layout to the following:
import { AddScreen } from '../../components/AddScreen/AddScreen';
import { TodoList } from '../../components/TodoList/TodoList';
Explanation
When you do export myFunction, its a named export. So you must import it with the named import syntax like this:
import { myFunction } from '..'
When you do export default myFunction, its a default export and can be imported as anything, so you don't use the curly braces and instead do this:
import Anything from '...' //path to myFunction

React redux thunk and component rendering too early before actions

I'm sending request to API and then saving the response to array for later use with thunk, but the problem is that my component is calling render function too fast before my action that saves the response from API to array.
The error that I'm getting is "Consider adding an error boundary to your tree to customize error handling behavior". I set "isFetching" bool state accordingly true/false each action but the render function seems to completely ignore if I put like a condition if isFetching is true then return <p>loading</p> in render. The action thats responsible for setting data from API works and sets properly but then its already too late.
Question is how do I delay rendering the component so by the time it renders I already have the data saved to array and ready to work with?
UPDATE WITH CODE:
Action.js:
import axios from "axios";
export let startFetch = () => {
return {
type: "START_FETCH"
}
}
export let endFetch = (array) => {
return {
type: "END_FETCH",
array
}
}
export let fetchApi = () => {
let url = "http://127.0.0.1:5000/api/stats"
return (dispatch) => {
dispatch(startFetch())
return axios.get(url).then(
(response) => {
dispatch(endFetch(response.data))
},
(err) => {
console.log(err);
}
)
}
}
Reducer.js:
export let fetchApiReducer = (state={isFetching : false, array : []},action) => {
switch(action.type){
case 'START_FETCH':
return {
isFetching : true
}
break;
case 'END_FETCH':
return{
isFetching : false,
array : action.array
}
break;
default:
return state;
}
}
Container.js:
import React, {Component} from "react";
import {connect} from "react-redux";
import {bindActionCreators} from 'redux';
import {fetchApi} from "../actions/adsActions"
class AdsList extends Component {
componentWillMount() {
this.props.fetchApi();
}
renderList(ad) {
return (
<a className="list-group-item list-group-item-action flex-column align-items-start">
<div className="d-flex w-100 justify-content-between">
<h5 className="mb-1">{ad.text}</h5>
<small className="text-muted">{ad.date}</small>
</div>
</a>
);
}
render() {
if(this.props.isFetching == true) {
return (<p>Loading</p>);
} else if (this.props.isFetching == false && this.props.array.length >= 1) {
return (
<div>
{this.props.array.map(this.renderList)}
</div>
);
}
}
}
function mapStateToProps(state) {
return {
isFetching: state.isFetching,
array: state.array
};
}
function matchDispatchToProps(dispatch){
return bindActionCreators({fetchApi: fetchApi}, dispatch);
}
export default connect(mapStateToProps, matchDispatchToProps)(AdsList);
Thanks a lot.

Adding events to ReactBigCalendar using redux

I'm currently working on a booking app, and in that regard I need to fetch some data from an API. I'm using react + redux to do this, but I can't seem to get this element BigCalendar to update when the state is updated. BigCalendar has to have some kind of object for it's init process.
BookingReducer.js
import { fetchBookingStart, fetchBookingsSuccess, fetchBookingsError } from '../types.js';
const initialState = {
fetching: false,
fetched: false,
events: [],
error: null
}
export default function (state = initialState, action) {
switch (action.type) {
case fetchBookingStart:
return {
...state,
fetching: true,
fetched: false
}
case fetchBookingsSuccess:
return {
...state,
fetching: false,
fetched: true,
events: action.payload.bookings.length < 0 ? [] : action.payload.bookings.map(B => {
return {
id:22,
title: "testTitle",
description: B.description,
start: B.dateFrom,
end: B.dateTo,
room: B.room,
user: B.user
}
})
}
case fetchBookingsError:
return {
...state,
fetching: false,
error: action.payload.error
}
default:
return state;
}
}
bookingAction.js
import axios from "axios";
import { fetchBookingStart, fetchBookingsSuccess, fetchBookingsError } from '../types.js';
const apiUrl = "http://localhost/api/booking"; //CHANGE FOR PROD!
export const getBookings = () => dispatch => {
let allUrl = apiUrl + "/find/";
dispatch({type: fetchBookingStart});
axios.get(allUrl).then(response => {
console.log(response.data);
dispatch({
type: fetchBookingsSuccess,
payload: response.data
})
}).catch(error => {
dispatch({
type: fetchBookingsError,
payload: error
});
});
}
App.js
import React, { Component } from "react";
import { connect } from 'react-redux';
import { getBookings } from './components/redux/actions/bookingActions';
import Link from "react-router-dom/Link";
import BigCalendar from 'react-big-calendar';
import moment from 'moment';
import 'react-big-calendar/lib/css/react-big-calendar.css';
import "./style.css";
import 'moment/locale/da';
import BookingDialog from './components/booking_create_dialog/BookingDialog';
import Dialog from "material-ui/Dialog/Dialog";
import FlatButton from "material-ui/FlatButton/FlatButton";
let isDialogOpen = false;
moment.locale('da');
BigCalendar.momentLocalizer(moment);
function eventStyleGetter(event, start, end, isSelected) {
let style = { backgroundColor: "" }
switch (event.room) {
case "sal":
style.borderColor = "#781B7F";
style.backgroundColor = "#781B7F";
break;
case "cafe":
style.borderColor = "#067F3D";
style.backgroundColor = "#067F3D";
break;
default:
break;
}
return { style: style };
}
class App extends Component {
componentWillMount() {
this.props.getBookings();
}
componentDidUpdate() {
if (this.props.fetched === true && this.props.fetching === false) {
this.refs.BigCalendar.forceUpdate();
}
}
render() {
return (
<div className="MainContainer">
{/*<Link to="/login"><FlatButton>Login</FlatButton></Link>*/}
<div className="CalendarContainer">
<BigCalendar
ref="BigCalendar"
selectable
className="Calendar"
events={this.props.events}
defaultView="week"
defaultDate={new Date()}
step={60}
eventPropGetter={eventStyleGetter}
/>
</div>
<div className="TestContainer">
<button onClick={() => {
this.props.events.push({
id: 55,
title: "test event",
allDay: true,
start: new Date(),
end: new Date()
}); console.log(this.props.events)
}}> bookigns </button>
{this.props.events.map(E => <h1> {E.room} </h1>)}
</div>
</div>
)
}
}
const mapStateToProps = state => ({
events: state.bookings.events,
fetched: state.bookings.fetched,
fetching: state.bookings.fetching
})
export default connect(mapStateToProps, { getBookings })(App);
Please point out any mistakes that can help me along the right way of doing this.
I was having a similar problem with my react-redux setup and realized my events array of objects wasn't formatted correctly. Other than checking this, I'd also check to see if BigCalendar is getting rendered with anything from this.props.events in the first place. Every time the events prop is changed for BigCalendar it updates itself.

Cannot access nested data from action creator which grabs data from contentful

So i am trying to access state from a component via react-redux's connect. The state is coming from an asynchronous action grabbing data from the Contentful api. I am pretty sure its because i am replacing the article:{} in the state with article: action.article (this action.article has nested data).
Here is the component:
import React, { Component } from 'react';
import ReactMarkDown from 'react-markdown';
import { connect } from 'react-redux';
import { getArticle } from '../../actions/blog.actions';
class ArticlePage extends Component {
constructor(props) {
super(props);
}
componentDidMount() {
this.props.getArticle(this.props.match.params.id)
}
render() {
let {article} = this.props;
return (
<div className='article-page-wrapper'>
<div className='navbar-background'></div>
<div className='article-details'>
<img src={`https:${article}`} />
</div>
<ReactMarkDown className={'main-content'} source={article.blogContent} />
{console.log(this.props.article)}
</div>
)
}
}
function mapStateToProps(state) {
return {
article: state.blogReducer.article
}
}
function mapDispatchToProps(dispatch) {
return {
getArticle: (articleId) => {
dispatch(getArticle(articleId));
}
}
}
export default connect(mapStateToProps, mapDispatchToProps)(ArticlePage);
Here is the action:
export function getArticle (articleId) {
return function (dispatch) {
dispatch(getArticleRequest())
client.getEntries({'sys.id':`${articleId}`,include:1})
.then((article) =>{
console.log(article)
dispatch(getArticleSuccess(article.items[0].fields));
})
.catch((error) => {
dispatch(getArticleError(error));
})
}
}
export function getArticleRequest () {
return {
type: types.GET_ARTICLE_REQUEST
}
}
export function getArticleSuccess (article) {
return {
type: types.GET_ARTICLE_SUCCESS,
article: article
}
}
export function getArticleError (error) {
return {
type: types.GET_ARTICLE_ERROR,
error: error
}
}
Here is the reducer:
import * as types from '../types/blog.types';
const initialState = {
articles:[],
article:[],
error: null,
loading: true
}
export default function blogReducer (state=initialState, action) {
switch(action.type) {
case types.GET_ALL_ARTICLES_REQUEST :
return {...state, loading: true}
case types.GET_ALL_ARTICLES_SUCCESS :
return {...state,loading:false, articles:action.articles.items}
case types.GET_ALL_ARTICLES_ERROR :
return {...state, loading:false, error: action.error}
case types.GET_ARTICLE_REQUEST :
return {...state, loading: true}
case types.GET_ARTICLE_SUCCESS :
return {...state,loading:false,article: action.article}
case types.GET_ARTICLE_ERROR :
return {...state, loading:false, error: action.error}
default :
return state
}
}
Here is the structure of the data being retrieved from Contentful:
So it fails in the ArticlePage when i try and do article.authorImage.fields in the src for the authors image on the article. Here is the error message:
I am pretty sure its because when the empty {} in the state is updated by replacing it with the nested data from getArticle, it isn't setting the newState to the entire payload.
I would appreciate any help you can give, if it is indeed due to nested state and mutations can you provide a way of setting the state to equal the payload of the action.
When setting article in your redux store, you are setting
article: article.items[0].fields
whereas you are trying to access fields from this.props.article.fields, instead you have a fields called authorImage under fields which in turn contains fields key, so either you must use
this.props.articles.authorImage.fields
Also check for undefined property before using it since it may not be present initially and will only be populated on an async Request

Update redux state in two places with one action

I am building a webshop and I´ve run in to some problems. When I click on the buy button I want three things to happen:
Add the product to the redux state (shoppingcartReducer)
Update the sum and quantity in the redux state (shoppingcartReducer)
Render out the sum and quantity in my shoppingCart.js
component
The problem is in the shoppingcartReducer. I dont know how to update the state in two different paths on one action.
return state
.setIn(['products', action.articleNr], addArticle(action, state))
Here I am in the 'products/articleNr' path and manipulating data, but I also wants to update the sum and quantity in './'.
So my alternatives that I thought of are maybe to have some sort of middleware thing in the shoppingcartActions but I don´t really know if this is right, nor how to do it!
I appreciate all the help, thanks!
shoppingcartReducer:
import Immutable from 'seamless-immutable'
import { addToShoppingcart, createShoppingCart } from '../actions/shoppingcartActions'
const ADD_TO_SHOPPINGCART = 'ADD_TO_SHOPPINGCART'
const CREATE_SHOPPING_CART = 'CREATE_SHOPPING_CART'
const initialState = Immutable({
sum: 0,
quantity: 0
})
export default function shoppingcartReducer(state = initialState, action) {
switch (action.type) {
case ADD_TO_SHOPPINGCART:
return state
.setIn(['products', action.articleNr], addArticle(action, state))
case CREATE_SHOPPING_CART:
return state
.set(action.id, createCart(action))
}
return state
}
function addArticle(action, state) {
return {
product: action.product
}
}
function createCart(action) {
return {
id: action.id,
}
}
shoppingcartActions:
let nextTodoId = 0
const ADD_TO_SHOPPINGCART = 'ADD_TO_SHOPPINGCART'
const CREATE_SHOPPING_CART = 'CREATE_SHOPPING_CART'
export function addToShoppingcart(product) {
return {
type: ADD_TO_SHOPPINGCART,
articleNr: product.articleNr,
product: product,
}
}
export function createShoppingCart() {
return {
type: CREATE_SHOPPING_CART,
id: 'productNr:'+nextTodoId++,
}
}
ShoppingCart.js:
import React, { Component } from 'react'
import { bindActionCreators } from 'redux'
import { connect } from 'react-redux'
import * as shoppingcartActions from '../../actions/shoppingcartActions'
class ShoppingCart extends Component {
componentWillMount() {
this.state = {
shoppingcartReducer: []
}
}
componentWillReceiveProps(nextProps) {
this.setState({
shoppingcartReducer: nextProps.shoppingcartReducer ? nextProps.shoppingcartReducer : ''
})
}
render() {
const { shoppingcartReducer } = this.props
const { sum, quantity } = shoppingcartReducer
return (
<div className="shoppingCart">
<ul>
<li>Summa: {sum} :-</li>
<li>Antal varor: {quantity}</li>
</ul>
</div>
)
}
}
function mapStateToProps(state) {
return {
shoppingcartReducer: state.shoppingcartReducer
}
}
function mapDispatchToProps(dispatch) {
return bindActionCreators(shoppingcartActions, dispatch)
}
export default connect(mapStateToProps, mapDispatchToProps)(ShoppingCart)
This worked!
export function addProduct(product) {
return {
type: ADD_PRODUCT,
articleNr: product.articleNr,
product: product,
}
}
export function addSummary(product) {
return {
type: ADD_SUMMARY,
product: product
}
}
export function addToShoppingcart(product) {
return (dispatch) => {
dispatch( addProduct(product))
dispatch( addSummary(product))
}
}

Resources