React dangerouslySetInnerHTML error causing promise rejection in Redux - javascript

I have a React component set up to display content from a CMS, where I use dangerouslySetInnerHTML. I have a standard fetch, request, receive pattern set up with a thunk to handle the async action.
The action creator looks like this:
export const fetchPage = (slug) => {
return (dispatch) => {
dispatch(requestPage());
return fetch(`${config.api}/pages/?slug=${slug}`)
.then((response) => {
const status = response.status;
switch (status) {
case 200:
return response.json();
case 401:
return Promise.reject(status);
default:
return Promise.reject(status);
}
})
.then(json =>
dispatch(receivePage({
slug,
json,
}))
).catch(err =>
dispatch(receivePageError(err)));
};
};
When I first created the React component, I forgot to pass to dangerouslySetInnerHTML the object in format { __html: content } and just passed it content, giving me the invariant violation. What confuses me is that this error bubbled up into the redux async flow and caused a dispatch of receivePageError. Why did this error show up here, rather than in the React component itself?

The error has been caught by the Promise's catch. The code example will console log the exception.
const callAnError = () => {
throw Error('Error');
}
const callAnErrorParent = () => {
callAnError();
}
const callAnErrorGrandparent = () => {
callAnErrorParent();
};
Promise.resolve().then(() => {
callAnErrorGrandparent();
}).catch((error) => {
console.log(error);
});
https://jsfiddle.net/nfyeu5co/1/
This is great resource for understanding promises
https://github.com/getify/You-Dont-Know-JS/blob/master/async%20%26%20performance/ch3.md

Related

Add Async/Await to React Fetch Request

