React-admin - Cannot update a component (`xxx`) while rendering a different component (`SelectInput`) - react-admin

React-admin 3.8.4
I'm rendering some form fields conditionally, and these fields have some validations. Because of this, I'm receiving this error below:
Warning: Cannot update a component (nameOfTheComponent) while rendering a different component
(SelectInput). To locate the bad setState() call inside SelectInput, follow the stack trace
as described in...
I already have read some explanation about the problem and I've discovered that react-final-form
calls a setState() when registering those fields and this seems to be the issue.
I also saw that there is a fix into FileConfig called silent that solves this problem React final form silent
But I don't know if I'm using wrong, because the warning remains showing up.
I'm trying to do something like this:
const OfferVariation = ({ formData, ...rest }) => {
const form = useForm();
useEffect(() => {
return () => {
const initialState = {}
let inConstructor = true
const fieldName = "internalOffer.type"
form.registerField(fieldName, fieldState => {
if (inConstructor) {
initialState[fieldName] = fieldState
} else {
this.setState({ [fieldName]: fieldState })
}
}, { silent: true })
}
}, [])
if (flowType === "Interna") {
return (
<SelectInput
source="internalOffer.type"
label="Tipo da Oferta"
choices={offerTypes}
validate={validateArrayNotEmpty}
{...rest}
/>
)
} else if (flowType === "Externa") {
return (
<TextInput
label="Url Externa"
source="externalOffer.externalURL"
{...rest}
/>
)
}
}
};
export default OfferVariation;
Does anyone know how to fix it and could help me?

Related

Referencing JSON object fields after assigning to variable in useEffect()

