I have a typescript function that is responsible for fetching data from the NASA api using the React Native fetch function and returning a custom object that has type Item[]. The issue is that the returned object from this function is always [].
The responseData.collection.items worked when originally setting a state variable of type Item[] to it so it is not a mapping issue.
However, I would like this to be a separate typescript function outside of any React components so I cannot use state.
What is the correct way of accessing the fetched JSON response data in typescript without using React Native state?
import { Item } from "../types/data"
export function fetchData(searchTerm: string): Item[] {
let result: Item[] = []
fetch(`https://images-api.nasa.gov/search? q=${searchTerm}&media_type=image`)
.then(response => response.json())
.then((responseData) => {
result = responseData.collection.items
})
.catch((error) => {
console.error(error);
})
return result
}
You can create an async function:
import { Item } from "../types/data"
export async function fetchData(searchTerm: string): Promise<Item[]> {
const response = await fetch(`https://images-api.nasa.gov/search?q=${searchTerm}&media_type=image`)
const responseResult = await response.json()
return result.collection.items as Item[]
}
Now the function returns a promise, so wherever you need the data:
import { fetchData } from './path/to/file.ts'
fetchData(searchTerm)
.then((items: Item[]) => {
// Here you have access to your items and you can do whatever you need with them
})
.catch(err => console.warn(err)
Related
I've recently checked this material (https://thecodebarbarian.com/a-beginners-guide-to-redux-observable.html) and tried something with a fetch request the way it says it's supposed to be, e.g.
import { fetchCharactersAction, setCharactersList, setError } from 'models/actions/characterActions';
import { ofType } from 'redux-observable';
import { concatMap } from 'rxjs/operators';
const fetchCharactersEpic = action$ =>
action$.pipe(
ofType(fetchCharactersAction.type),
concatMap(async ({ payload: { name, status, gender, page } }) => {
const url = `${process.env.REACT_APP_RICKANDMORTY}/character?page=${page}&name=${name}&status=${status}&gender=${gender}`;
const characters = await fetch(url);
const returnedCharacters = await characters.json();
if (returnedCharacters?.error === '' || !returnedCharacters?.error) {
return setCharactersList(returnedCharacters);
}
return setError(returnedCharacters?.error);
})
);
This seems just fine, but when I try to return multiple actions inside an array e.g.
return [setCharactersList(returnedCharacters), action2(), action3(), ...]
it says "actions must be plain objects. use custom middleware for async actions"
The actions I am trying to return are all created with the createAction of redux toolkit, but the weird thing is that when they are returned as single, there is no problem. The problem is caused when trying to return multiple actions.
Any idea?
You need to return an observable that concatMap will merge in, so use return of(setCharactersList(returnedCharacters), action2(), action3(), ...)
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)
});
});
}
I try to display the item name (here the item is an ingredient) after getting it by an axios request. I don't understand what I need to do to use to "return" the item name.
Axios return the name of the item without any problem so I tried to display it with return <p>{response.data.name}</p> but nothing is displayed.
I juste have this message : "Expected to return a value in arrow function"
ListIng is called (props.recipe.ing_list = ["whateverid", "whateverid"]) :
<td><ListIng list={props.recipe.ing_list} /></td>
and I try this to display the name of the item :
const ListIng = props => (
props.list.map((item) => {
axios.get('http://localhost:4000/ingredient/' + item)
.then(response => {
return <p>{response.data.name}</p>
})
.catch(function (error) {
console.log(error)
})
})
)
It's my first post so if there is anything I can improve, don't hesitate to tell me ;-)
You are returning value from .then callback function. Returned value will be passed to nest .then if any, but will not be used as return value from functional component.
As you're using async call, you should use state, to make React know, that data is ready and component should be re-rendered. You can use React Hooks to achieve this like below (not tested, use as hint)
const ListIng = props => {
const [data, setData] = useState([]); // Initial data will be empty array
props.list.map((item) => {
axios.get('http://localhost:4000/ingredient/' + item)
.then(response => {
setData(e => ([...e, response.data.name])); // On each response - populate array with new data
})
.catch(function (error) {
console.log(error)
})
})
// Display resulting array as data comes
return <div>{data.map(d => ({<p>{d}</p>}))}</div>
}
Usually api calls should be inside componentDidMount() lifecycle method.
import React,{Component} from 'react';
const ListIng extends Component {
state={name:null};
componentDidMount() {
this.props.list.map((item) => {
axios.get('http://localhost:4000/ingredient/' + item)
.then(response => {
this.setState({name:response.data.name});
})
.catch(function (error) {
console.log(error)
})
})
}
render() {
return(
<p>{this.state.name}</p>
);
}
};
I have a function, that is fetching data from the backend. When fetch is successful, it extracts one value from the response, and then call another function (parseAllRecordsData), that is converting the value into other value. I'm trying to test this function, but after mocking parseAllRecordsData function, it's still trying to call original function (and throws errors from that function).
In other tests jest.fn or jest.spy is working correctly, but when I'm trying to mock function that is used in "then" it's not.
export function fetchAllRecordsData(payload) {
const url = `/apis/${payload.link.split('apis/')[1]}`;
return axios.get(url)
.then(({ data }) => {
if (data && data._embedded) {
const parsedData = data._embedded['taxonomies:entry'];
const arrayData = parseAllRecordsData(parsedData, payload);
return { data: List(arrayData) };
}
return { data: List([]) };
})
.catch((error) => ({ error }));
}
And my test:
describe('fetchAllRecordsData', () => {
const mockedPayload = {
link: 'apis/ok_link',
};
beforeAll(() => {
jest.spyOn(LegalListRecordsApi,'parseAllRecordsData').mockReturnValue(['test']);
});
it('test', async () => {
const test = await LegalListRecordsApi.fetchAllRecordsData(mockedPayload);
expect(test).toEqual(1);
});
});
When it's called like this, parseAllRecordsData calls real function, and throws the error, because mocked Axios response doesn't have some values that parsing function use. I'm only interested in return value, not calling this function.
jest.spyOn(LegalListRecordsApi,'parseAllRecordsData').mockReturnValue(['test']); mocks the module export for parseAllRecordsData.
This doesn't have any effect on fetchAllRecordsData because it is in the same module as parseAllRecordsData and is calling it directly.
ES6 modules supports cyclic dependencies so you can import a module into itself.
Import the module into itself and use the module to call parseAllRecordsData:
import * as LegalListRecordsApi from './LegalListRecordsApi'; // import module into itself
export function fetchAllRecordsData(payload) {
const url = `/apis/${payload.link.split('apis/')[1]}`;
return axios.get(url)
.then(({ data }) => {
if (data && data._embedded) {
const parsedData = data._embedded['taxonomies:entry'];
const arrayData = LegalListRecordsApi.parseAllRecordsData(parsedData, payload); // use the module
return { data: List(arrayData) };
}
return { data: List([]) };
})
.catch((error) => ({ error }));
}
...and the call will be mocked when you mock the module export for parseAllRecordsData.
export function fetchAllRecordsData(payload, axiosInterface = axios) {
return return axiosInterface.get(url)
. then(({ data }) => {
// your code
})
.catch((error) => ({ error }))
}
So, you need create mock object with method get, method get should return promise.
What's the best approach to using the results of one fetch request to make another fetch request to a different endpoint? How can I confirm the first fetch has completed and setState has happened?
class ProductAvailability extends React.Component {
state = {
store_ids: []
}
componentDidMount() {
fetch(`myapi.com/availability?productid=12345`)
.then((results) => {
return results.json();
})
.then((data) => {
const store_ids = data.result.map((store) => {
return store.store_id
})
this.setState({store_ids: store_ids})
})
/* At this point I would like to make another request
to myapi.com/storedata endpoint to obtain information
on each store in the state.store_ids, and add details
to the state */
}
render() {
return (
<div>
<p>STORE INFO FROM STATE GOES HERE</p>
</div>
)
}
}
When you do setState, it updates the component, so the natural point to read state after you've done a setstate, if you'd have read the docs, is in componentDidUpdate(prevProps, prevState).
I leave you to the doc: https://reactjs.org/docs/react-component.html#componentdidupdate
Attention: Don't use willupdate, it's unsafe as you read in the docs.
A further consideration could be done. If you could avoid to put these data in the state, you could also do everything in the componendDidMount (with promiseall for all the other requests maybe) and then set the state with the old and new data, this is preferable since you update your component only once.
Easier to do with async/await (.then/.catch requires a bit more work -- also, reference to Bluebird's Promise.each function). For clarity, its best to move this out of componentDidMount and into its own class method.
As a side note, if this action is happening for every product, then this secondary query should be handled on the backend. That way, you only need to make one AJAX request and retrieve everything you need in one response.
componentDidMount = () => this.fetchData();
fetchData = async () => {
try {
const productRes = fetch(`myapi.com/availability?productid=12345`) // get product data
const productData = await productRes.json(); // convert productRes to JSON
const storeIDs = productData.map(({store_id}) => (store_id)); // map over productData JSON for "store_ids" and store them into storeIDs
const storeData = [];
await Promise.each(storeIDs, async id => { // loop over "store_ids" inside storeIDs
try {
const storeRes = await fetch(`myapi.com/store?id=${id}`); // fetch data by "id"
const storeJSON = await storeRes.json(); // convert storeRes to JSON
storeData.push(storeJSON); // push storeJSON into the storeData array, then loop back to the top until all ids have been fetched
} catch(err) { console.error(err) }
});
this.setState({ productData, storeData }) // set both results to state
} catch (err) {
console.error(err);
}
}