I'm making a fetch request to this API and I'm successfully getting the data and printing it to the console. However I'm new to asynchronous calls in Javascript/React. How do add async/await in this code to delay the render upon successful fetch? I'm getting the classic Uncaught (in promise) TypeError: Cannot read property '0' of undefined because I believe that the DOM is trying to render the data that hasn't been fully fetched yet.
import React, { useEffect, useState } from "react";
export default function News() {
const [error, setError] = useState(null);
const [isLoaded, setIsLoaded] = useState(false);
const [stories, setStory] = useState([]);
useEffect(() => {
fetch(
"http://api.mediastack.com/v1/news"
)
.then((res) => res.json())
.then(
(result) => {
setIsLoaded(true);
setStory(result);
console.log(result.data[0]); // printing to console to test response
console.log(result.data[0].title); // printing to console to test response
},
(error) => {
setIsLoaded(true);
setError(error);
}
);
}, []);
if (error) {
return <div>Error: {error.message}</div>;
} else if (!isLoaded) {
return <div>Loading...</div>;
} else {
return (
<div>
<p>{stories.data[0].title} </p> // this is the where render error is
</div>
);
}
}
The problem is that your isLoaded state variable is updated BEFORE stories, despite the fact you set the former state first. Here is how to fix this:
import { useEffect, useState } from "react";
export default function App() {
const [error, setError] = useState(null);
const [stories, setStory] = useState(null);
useEffect(() => {
fetch("your_url")
.then((res) => return res.json())
.then((result) => {
setStory(result);
console.log("Success ", result);
})
.catch((error) => {
console.log("Error", error);
});
}, []);
if (error) {
return <div>Error: {error.message}</div>;
} else if (!stories) {
return <div>Loading...</div>;
} else {
return (
<div>
<p>stories.data[0].title} </p>
</div>
);
}
}
Get rid of the isLoaded var altogether and use the stories var to indicate that it's being loaded.
If you want to add artificial load time to your api call. You can do something like this:
useEffect(() => {
fetch("your_url")
.then((res) => return res.json())
.then((result) => {
setTimeout(() => setStory(result), 2000)
})
.catch((error) => {
console.log("Error", error);
});
}, []);
This will add 2 seconds before setting your state thus letting you see what your loader looks like.
async/await is just another form to retrieve asynchronous data, like you are doing with then.
The message:
Cannot read property '0' of undefined
means that 'result.data' is undefined.
Anyway, if it entered the "then" callback it always means that the request was fetched, there is no such thing as "half fetched" or "fully fetched".
I suggest you to use the debugger, by placing
debugger;
right in the beginning of the 'then' callback to ensure what your result is.
or you may also console.log the result.
Just to clarify:
myFunctionThatReturnsPromise.then(response => //do something with response)
is the same as
await response = myFunctionThatReturnsPromise;
You might consider using stories?.data?.[0]?.title to fix this problem.
your error will not be gone with async await because you are calling a nested empty object which has no value. render method in react is prior to the useEffect you have used. you can approach two solutins:
1.use optional chaining es20 feature like this:
<p>{stories?.data?.[0]?.title} </p>
2.use nested if statement before the p tag to check whether it has data or not:
it seems the first option is much cleaner
You already have async code using Promise.then so async/await will not help you here - it's just a different way of writing Promise-based functionality.
Your problem is just as you say - React is trying to render your else block before stories has anything for you to render. I think you just need to write stricter conditions, e.g.
if (error) {
return <div>Error: {error.message}</div>;
}
if (!isLoaded || !stories.data.length) {
return <div>Loading...</div>;
}
return (
<div>
<p>{stories.data[0].title} </p> // this is the where render error is
</div>
);

How to Cancel subscription in async promise to avoid memory leak in Reactjs

Within my React component, I have an async request which dispatches an action to my Redux store which is called within the useEffect hook:
const loadFields = async () => {
setIsLoading(true);
try {
await dispatch(fieldsActions.fetchFields(user.client.id));
} catch (error) {
setHasError(true);
}
setIsLoading(false);
}
useEffect(() => { if(isOnline) { loadFields() } }, [dispatch, isOnline]);
The action requests data via a fetch request:
export const fetchFields = clientId => {
return async dispatch => {
try {
const response = await fetch(
Api.baseUrl + clientId + '/fields',
{ headers: { 'Apiauthorization': Api.token } }
);
if (!response.ok) {
throw new Error('Something went wrong!');
}
const resData = await response.json();
dispatch({ type: SET_FIELDS, payload: resData.data });
} catch (error) {
throw error;
}
}
};
export const setFields = fields => ({
type : SET_FIELDS,
payload : fields
});
When this is rendered within the React app it results in the following warning:
Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in %s.%s, a useEffect cleanup function
I believe this occurs because the promise doesn't have a "clean-up" function. But I am unsure where to place this? Should I have some logic within LoadFields()? Or must this be done within the useEffect hook?
This tutorial which will help you to resolve your issue.
Quick example: with Promises
function BananaComponent() {
const [bananas, setBananas] = React.useState([])
React.useEffect(() => {
let isSubscribed = true
fetchBananas().then( bananas => {
if (isSubscribed) {
setBananas(bananas)
}
})
return () => isSubscribed = false
}, []);
return (
<ul>
{bananas.map(banana => <li>{banana}</li>)}
</ul>
)
}
Quick example: with async/await (Not the best one but that should work with an anonymous function)
function BananaComponent() {
const [bananas, setBananas] = React.useState([])
React.useEffect(() => {
let isSubscribed = true
async () => {
const bananas = await fetchBananas();
if (isSubscribed) {
setBananas(bananas)
}
})();
return () => isSubscribed = false
}, []);
return (
<ul>
{bananas.map(banana => <li>{banana}</li>)}
</ul>
)
}
First issue
If your useEffect() fetches data acynchronously then it would be a very good idea to have a cleanup function to cancel the non-completed fetch. Otherwise what could happen is like that: fetch takes longer than expected, meantime the component is re-rendered for whatever reason. Maybe because its parent is re-rendered. The cleanup of useEffect runs before re-render and the useEffect itself runs after re-render. To avoid having another fetch inflight it's better to cancel the previous one. Sample code:
const [data, setData] = useState();
useEffect(() => {
const controller = new AbortController();
const fetchData = async () => {
try {
const apiData = await fetch("https://<yourdomain>/<api-path>",
{ signal: controller.signal });
setData(apiData);
} catch (err) {
if (err.name === 'AbortError') {
console.log("Request aborted");
return;
}
}
};
fetchData();
return () => {
controller.abort();
}
});
Second issue
This code
return async dispatch => {
will not work because neither dispatch nor Redux store support async actions. The most flexible and powerful way to handle this issue is to use middleware like redux-saga. The middleware lets you:
dispatch 'usual' sync actions to Redux store.
intercept those sync actions and in response make one or several async calls doing whatever you want.
wait until async call(s) finish and in response dispatch one or several sync actions to Redux store, either the original ones which you intercepted or different ones.

Using React.setState in componentDidMount() for data returned within nested promises?

I'm trying to put some data into state in a React app. The flow involves fetching a list of IDs from the HackerNews API, then taking each ID and making an additional API call to fetch the item associated with each ID. I ultimately want to have an array of 50 items in my component state (the resulting value of the each '2nd-level' fetch.
When I setState from JUST the single 'top-level' promise/API call, it works fine and my state is set with an array of IDs. When I include a second .then() API call and try to map over a series of subsequent API calls, my state gets set with unresolved Promises, then the fetch() calls are made.
I'm sure this a problem with my poor grasp on building appropriate async methods.
Can someone help me figure out what I'm doing wrong, and what the best practice for this is??
My component:
import React from 'react'
import { fetchStoryList } from '../utils/api'
export default class Stories extends React.Component {
state = {
storyType: 'top',
storyList: null,
error: null,
}
componentDidMount () {
let { storyType } = this.state
fetchStoryList(storyType)
.then((data) => {
console.log("data", data)
this.setState({ storyList: data })
})
.catch((error) => {
console.warn('Error fetching stories: ', error)
this.setState({
error: `There was an error fetching the stories.`
})
})
}
render() {
return (
<pre>{JSON.stringify(this.state.storyList)}</pre>
)
}
}
My API Interface:
// HackerNews API Interface
function fetchStoryIds (type = 'top') {
const endpoint = `https://hacker-news.firebaseio.com/v0/${type}stories.json`
return fetch(endpoint)
.then((res) => res.json())
.then((storyIds) => {
if(storyIds === null) {
throw new Error(`Cannot fetch ${type} story IDs`)
}
return storyIds
})
}
function fetchItemById(id) {
const endpoint = `https://hacker-news.firebaseio.com/v0/item/${id}.json`
return fetch(endpoint)
.then((res) => res.json())
.then((item) => item)
}
export function fetchStoryList (type) {
return fetchStoryIds(type)
.then((idList) => idList.slice(0,50))
.then((idList) => {
return idList.map((id) => {
return fetchItemById(id)
})
})
//ABOVE CODE WORKS WHEN I COMMENT OUT THE SECOND THEN STATEMENT
You are not waiting for some asynchronous code to "finish"
i.e.
.then((idList) => {
return idList.map((id) => {
return fetchItemById(id)
})
})
returns returns an array of promises that you are not waiting for
To fix, use Promise.all
(also cleaned up code removing redundancies)
function fetchStoryIds (type = 'top') {
const endpoint = `https://hacker-news.firebaseio.com/v0/${type}stories.json`;
return fetch(endpoint)
.then((res) => res.json())
.then((storyIds) => {
if(storyIds === null) {
throw new Error(`Cannot fetch ${type} story IDs`);
}
return storyIds;
});
}
function fetchItemById(id) {
const endpoint = `https://hacker-news.firebaseio.com/v0/item/${id}.json`
return fetch(endpoint)
.then(res => res.json());
}
export function fetchStoryList (type) {
return fetchStoryIds(type)
.then(idList => Promise.all(idList.slice(0,50).map(id => fetchItemById(id)));
}
One solution would be to update fetchStoryList() so that the final .then() returns a promise that is resolved after all promises in the mapped array (ie from idList.map(..)) are resolved.
This can be achieved with Promise.all(). Promise.all() take an array as an input, and will complete after all promises in the supplied array have successfully completed:
export function fetchStoryList(type) {
return fetchStoryIds(type)
.then((idList) => idList.slice(0,50))
.then((idList) => {
/* Pass array of promises from map to Promise.all() */
return Promise.all(idList.map((id) => {
return fetchItemById(id)
});
});
}

Redux-Thunk: async dispatch is not working

I am trying to build an app in react native that is suppose to take take two inputs by a user and then make a query to an api and get information about the two inputs. I have been having trouble with redux and redux-thunk and specifically with async actions.
This is the code in my app that i am specifically having trouble with
export const fetchData = url => {
console.log("start Fetching");
return async dispatch => { // this is where the problem is
dispatch(fetchingRequest());
try {
const response = await fetch("https://randomuser.me/api/?results=10");
const json = await response.text();
if (response.ok) {
dispatch(fetchingSuccess(json));
console.log("JSON", json);
} else {
console.log("fetch did not resolve");
}
} catch (error) {
dispatch(fetchingFailure(error));
}
};
console.log("Fetched data");
};
Upon debugging the function, I have ended with finding that when the fetchData function is called the function will execute but the async dispatch that is being returned has undefined behavior.
The output in the debugger when the function is called should be
start Fetching
JSON file information/Error
but the output in the debugger is actually
start Fetching
Why not simply using Promise syntax?
fetch("https://randomuser.me/api/?results=10")
.then(response => {
if (response.ok) {
// do something
}
})
.catch(e => console.log('fetch did not resolve'));

Throwing a SubmissionError of redux-form causes Uncaught (in promise)

I apologize in advance for the formatting (still a newb on this), and maybe for the stupid question (still a newb on this whole React ecosystem).
I've recently picked up redux-form, and since then I've been trying to use it in the following way:
export const searchPermissions = () => {
return dispatch => {
Axios.get(`${URL}/findPermissions`)
.then(resp => {
console.log(resp.data);
dispatch({ type: PERMISSIONS_SEARCHED, payload: resp.data });
})
.catch(error => {
console.log(error);
throw new SubmissionError({
_error: "Submission error!"
});
});
};
};
And I keep getting the Uncaught error.
Searching through redux-form's github, I saw several similar problems that ended up being solved by adding the return statement (which I think I did correctly) and now I'm kinda lost.
Thanks in advance for any help.
EDIT:
I'm trying to fetch the permissions to display them in 3 combo boxes as soon as the user enters the page. In the component used to fetch the data I have the following code:
componentWillMount() {
this.props.searchPermissions();
}
render() {
return (
<div>
<LayoutGroupForm
onSubmit={this.props.addLayoutGroup}
loadPermissions={this.props.loadPermissions}
visualizationPermissions={this.props.visualizationPermissions}
detailPermissions={this.props.detailPermissions}
resetForm={this.props.resetForm}
/>
</div>
);
}
}
const mapStateToProps = state => ({
loadPermissions: state.layoutGroup.loadPermissions,
visualizationPermissions: state.layoutGroup.visualizationPermissions,
detailPermissions: state.layoutGroup.detailPermissions
});
const mapDispatchToProps = dispatch =>
bindActionCreators(
{
searchPermissions,
addLayoutGroup,
resetForm
},
dispatch
);
And on my reducer I have the following:
case PERMISSIONS_SEARCHED:
return {
...state,
loadPermissions: action.payload.loadPermissions,
visualizationPermissions: action.payload.visualizationPermissions,
detailPermissions: action.payload.detailPermissions
};
For those of you who still encounter this issue (like me), the solution is to add return to your submit handler.
Read more here
https://github.com/erikras/redux-form/issues/2269
Redux Form is expecting the error to come as the error in a rejected promise. Try this:
export const searchPermissions = () => {
return dispatch => {
return Axios.get(`${URL}/findPermissions`)
// ^^^^^^-------------------------------------- Actually return the promise!
.then(resp => {
console.log(resp.data);
dispatch({ type: PERMISSIONS_SEARCHED, payload: resp.data });
})
.catch(error => {
console.log(error);
return Promise.reject(new SubmissionError({
// ^^^^^^^^^^^^^^^^^^^^^^------------------------ Return rejected promise
_error: "Submission error!"
}));
});
};
};

Categories

Resources