I'm attempting to retrieve a JSON object from an API call, and then set a useState variable to the response for use in my app. I am able to successfully receive the JSON response, but if I try to reference specific fields I get an error saying
null is not an object (evaluating data.type). I understand that this happens because initially the data variable is simply null, but I'm not sure the best way to go about preventing this and I suspect I'm doing something fundamentally wrong.
Here's the function which queries the API and retrieves the response data:
export function searchFlightOffers(postData) {
return getAmadeusToken().then(data => {
const config = {
headers: {
'Authorization': `Bearer ${data.access_token}`
}
}
return axios.post("https://test.api.amadeus.com/v2/shopping/flight-offers", postData, config).then(data => data);
});
}
Then the React Native function which uses it
export default function FlightScreen() {
const [flightResponse, setFlightResponse] = useState(null);
useEffect(() => {
searchFlightOffers(postData).then(data => setFlightResponse(data.data));
}, [])
console.log("TEST: ", flightResponse.type);
Is there a more efficient way I should be doing this?
EDIT: To add hopefully a more clear description of the issue, here's the JSX I'm trying to use this data in:
return (
<ScrollView style={{marginTop: 220}}>
{
flightResponse != null ? flightResponse.map(data => {
return (
<FlightCard
key={data.id}
data={data}
onPress={() => {}}
/>
)
}) : false
}
</ScrollView>
If I add a conditional which checks to see if the flightResponse data is null and then map the data if not then this works just fine. Removing the conditional and just leaving it like this:
return (
<ScrollView style={{marginTop: 220}}>
{
flightResponse.map(data => {
return (
<FlightCard
key={data.id}
data={data}
onPress={() => {}}
/>
)
})
}
</ScrollView>
Leaves me with this error: null is not an object (evaluating 'flightResponse.map') .
While technically the conditional is a solution to my problem there must be a cleaner way to handle this no?
Update: A solution to this problem is to change
const [flightResponse, setFlightResponse] = useState(null);
to
const [flightResponse, setFlightResponse] = useState([]);
and then I can remove the conditional from the JSX.
My apology that I didn't see the additional info you have put there. Apparently you have resolved this issue on your own by adding the check for null, which, to my knowledge, is the correct usage pattern.
You have to check for null because the code in useEffect is not guaranteed to complete (because it is async) before react native executes the code to render the components. By checking for null, you place a guard on this exact situation (and other situations, such as error during fetching data, data itself is empty, etc.).
Original answer (obsolete)
Try rewrite your searchFlightOffers function in async/await style. This way, it is clearer what object is returned. I suspect your current version does not return the data.
The rewrite can be something like this (untested, use with caution).
export const searchFlightOffers = async (postData) => {
try {
const token = await getAmadeusToken();
} catch (err) {
// handle error during token acquisition
console.log(err);
}
const config = {
headers: {
'Authorization': `Bearer ${token.access_token}`
}
}
try {
const flightOfferData = await axios.post(
"https://test.api.amadeus.com/v2/shopping/flight-offers",
postData,
config,
);
return flightOfferData;
} catch (err) {
// handle error during flight offer acquisition
console.log(err);
}
}

(react native chat) Is there any way to use the global variable independently?

Screenshot
In the banner above the chat, there are two buttons that temporarily turn off the banner and always turn it off. When the user presses the always off button, I hope it applies only to the chat room, but maybe because it is a global variable, it affects other chat rooms. Is there a way to use the global variable independently for each chat?
I thought about making it a key value object and using the id value of the chatroom as the key value, but there is no way to get the id value because I have to allocate it as a global variable.
// ChatScreen
...
<InfoBanner postId={postId} errandInfo={errandInfo} />
<View style={styles.contents}>
<GiftedChat
messages={messages}
...
// InfoBanner
let alwaysOption = true
export default InfoBanner = (props) => {
const { postId, errandInfo } = props;
const [bannerVisible, setBannerVisible] = useState(true)
return (
<Banner
visible={alwaysOption && bannerVisible}
style={styles.banner}
actions={[
{
label: 'turn off',
color: 'black',
onPress: () => setBannerVisible(false),
},
{
label: 'always turn off',
color: 'black',
style: {justifyContent: 'center'},
onPress: () => {alwaysOption=false; setBannerVisible(false)},
},
]}
>
...
We could create a custom "hook" that implements a global application cache. We can decide later whether we want to store the chats that should show a banner or not, along with the user ID, on a server or in the phone's local storage.
I will implement a solution using the phone's local storage. It might be useful to store this information in a global cache once it is loaded. I will use Vercel's SWR and react-native-async-storage.
The hook: useChatBanner
import AsyncStorage from '#react-native-async-storage/async-storage';
import useSWR from 'swr'
const keyTemplate = `chat/banner/`
const useChatBanner = (postId) => {
const {data, error, mutate} = useSWR(postId ? `${keyTemplate}${postId}` : null, () => {
const value = await AsyncStorage.getItem(`${keyTemplate}${postId}`).then(response => response === "true" ? true : false)
return value
})
const setBannerVisible = React.useCallback((id, visible) => {
const newData = visible ? "true" : "false"
mutate(newData, false)
AsyncStorage.set(`${keyTemplate}${postId}`, new).then(() => {
mutate(newData)
})
}, [data, mutate])
return {
isVisible: data,
isLoading: !error && !data,
isError: error,
setBannerVisible
}
}
We need to an additional config wrapper component around our main application for SWR.
import { SWRConfig } from "swr"
export default const App = () => {
return <SWRConfig>
// main entry point, navigator code or whatever
</SWRConfig>
}
Then you can use it as follows in your InfoBanner component.
export default InfoBanner = (props) => {
const { postId, errandInfo } = props;
const [bannerVisible, setBannerVisible] = useState(true)
const { isLoading, isVisible } = useChatBanner(postId)
if (isLoading) {
// might be useful to show some loading indicator here
return null
}
return (
<Banner
visible={isVisible && bannerVisible}
...
I have included a setter function in the hook in case you want to allow the user to enable the global flag again.
Using the above, it is possible to hold a boolean flag (or any other data) globally for each postId. Notice that we can implement a similar behavior using the Context API.
If you have a server and an user management, then it is very simple to adapt the above code. Replace the async storage calls with your API and you are good to go. If we manage this using userId's rather then postIds, then the data would be an array and we can filter for postIds to retrieve the correct boolean value.

React Native hooks - correct use of useEffect()?

I'm new to hooks and ran across this setup on SO and wanted to confirm that this is the correct pattern. I was getting the RN "unmounted component" leak warning message before and this seemed to solve it. I'm trying to mimic in some way compnentDidMount. This is part of a phone number verify sign up flow and onMount I want to just check for navigation and then fire off a side effect, set mounted true and then unmount correctly.
const SMSVerifyEnterPinScreen = ({ route, navigation }) => {
const [didMount, setDidMount] = useState(false)
const { phoneNumber } = route.params
useEffect(() => {
if(navigation) {
signInWithPhoneNumber(phoneNumber)
setDidMount(true)
}
return () => setDidMount(false)
}, [])
if (!didMount) { return null }
async function signInWithPhoneNumber(phoneNumber) {
const confirmation = await auth().signInWithPhoneNumber('+1'+phoneNumber)
...
}
return (
...
)
}
RN 0.62.2 with react-nav 5 - thanks!
Since signInWithPhoneNumber is a async function and will setState you will see warning it the component is unmounted before the response is available
In order to handle such scenarios you can keep a variable to keep track whether its mounted or not and then only set state is the mounted variable is true
However you do not need to return null if component has unmounted since that doesn't accomplish anything. The component is removed from view and will anyways not render anything.
Also you do not need to maintain this value in state, instead use a ref
const SMSVerifyEnterPinScreen = ({ route, navigation }) => {
const isMounted = useRef(true)
const { phoneNumber } = route.params
useEffect(() => {
if(navigation) {
signInWithPhoneNumber(phoneNumber)
}
return () => {isMounted.current = false;}
}, [])
async function signInWithPhoneNumber(phoneNumber) {
const confirmation = await auth().signInWithPhoneNumber('+1'+phoneNumber)
...
}
return (
...
)
}

Force refetch in Relay Modern RefetchContainer with no (new) variables

I'm trying to find out how/if it is possible to trigger a refresh in a Relay Modern RefreshContainer without passing (new) variables?
I’m looking for the best way to implement the good ol’ pull-to-refresh on a React Native list, that should simply refetch the original query - no variables needed?
According to docs (https://facebook.github.io/relay/docs/api-cheatsheet.html) this should be possible using
this.props.relay.refetch({}, callback, {force: true})
but I get an error saying "undefined is not an object ('evaluating taggedNode.modern')"
The query works just fine if I use a plain old FragmentContainer instead, but I'd just like a simple pull-to-refresh functionality :-)
EDIT
Adding more code for clarity. Also updated call to reflect change to API that includes render variables, passing null
class HistoryList extends React.PureComponent<void, Props, State> {
state = { refreshing: false };
_renderRow = ({ item }) => {
return <HistoryListItem item={item.node} />;
};
_renderHeader = ({ section }) => {
return (
<Text style={[cs.breadText, _styles.sectionHeader]}>
{section.title}
</Text>
);
};
_onRefresh = () => {
this.setState({ refreshing: true });
this.props.relay.refetch({}, null, this._onRefreshDone(), { force: true });
};
_onRefreshDone = () => {
this.setState({ refreshing: false });
};
_sortDataIntoSections = (edges: Array<Node>) => {
return _.chain(edges)
.groupBy(element => {
return moment(element.node.huntDate).format('MMMM YYYY');
})
.map((data, key) => {
return { title: key, data: data };
})
.value();
};
render() {
return (
<View style={_styles.container}>
<SectionList
renderSectionHeader={this._renderHeader}
sections={this._sortDataIntoSections(
this.props.entries.allJournalEntries.edges
)}
renderItem={this._renderRow}
keyExtractor={(item, index) => item.node.__id}
onRefresh={this._onRefresh}
refreshing={this.state.refreshing}
/>
</View>
);
}
}
export default createRefetchContainer(
HistoryList,
graphql`
fragment HistoryList_entries on Viewer {
allJournalEntries(orderBy: huntDate_DESC) {
count
edges {
node {
huntDate
...HistoryListItem_item
}
}
}
}
`
);
It seems that the arguments of this.props.relay.refetch has been change to refetch(refetchVariables, renderVariables, callback, options) (in https://facebook.github.io/relay/docs/refetch-container.html) and the cheetsheet has been out-of-date.
I'm not sure that in which version that this has change, but you can give it a try and change your code to:
this.props.relay.refetch({}, null, callback, {force: true})
A solution has been found by robrichard at github.
I was missing the third argument for the RefetchContainer, which is the query to execute on refetch. This, combined with the suggestion from #zetavg was what was needed.
The exported module now looks like this:
export default createRefetchContainer(
HistoryList,
{
entries: graphql`
fragment HistoryList_entries on Viewer {
allJournalEntries(orderBy: huntDate_DESC) {
count
edges {
node {
huntDate
...HistoryListItem_item
}
}
}
}
`
},
graphql`
query HistoryListRefetchQuery {
viewer {
...HistoryList_entries
}
}
`
);
I have both solution applied (query for refetch and relay refetch call).
Refetch query
(do not pay attention at fact, that I didn't specify a component for container, there is special decorator in our code base for it):
{
viewer: graphql`
fragment RatingSchoolsTableContainer_viewer on Viewer {
rating {
schools {
uid
masterUrl
paidOrderSum
paidOrderCount
averageReceipt
}
}
}
`,
},
graphql`
query RatingSchoolsTableContainer_RefetchQuery {
viewer {
...RatingSchoolsTableContainer_viewer
}
}
`,
And relay call:
this.props.relay?.refetch({}, null, () => {}, {force: true})
There is no re-render anyway, but I have new response from server in network.

realm.js - Accessing object of type Contact which has been invalidated or deleted with NO stack trace

Would (really) appreciate help on this one.
I have a realm listView which navigates away to a detailed view.
The detailed view is deleting the entry from the original list view and navigate back. I have registered listeners on the realm DB change to update the content of the list view.
Here is the code I'm using, which, after delete get an exception - "Accessing object of type Contact which has been invalidated or deleted" after the navigation occurs.
Does anyone have an idea why?
Also, it seems that the change listener (updateContactsFromDB) is called twice, while deleting just one object - ideas?
10x
ContactPage.js:
export default class ContactsPage extends Component {
updateContactsFromDB(){
console.log("ContactsPage:updateContactsFromDB()");
let contacts = Realm.objects('Contact');
this.setState({
dataSource: this.state.dataSource.cloneWithRows(contacts.snapshot()),
});
}
constructor(props) {
console.log("ContactsPage:constructor()");
super(props);
const ds = new ListView.DataSource({rowHasChanged: (r1, r2) => (r1 !== r2)});
let contacts = Realm.objects('Contact');
this.state = {
dataSource: ds.cloneWithRows(contacts.snapshot()),
};
this.updateContactsFromDB = this.updateContactsFromDB.bind(this);
}
componentWillMount(props){
console.log("ContactsPage:componentWillMount");
Realm.addListener('change', this.updateContactsFromDB);
}
componentWillUnmount(props){
console.log("ContactsPage:componentWillUnmount");
Realm.removeListener('change', this.updateContactsFromDB);
}
render() {
console.log("ContactsPage:render()");
return (
<ListView
dataSource={this.state.dataSource}
renderRow={(contact) => (
<TouchableOpacity onPress={ () => this.props.navigation.navigate('ContactNotesPage', { contact: contact}) }>
<Text>test Contact</Text>
</TouchableOpacity>
)
}
/>
);
}
}
ContactNotesPage.js:
export default class ContactNotesPage extends Component {
constructor(props) {
console.log("ContactNotesPage:constructor");
super(props);
}
render(){
console.log("ContactNotesPage:render()");
const { params } = this.props.navigation.state;
return (
<TouchableOpacity onPress={ () => {
console.log("ContactNotesPage:delete");
Realm.write(() => { Realm.delete(params.contact);});
this.props.navigation.navigate('ContactsPage');
}
}>
<Text>DeleteContact</Text>
</TouchableOpacity>
)
}
};
// main.js
const MainStack = StackNavigator({
ContactsPage: {
screen: ContactsPage,
},
ContactNotesPage:{
screen: ContactNotesPage,
},
});
export default MainStack;
Seems like it's a bug in realm/react-navigation. when passing a parameter which is a realm object - if you delete this object your next navigation will fail.
A bug is already open at https://github.com/realm/realm-js/issues/1031
This happens when you get an object from realm (using realm.objects('collection')) and use realm.close() function on it. Either use Object.assign method or destructuring object or array approach to mitigate this problem. Like below,
const realmObject = realm.objects('collection');
const obj = [...realmObject] or {...realmObject[0]}; // the later one when filtered is used on realmObject.
My solution was that I did not call realm.close() because I was constantly engaged with the database.
https://realm.io/docs/javascript/0.14.0/api/Realm.html#close
i used useFocusEffect from #react-navigation/native after delete item in array for update list array like this
//state to check get data ability:
const [getDataAbility, setGetDataAbility] = useState(true);
//useFocusEffect
useFocusEffect(
React.useCallback(() => {
if (getDataAbility === true) {
// handle data
}
}, [ dependencies... ]),
);
// catch error ""accessing obj of..."
try {
console.log(pickingItem.id);
} catch (error) {
setGetDataAbility(false);
console.log('err picking: ', error);
}
-> solve
This is work for me:
async function updateRealm() {
const realm = Realm.open({
schemas: [
/*myschemas*/
]
});
realm.addListener('change', () => {
/* consult function */
});
}