asynchronous code to nested synchronous manner in Meteor/React - javascript

I have been trying to run data fetching code in a container component and pass it to the display one to reduce using hooks and load time. I tried both await/sync and Meteor's wrapAsync Here is its Docs but I admit I dont really understand this function.
N.B: The code is pseudo code that explains the basic structure and functionality I need to apply, everything has been imported properly. The code structure and order is the same as the actual code. Thats what matters
Current Result
display component errors out as undefined because the data or t1Doc are empty
Container async/await
const Container = withTracker(({ id, ...rest }) => {
const t1Handle = Meteor.subscribe("table1");
const t2Handle = Meteor.subscribe("table2");
const isLoading = !t1Handle.ready() && !t2Handle.ready();
const fetchT1Documents = async () => {
const t1Doc = await t1Collection.findOne(query);
let t2Doc;
const fetchT2Documents = async () => {
t2Doc = await t2Collection.findOne(t1Doc.FK);
}
fetchT2Documents();
parseXmltoJs(t1Doc, (err, result) => {
fetchAjaxData(result,t2Doc)
.then((data) => {
return {
isLoading,
t1Doc,
t2Doc,
data
}
})
});
}
fetchT1Documents();
return {
isLoading,
t1,
...rest,
};
})(WithLoading(displayComponent));
Loader
const WithLoading = (Comp) => ({ isLoading, children, ...props }) => {
if (isLoading) {
return <Loader />;
} else {
return (
<Fade in={true} timeout={500}>
<div>
<Comp {...props}>{children}</Comp>
</div>
</Fade>
);
}
};
export { WithLoading };
sorry for the bad editing

A few tips on this one...
You can make a publication that publishes more than one collection/query, eg table1 and table2. It returns a single handle, that is ready when both collections have data.
No need to do an await on the collection.findOne() calls - these are calls to minimongo (client side cache), and will return either null when the subscription is loading, and will return the data otherwise.
The pattern you are using is excellent, and allows separation of display logic from the grubby data layer :)

Related

Saving api response to State using useState and Axios (React JS)

