I'm using the react-native-wifi-reborn package to get a list of all nearby wifi points. How do I get the list out of the promise? I've looked at one solution and it uses a class, but where my app calls the function to get the list, it isn't in one.
const MainScreen = ({navigation}) => {
requestFineLocationPermission();
WifiManager.setEnabled(true);
let wifiList = WifiManager.reScanAndLoadWifiList().then((data) => {return data});
console.log(wifiList);
return (
<Layout style={styles.container}>
......
</Layout>
);
}
export default MainScreen;
When wifiList is logged, the output is {"_U": 0, "_V": 0, "_W": null, "_X": null}
This is a case of a React functional component, which is an simpler way to define a React components compared to using classes. See more info in the docs.
In your case, your component not being updated and re-rendered with the new wifiList data, because in your current code, you are not updating the component state at all:
let wifiList = WifiManager.reScanAndLoadWifiList().then((data) => {
return data
});
In this case, you are returning data result in a promise, but you are not using it to update the component state, and you aren't using await neither to retrieve the promise result. So you are actually assigning a Promise reference to wifiList, not the promise data result.
To correctly update the state on a functional component, you can use the React hook useState().
The result would look something like:
const MainScreen = ({navigation}) => {
requestFineLocationPermission();
const [wifiList, setWifiList] = useState(/* initialValue */); // you may provide an initial value (optional, defaults to undefined)
WifiManager.setEnabled(true);
WifiManager.reScanAndLoadWifiList().then((data) => {
// update the state here
setWifiList(data);
});
// this will log the initialValue 'undefined' the first time,
// then after state is is updated,
// it will log the actual wifi list data, resolved by 'WifiManager.reScanAndLoadWifiList()'
console.log(wifiList);
return (
<Layout style={styles.container}>
......
</Layout>
);
}
export default MainScreen;
Related
All articles I have read on promises show examples with console.log - I am using AWS Athena and want to display the result on the webpage in my React export. The react export does not allow the use of .then. So I need to resolve the promise to an external variable.
client is a aws athena client which returns a promise I need to resolve.
async function getResult(){
try {
return await client.send(command);
} catch (error) {
return error.message;
}
}
export default getResult()
I want to display the result in App.js
render()
{
return (
{ athena }
)
It displays in the console but not on the webpage as the page is loaded before the variable is resolved.
More complete example of App.js
import athena from './athena';
class App extends Component {
render()
{
let athena_result = athena.then(function(result) {
console.log(result)
}
)
return ( athena_result )
Causes Error
Error: Objects are not valid as a React child (found: [object Promise])
The render method of all React components is to be considered a pure, synchronous function. In other words, there should be no side effects, and no asynchronous logic. The error Error: Objects are not valid as a React child (found: [object Promise]) is the component attempting to render the Promise object.
Use the React component lifecycle for issuing side-effects. componentDidMount for any effects when the component mounts.
class App extends Component {
state = {
athena: null,
}
componentDidMount() {
athena.then(result => this.setState({ athena: result }));
}
render() {
const { athena } = this.state;
return athena;
}
}
If you needed to issue side-effects later after the component is mounted, then componentDidUpdate is the lifecycle method to use.
Class components are still valid and there's no plan to remove them any time soon, but function components are really the way going forward. Here's an example function component version of the code above.
const App = () => {
const [athenaVal, setAthenaVAl] = React.useState(null);
React.useEffect(() => {
athena.then(result => setAthenaVAl(result));
}, []); // <-- empty dependency array -> on mount/initial render only
return athenaVal;
}
The code is a little simpler. You can read more about React hooks if you like.
You can use a state, and just set the state to the response's value when it's done:
const Component = () => {
const [athena, setAthena] = useState(""); // Create a state with an empty string as initial value
// Send a request and on success, set the state to the response's body, and on fall set the state to the error message
useEffect(() => client.send(command).then((response) => setAthena(response.data)).catch((error) => setAthena(error.message)), []);
return <>{athena}</>;
};
I have a stateless functional component which has no props and populates content from React context. For reference, my app uses NextJS and is an Isomorphic App. I'm trying to use React.memo() for the first time on this component but it keeps re-rendering on client side page change, despite the props and context not changing. I know this due to my placement of a console log.
A brief example of my component is:
const Footer = React.memo(() => {
const globalSettings = useContext(GlobalSettingsContext);
console.log('Should only see this once');
return (
<div>
{globalSettings.footerTitle}
</div>
);
});
I've even tried passing the second parameter with no luck:
const Footer = React.memo(() => {
...
}, () => true);
Any ideas what's going wrong here?
EDIT:
Usage of the context provider in _app.js looks like this:
class MyApp extends App {
static async getInitialProps({ Component, ctx }) {
...
return { globalSettings };
}
render() {
return (
<Container>
<GlobalSettingsProvider settings={this.props.globalSettings}>
...
</GlobalSettingsProvider>
</Container>
);
}
}
The actual GlobalSettingsContext file looks like this:
class GlobalSettingsProvider extends Component {
constructor(props) {
super(props);
const { settings } = this.props;
this.state = { value: settings };
}
render() {
return (
<Provider value={this.state.value}>
{this.props.children}
</Provider>
);
}
}
export default GlobalSettingsContext;
export { GlobalSettingsConsumer, GlobalSettingsProvider };
The problem is coming from useContext. Whenever any value changes in your context, the component will re-render regardless of whether the value you're using has changed.
The solution is to create a HOC (i.e. withMyContext()) like so;
// MyContext.jsx
// exported for when you really want to use useContext();
export const MyContext = React.createContext();
// Provides values to the consumer
export function MyContextProvider(props){
const [state, setState] = React.useState();
const [otherValue, setOtherValue] = React.useState();
return <MyContext.Provider value={{state, setState, otherValue, setOtherValue}} {...props} />
}
// HOC that provides the value to the component passed.
export function withMyContext(Component){
<MyContext.Consumer>{(value) => <Component {...value} />}</MyContext.Consumer>
}
// MyComponent.jsx
const MyComponent = ({state}) => {
// do something with state
}
// compares stringified state to determine whether to render or not. This is
// specific to this component because we only care about when state changes,
// not otherValue
const areEqual = ({state:prev}, {state:next}) =>
JSON.stringify(prev) !== JSON.stringify(next)
// wraps the context and memo and will prevent unnecessary
// re-renders when otherValue changes in MyContext.
export default React.memo(withMyContext(MyComponent), areEqual)
Passing context as props instead of using it within render allows us to isolate the changing values we actually care about using areEqual. There's no way to make this comparison during render within useContext.
I would be a huge advocate for having a selector as a second argument similar to react-redux's new hooks useSelector. This would allow us to do something like
const state = useContext(MyContext, ({state}) => state);
Who's return value would only change when state changes, not the entire context.
But I'm just a dreamer.
This is probably the biggest argument I have right now for using react-redux over hooks for simple apps.
I've had major issues the last few days getting state information into the custom contentComponent of my createDrawerNavigator. I've decided to make a few pieces of content global such as user ID using the state of app.js and just passing the state as screenProps to the router like so.
App.js
state = {
score:0,
rank:0,
}
setScore = (value) => this.setState({score:value});
setRank = (value) => this.setState({rank:value});
render() {
const globalProps={
state:this.state,
setScore:this.setScore,
setRank:this.setRank
}
let RootNav = createRootNavigator();
return (
<RootNav screenProps={globalProps}></RootNav>
);
Router.js
contentComponent: ({ navigation, screenProps}) => (
<DrawerContent navigation={navigation} screenProps={screenProps}/>
),
Child.js
this.props.screenProps.setScore(5);
I'm able to access the data, but when I call to setState from the child I get the warning telling me that app.js is unmounted. My understanding was that app.js was always mounted and running because it contains your entire app? if anyone has a solution for this it'd be greatly appreciated.
I think you're getting this error because you're setting your state in the render method of your component. You would need to move it up into a lifecycle method like componentDidMount(). See the example below of your refactored code.
state = {
score:0,
rank:0,
}
setScore = (value) => this.setState({score:value});
setRank = (value) => this.setState({rank:value});
componentDidMount(){
const globalProps={
state:this.state,
setScore:this.setScore,
setRank:this.setRank
}
}
render() {
let RootNav = createRootNavigator();
return (
<RootNav screenProps={globalProps}></RootNav>
);
The componentDidMount() runs everytime the component is called or data/state changes. I think setting your state in the render() is not good practice. i havent actually tested this code but if it doesnt work you could try sending more code snippets.
I know, there are many, many similary questions.. **duplicate alarm!**
But: I looked through all of them, I promise. I'm quite sure now, that this is another case, that could have to do with the props being an object (from what I've read here). But I couldn't solve the following, anyway:
class CsvListDropdown extends Component {
constructor(props) {
super(props);
this.state = { sessions: props.sessions }
this csvsInSession = this.csvsInSession.bind(this);
}
csvsInSession(sessions) {
return (sessions
.map(keys => Object.entries(keys)[2][1])
.map((csv, i) => (
<option value={csv} key={i}>{csv}</option>
))
)
}
render() {
const { isLoading } = this.props
if (isLoading) { blablabla.. }
else {
return (
...
<select value={this.props.sessions[0].currentCsv}>
{this.csvsInSession(this.state.sessions)}
</select>
...
)
}
}
}
export default withTracker(() => {
const handle = Meteor.subscribe('sessions');
return {
isLoading: !handle.ready(),
sessions: Sessions.find({}).fetch()
};
})(CsvListDropdown);
Now from the client I am writing another document into the Sessions collection, containing the .csv filename, while this new csv file is being uploaded to a remote server. console.log(this.props.sessions) gives me an array, which is up to date. But the component itself does not re-render.
What I also don't understand is: console.log(this.state.sessions) returns undefined. (note: state)
What I tried so far:
{this.csvsInSession(this.props.sessions)} (note: props)
Adding a withTracker / State / Props to the parent component and passing the sessions object from either state or props as params to the child component, that should re-render.
forceUpdate()
componentWillUpdate()
What may be important as well: The component should re-render about the same time another component also re-renders (which displays the contents of uploaded CSVs, that return from a microservice and get written into another collection). The latter does actually re-render.. But that dropdown does not.. argh!
this.state will only change if you call this.setState(), which you are not doing. You are initializing state with a value from props, but only in the constructor when the component is first instantiated. After that, even if props changes your component may re-render but what it displays won't change because state hasn't been updated.
In fact, there does not appear to be any reason whatsoever to store data in state in that component. It might as well be a functional presentational component:
function CsvListDropdown(props) {
function csvsInSession(sessions) {
return (sessions
.map(keys => Object.entries(keys)[2][1])
.map((csv, i) => (
<option value={csv} key={i}>{csv}</option>
))
)
}
const { isLoading } = props;
if (isLoading) { blablabla.. }
else {
return (
...
<select>
{csvsInSession(props.sessions)}
<select>
...
)
}
}
Generally all of your components should be stateless functional components unless they specifically need to store internal state for some reason.
Now I finally solved it, and it turns out that the component did actually update at any time, but I did not notice it, simply because the latest item in the array was quietly appended to the bottom of the dropdown list. This however I was not expecting, as I had published the collection with a descending sorting.
// server-side
Meteor.publish('sessions', function() {
return Sessions.find({ userId: this.userId }, { sort: {createdAt: -1} });
});
Server-side seems to be the wrong place to sort. It simply does not have an effect. So sorted on the client side, when subscribing:
// client-side
export default withTracker(() => {
const handle = Meteor.subscribe('sessions');
return {
isLoading: !handle.ready(),
sessions: Sessions.find({}, { sort: {createdAt: -1} }).fetch()
};
})(App)
I had omitted an important detail from my question, that is how I set the value of the dropdown field:
<select value={this.props.sessions[0].currentCsv}>
{this.csvsInSession(sessions)}
</select>
So lesson learned: If you think your react component does not re-render, always check if that's true, before assuming so.
As a side effect of debugging I restructered my components. Now the Meteor.subscribe() is within the parent component, that contains all the children, that have to handle the sessions object. And the sessions object gets passed down from the parent to the (grand)children as props. I think it's more readable and easier to maintain that way.
I have a component which gets a collection of items as props and maps them to a collection of components which are rendered as children of a parent component. We use images stored in WebSQL as byte arrays. Within the map function I get an image Id from the item and make an async call to the DAL in order to get the byte array for the image. My problem is that I cannot propagate the promise in to React, since it was not designed to deal with promises in rendering (not as far as I can tell anyway). I come from a C# background, so I guess I'm looking for something like the await keyword for resync-ing branched code.
The map function looks something like this (simplified):
var items = this.props.items.map(function (item) {
var imageSrc = Utils.getImageUrlById(item.get('ImageId')); // <-- this contains an async call
return (
<MenuItem text={item.get('ItemTitle')}
imageUrl={imageSrc} />
);
});
and the getImageUrlById method looks like this:
getImageUrlById(imageId) {
return ImageStore.getImageById(imageId).then(function (imageObject) { //<-- getImageById returns a promise
var completeUrl = getLocalImageUrl(imageObject.StandardConImage);
return completeUrl;
});
}
This doesn't work, but I don't know what I need to modify to make this work. I tried adding another promise to the chain, but then I get an error because my render function return a promise instead of legal JSX. I was thinking that maybe I need to leverage one of the React life-cycle methods to fetch the data, but since I need the props to already be there, I can't figure out where I can do this.
render() method should render UI from this.props and this.state, so to asynchronously load data, you can use this.state to store imageId: imageUrl mapping.
Then in your componentDidMount() method, you can populate imageUrl from imageId. Then the render() method should be pure and simple by rendering the this.state object
Note that the this.state.imageUrls is populated asynchronously, so the rendered image list item will appear one by one after its url is fetched. You can also initialize the this.state.imageUrls with all image id or index (without urls), this way you can show a loader when that image is being loaded.
constructor(props) {
super(props)
this.state = {
imageUrls: []
};
}
componentDidMount() {
this.props.items.map((item) => {
ImageStore.getImageById(item.imageId).then(image => {
const mapping = {id: item.imageId, url: image.url};
const newUrls = this.state.imageUrls.slice();
newUrls.push(mapping);
this.setState({ imageUrls: newUrls });
})
});
}
render() {
return (
<div>
{this.state.imageUrls.map(mapping => (
<div>id: {mapping.id}, url: {mapping.url}</div>
))}
</div>
);
}
Or you can use react-promise :
Install the package :
npm i react-promise
And your code will look something like so :
import Async from 'react-promise'
var items = this.props.items.map(function (item) {
var imageSrc = Utils.getImageUrlById(item.get('ImageId')); // <-- this contains an async call
return (
<Async promise={imageSrc} then={(val) => <MenuItem text={item.get('ItemTitle')} imageUrl={val}/>} />
);
});
Edit: Oct 2019
The last build of react-promise provide a hook called usePromise:
import usePromise from 'react-promise';
const ExampleWithAsync = (props) => {
const {value, loading} = usePromise<string>(prom)
if (loading) return null
return <div>{value}</div>}
}
Full docs: react-promise