I'm having an issue when trying to save to State an axios API call. I've tried
useState set method not reflecting change immediately 's answer and many other and I can't get the state saved. This is not a duplicate, because I've tried what the accepted answer is and the one below and it still doesn't work.
Here's the (rather simple) component. Any help will be appreciated
export const Home = () => {
const [widgets, setWidgets] = useState([]);
useEffect(() => {
axios
.get('/call-to-api')
.then((response) => {
const data = response.data;
console.log(data); // returns correctly filled array
setWidgets(widgets, data);
console.log(widgets); // returns '[]'
});
}, []); // If I set 'widgets' here, my endpoint gets spammed
return (
<Fragment>
{/* {widgets.map((widget) => { // commented because it fails
<div>{widget.name}</div>;
})} */}
</Fragment>
);
};
Welcome to stackoverflow, first thing first the setting call is incorrect you must use spread operator to combine to array into one so change it to setWidgets([...widgets, ...data]); would be correct (I assume both widgets and data are Array)
second, react state won't change synchronously
.then((response) => {
const data = response.data;
console.log(data); // returns correctly filled array
setWidgets(widgets, data);
console.log(widgets); // <--- this will output the old state since the setWidgets above won't do it's work till the next re-render
so in order to listen to the state change you must use useEffect hook
useEffect(() => {
console.log("Changed Widgets: ", widgets)
}, [widgets])
this will console log anytime widget changes
the complete code will look like this
export const Home = () => {
const [widgets, setWidgets] = useState([]);
useEffect(() => {
axios
.get('/call-to-api')
.then((response) => {
const data = response.data;
setWidgets([...widgets, ...data])
});
}, []);
useEffect(() => {
console.log("Changed Widgets: ", widgets)
}, [widgets])
return (
<Fragment>
{/* {widgets.map((widget) => { // commented because it fails
<div>{widget.name}</div>;
})} */}
</Fragment>
);
};
Try:
setWidgets(data);
istead of
setWidgets(widgets, data);
Your widgets.map() probably fails because there isn't much to map over when the component is being rendered.
You should update it with a conditional like so, just for clarity:
widgets.length>0 ? widgets.map(...) : <div>No results</div>
And your call to setWidgets() should only take one argument, the data:
setWidgets(data)
or if you want to merge the arrays use a spread operator (but then you need to add widgets as the dependency to the useEffect dependency array.
setWidgets(...widgets, ...data)
You might also have to supply the setWidgets hook function to the useEffect dependency array.
Let me know if this helps..

Using React Native Await/Async in main function [duplicate]

I am pretty much familiar with the async await but with back end nodejs. But there is a scenario came across to me where I have to use it on front end.
I am getting array of objects and in that objects I am getting lat lng of the places. Now using react-geocode I can get the place name for a single lat lng but I want to use that inside the map function to get the places names. SO as we know it async call I have to use async await over there.
Here is the code
import Geocode from "react-geocode";
render = async() => {
const {
phase,
getCompanyUserRidesData
} = this.props
return (
<div>
<tbody>
await Promise.all(_.get(this.props, 'getCompanyUserRidesData', []).map(async(userRides,index) => {
const address = await Geocode.fromLatLng(22.685131,75.873468)
console.log(address.results[0].formatted_address)
return (
<tr key={index}>
<td>
{address.results[0].formatted_address}
</td>
<td>Goa</td>
<td>asdsad</td>
<td>{_.get(userRides,'driverId.email', '')}</td>
<td>{_.get(userRides,'driverId.mobile', '')}</td>
</tr>
)
}))
</tbody>
</div>
)
}
But when I use async with the map function here it doesn't return anything. Can anyone please help me where I going wrong?
You should always separate concerns like fetching data from concerns like displaying it. Here there's a parent component that fetches the data via AJAX and then conditionally renders a pure functional child component when the data comes in.
class ParentThatFetches extends React.Component {
constructor () {
this.state = {};
}
componentDidMount () {
fetch('/some/async/data')
.then(resp => resp.json())
.then(data => this.setState({data}));
}
render () {
{this.state.data && (
<Child data={this.state.data} />
)}
}
}
const Child = ({data}) => (
<tr>
{data.map((x, i) => (<td key={i}>{x}</td>))}
</tr>
);
I didn't actually run it so their may be some minor errors, and if your data records have unique ids you should use those for the key attribute instead of the array index, but you get the jist.
UPDATE
Same thing but simpler and shorter using hooks:
const ParentThatFetches = () => {
const [data, updateData] = useState();
useEffect(() => {
const getData = async () => {
const resp = await fetch('some/url');
const json = await resp.json()
updateData(json);
}
getData();
}, []);
return data && <Child data={data} />
}
With the wrapper function below, delayed_render(), you can write asynchronous code inside a React component function:
function delayed_render(async_fun, deps=[]) {
const [output, setOutput] = useState()
useEffect(async () => setOutput(await async_fun()), deps)
return (output === undefined) ? null : output
}
This wrapper performs delayed rendering: it returns null on initial rendering attempt (to skip rendering of this particular component), then asynchronously calculates (useEffect()) the proper rendering output through a given async_fun() and invokes re-rendering to inject the final result to the DOM. The use of this wrapper is as simple as:
function Component(props) {
return delayed_render(async () => { /* any rendering code with awaits... */ })
}
For example:
function Component(props) {
return delayed_render(async () => {
const resp = await fetch(props.targetURL) // await here is OK!
const json = await resp.json()
return <Child data={json} />
})
}
UPDATE: added the deps argument. If your async_fun depends on props or state variables, all of them must be listed in deps to allow re-rendering. Note that passing deps=null (always re-render) is not an option here, because the output is a state variable, too, and would be implicitly included in dependencies, which would cause infinite re-rendering after the async_fun call completes.
This solution was inspired by, and is a generalization of, the Jared Smith's one.

Why my axios get not always working in React

I try to fetch data from blogger api.
const NajnowszeWpisy = () => {
const [articles, setArticles] = useState([]);
useEffect(() => {
axios
.get(
'https://www.googleapis.com/blogger/v3/blogs/****/posts?key=****'
)
.then((res) => {
setArticles(res.data.items);
});
});
Then check if there are any articles (in my blog).
Check if there is any image in blog content, if yes, convey it to props.
if (articles.length) {
console.log('get');
let image = '';
const lastPost = articles[articles.length - 1];
if (lastPost.content.match(/<img\s+[^>]*src="([^"]*)"[^>]*>/i)) {
image = ReactHtmlParser(
lastPost.content.match(/<img\s+[^>]*src="([^"]*)"[^>]*>/i)[0]
);
}
return (
<StyledContainer>
<Post
image={
image ? (
image
) : (
<img src={defaultImage} alt="default-img" />
)
}
title={lastPost.title ? lastPost.title : 'Nowy Artykuł'}
content={lastPost.content}
id={lastPost.id}
key={lastPost.id}
/>
</StyledContainer>
);
} else {
console.log('no get');
}
};
In response i get error GET 429, so there are too many requests.
Also I can't get any of my console.log messages ('get' or 'no get').
Can you tell me if I missed something?
You should try to pass an empty array as second argument to useEffect to run your function only once after initial render:
useEffect(() => {
axios
.get(
'https://www.googleapis.com/blogger/v3/blogs/****/posts?key=****'
)
.then((res) => {
setArticles(res.data.items);
});
}, []);
From React docs:
If you want to run an effect and clean it up only once (on mount and
unmount), you can pass an empty array ([]) as a second argument. This
tells React that your effect doesn’t depend on any values from props
or state, so it never needs to re-run. This isn’t handled as a special
case — it follows directly from how the dependencies array always
works.

How do I pass a value from a promise to a component prop in react native?

Edit: I don't understand the reason for downvotes, this was a good question and no other questions on this site solved my issue. I simply preloaded the data to solve my issue but that still doesn't solve the problem without using functional components.
I'm trying to pass users last message into the ListItem subtitle prop but I can't seem to find a way to return the value from the promise/then call. It's returning a promise instead of the value which gives me a "failed prop type". I thought about using a state but then I don't think I could call the function inside the ListItem component anymore.
getMsg = id => {
const m = fireStoreDB
.getUserLastMessage(fireStoreDB.getUID, id)
.then(msg => {
return msg;
});
return m;
};
renderItem = ({ item }) => (
<ListItem
onPress={() => {
this.props.navigation.navigate('Chat', {
userTo: item.id,
UserToUsername: item.username
});
}}
title={item.username}
subtitle={this.getMsg(item.id)} // failed prop type
bottomDivider
chevron
/>
);
You could only do it that way if ListItem expected to see a promise for its subtitle property, which I'm guessing it doesn't. ;-) (Guessing because I haven't played with React Native yet. React, but not React Native.)
Instead, the component will need to have two states:
The subtitle isn't loaded yet
The subtitle is loaded
...and render each of those states. If you don't want the component to have state, then you need to handle the async query in the parent component and only render this component when you have the information it needs.
If the 'last message' is something specific to only the ListItem component and not something you have on hand already, you might want to let the list item make the network request on its own. I would move the function inside ListItem. You'll need to set up some state to hold this value and possibly do some conditional rendering. Then you'll need to call this function when the component is mounted. I'm assuming you're using functional components, so useEffect() should help you out here:
//put this is a library of custom hooks you may want to use
// this in other places
const useIsMounted = () => {
const isMounted = useRef(false);
useEffect(() => {
isMounted.current = true;
return () => (isMounted.current = false);
}, []);
return isMounted;
};
const ListItem = ({
title,
bottomDivider,
chevron,
onPress,
id, //hae to pass id to ListItem
}) => {
const [lastMessage, setLastMessage] = useState(null);
const isMounted = useIsMounted();
React.useEffect(() => {
async function get() {
const m = await fireStoreDB.getUserLastMessage(
fireStoreDB.getUID,
id
);
//before setting state check if component is still mounted
if (isMounted.current) {
setLastMessage(m);
}
}
get();
}, [id, isMounted]);
return lastMessage ? <Text>DO SOMETHING</Text> : null;
};
I fixed the issue by using that promise method inside another promise method that I had on componentDidMount and added user's last message as an extra field for all users. That way I have all users info in one state to populate the ListItem.
componentDidMount() {
fireStoreDB
.getAllUsersExceptCurrent()
.then(users =>
Promise.all(
users.map(({ id, username }) =>
fireStoreDB
.getUserLastMessage(fireStoreDB.getUID, id)
.then(message => ({ id, username, message }))
)
)
)
.then(usersInfo => {
this.setState({ usersInfo });
});
}
renderItem = ({ item }) => (
<ListItem
onPress={() => {
this.props.navigation.navigate('Chat', {
userTo: item.id,
UserToUsername: item.username
});
}}
title={item.username}
subtitle={item.message}
bottomDivider
chevron
/>
);

Write test to check local setState call with jest and react-testing-library

I am currently using react-testing-library and can't seem to work out how to test setState for components.
In the following example, I am trying to test that the number of items loaded is correct based on the data from the API. Will later on expand this to test things like the interactions between of the items.
Component:
...
componentDidMount() {
this.getModules();
}
getModules () {
fetch('http://localhost:4000/api/query')
.then(res => res.json())
.then(res => this.setState({data : res.data}))
.catch(err => console.error(err))
}
...
render() {
return(
<div data-testid="list">
this.state.data.map((item) => {
return <Item key={item.id} data={item}/>
})
</div>
)
}
Test:
...
function renderWithRouter(
ui,
{route = '/', history = createMemoryHistory({initialEntries: [route]})} = {},) {
return {
...render(<Router history={history}>{ui}</Router>),
history,
}
}
...
test('<ListModule> check list items', () => {
const data = [ ... ]
//not sure what to do here, or after this
const { getByTestId } = renderWithRouter(<ListModule />)
...
//test the items loaded
expect(getByTestId('list').children.length).toBe(data.length)
//then will continue testing functionality
})
I understand this has to do with jest mock functions, but don't understand how to make them work with setting states, or with simulating an API.
Sample Implementation (working!)
With more practice and learning about making components testable, I was able to get this working. Here is a full example for reference: https://gist.github.com/alfonsomunozpomer/de992a9710724eb248be3842029801c8
const data = [...]
fetchMock.restore().getOnce('http://localhost:4000/api/query', JSON.stringify(data));
const { getByText } = renderWithRouter(<ListModule />)
const listItem = await waitForElement(() => getByText('Sample Test Data Title'))
You should avoid testing setState directly since that is an implementation detail of the component. You are on the right path to testing that the correct number of items are rendered. You can mock the fetch function by either replacing window.fetch with a Jest mock function or using the fetch-mock library to handle the heavy lifting for you.
// Note that this method does not build the full response object like status codes, headers, etc.
window.fetch = jest.fn(() => {
return Promise.resolve({
json: () => Promise.resolve(fakeData),
});
});
OR
import fetchMock from "fetch-mock";
fetchMock.get(url, fakeData);

Categories

Resources