React Native : update context throw "Cannot update during an existing state transition" - react-native

I have a react native app and i'm trying to update a Date in a custom context.
ThemeContext
export const ThemeContext = React.createContext({
theme: 'dark',
toggleTheme: () => { },
date: new Date(),
setDate: (date: Date) => { }
});
Basic context with the date and the function to update it
App.tsx
export default class App extends React.Component {
constructor(props) {
super(props);
this.state = {
theme: 'dark',
date: new Date()
};
}
render() {
const { theme, date } = this.state;
const currentTheme = themes[theme];
const toggleTheme = () => {
const nextTheme = theme === 'light' ? 'dark' : 'light';
this.setState({ theme: nextTheme });
};
const setDate = (date: Date) => {
// ERROR is raised HERE
this.setState({date: date});
}
return (
<>
<IconRegistry icons={EvaIconsPack} />
<ThemeContext.Provider value={{ theme, toggleTheme, date, setDate }}>
...
</ThemeContext.Provider>
</>
);
}
}
I simply hava a state with a Date and create a setDate function. I wrapped my app into the context provider.
Picker
class PickerContent extends React.Component<{}, ContentState> {
static contextType = ThemeContext;
constructor(props: {} | Readonly<{}>) {
super(props);
let currentMode = 'date';
let show = false;
if (Platform.OS === 'ios') {
currentMode = 'datetime';
show = true;
}
this.state = {
date: new Date(),
mode: currentMode,
show: show
};
}
setMode(currentMode: string) {
this.setState({ mode: currentMode, show: true });
};
onChange(_event: any, selectedDate: Date) {
const currentDate = selectedDate || this.state.date;
// I update the context here
this.context.setDate(currentDate);
this.setState({date: currentDate, show: Platform.OS === 'ios'})
}
render() {
const {show, date, mode } = this.state;
const color = 'dark';
return (
<>
...
{show && <DateTimePicker
...
onChange={(event, selectedDate) => this.onChange(event, selectedDate)}>
</DateTimePicker>}</>
)
}
}
I use the lib 'DateTimePicker' to choose a date and bind the onChange to update the context. This picker is on a modal.
So the warning appears when the onChange function of DateTimePicker is trigger. The error is on the setState of App.tsx (in setDate)
Warning: Cannot update during an existing state transition (such as within 'render'). Render methods should be a pure function of props and state.
Would you know how to correct this error?
EDIT :
I was using a Modal component from the UI Kitten library. I switch to the react native Modal and the error is gone. It seems that the error comes from the Library. SORRY
thank you in advance for your help,
Sylvain

If you're using class component, what you can do is to move the context update fn inside componentDidUpdate. You just compare your states, if your state has changed, you update your context with the new value.
Something along the lines of:
componentDidUpdate(prevProps, prevState) {
if (prevState.date !== this.state.date) {
this.context.setDate(this.state.date);
}
}
onChange(_event: any, selectedDate: Date) {
const currentDate = selectedDate || this.state.date;
this.setState({date: currentDate, show: Platform.OS === 'ios'})
}
This way you're not updating your context during render.
If you refactor this component to functional, you would be able to do the same thing using useEffect.

I edited my question. The error finally seems to come from the library used (ui kitten).

Related

componentDidUpdate not being called

I have a react native app, and I am calling componentDidUpdate on App.js, but it doesn't fire.
I wonder if this is because I am calling from App.js?
Here is the App.js files:
class App extends Component {
componentDidUpdate = () => {
if (this.props.text && this.props.text.toString().trim()) {
Alert.alert(this.props.title || 'Mensagem', this.props.text.toString());
this.props.clearMessage();
}
}
render() {
return (
<NavigationContainer>
<Navigator />
</NavigationContainer>
)
}
}
const mapStateToProps = ({ message }) => {
return {
title: message.title,
text: message.text
}
}
const mapDispatchToProps = dispatch => {
return {
clearMessage: () => dispatch(setMessage({
title: '',
text: ''
}))
}
}
const connectDispatch = connect(mapStateToProps, mapDispatchToProps);
const connectApp = connectDispatch(App);
export default connectApp;
And here is where I am calling it.Inside a dispatch in posts action.
.then(res => {
dispatch(fetchPosts());
dispatch(postCreated());
dispatch(setMessage({
title: 'Sucesso',
text: 'Nova Postagem!'
}));
});
All other dispatchs are fired.
It's not the if that is preventing the alert to be fired, because I already put the alert outside of the if.
Change this
componentDidUpdate = () => { ... }
for this:
componentDidUpdate(prevProps, prevState, snapshot) { ... }
Keep in mind the componentDidUpdate does not trigger on first render
Thanks all!
I could fix it.
Instead of importing from '.ActionTypes' I was importing from 'Message'
import { SET_MESSAGE } from '../actions/ActionTypes';
I am new to Redux and it caught me offguard!

React native modal error evaluating _this5.state.error[key]

i'm still new to react native and i'm trying to make a modal with timer and i got an error say undefined is not an object (evaluating '_this5.state.error[key]') i tried to open the modal with setTimeout(), i think it has the problem with the state, anyone has an idea to fix it? Thanks
here is my code
class FormInput extends Component {
constructor(props) {
super(props);
const { fields, error } = props;
this.state = this.createState(fields, error);
this.state = {
visible: false
}
//bind functions
this.onChange = this.onChange.bind(this);
this.onSubmit = this.onSubmit.bind(this);
}
componentDidMount(){
this.timer = setTimeout(this.showModal, 5000); //auto reset after 60 seconds of inactivity
}
componentWillUnmount(){
clearTimeout(this.timer);
}
showModal() {
this.setState ({ visible: true})
}
closeModal() {
this.setState ({ visible: false})
}
createState(fields, error) {
const state = {};
fields.forEach((field) => {
let { key, type, value, mandatory } = field;
state[key] = { type: type, value: value, mandatory: mandatory };
})
state["error"] = error;
state["submitted"] = false;
return state;
}
render() {
return (
<View>
<AlertModal visible={this.showModal} close={this.closeModal}/>
</View>
);
Make showModal and closeModal an arrow function
showModal = () => {
this.setState ({ visible: true })
}
closeModal = () => {
this.setState ({ visible: false })
}
or bind them in constructor.
Also visible prop is a boolean and you are passing a function. Pass this.state.visible to fix the issue.
<AlertModal visible={this.state.visible} close={this.closeModal} />
--- UPDATED ---
So after checking out your updated code, I was able to figure out what was the issue. In constructor you are doing this
this.state = this.createState(fields, error);
this.state = {
visible: false
}
which overrides this.state. So I will suggest you to move visible: false into createState function and remove it from constructor.
declare showModal as a arrow function
showModal = () => {
this.setState ({ visible: true})
}
or bind the context for showModal
this.timer = setTimeout(this.showModal.bind(this), 5000)
or
this.timer = setTimeout(() => {this.showModal()}, 5000)
learn more about javascript context this

Undefined props only in componentDidMount

In my code below you can see my component. How it is written will cause the app to crash with the error:
undefined is not an object (evaluation this.props.data.ID)
So in my componentDidMount that id variable is not receiving the props data.
However if i comment out that code in the componentDidMount the app will load fine and the props.data.ID will print out in View. Is there a reason why i can't access the props.data.ID in my componentDidMount?
Heres my code
// timeline.js
class TimelineScreen extends Component {
constructor(props) {
super(props);
this.state = {
posts: []
}
}
componentDidMount() {
const { id } = this.props.data.ID;
axios.post('/api/hometimeline', { id })
.then(res => {
this.setState({
posts: res.data
});
});
}
render() {
const { data } = this.props;
return (
<View style={s.container}>
{
data
?
<Text>{data.ID}</Text>
:
null
}
</View>
);
}
}
function mapStateToProps(state) {
const { data } = state.user;
return {
data
}
}
const connectedTimelineScreen = connect(mapStateToProps)(TimelineScreen);
export default connectedTimelineScreen;
The input of mapStateToProps is not react state, it is redux store. You shouldn't use this.setState in componentDidMount. Use redux actions and reducers to change redux store. Whenever redux store changes, it will invoke mapStateToProps and update your props
componentDidMount() {
console.log(this.props.data); // for test
const id = this.props.data.ID;
//OR
const {id} = this.props.data;
...
}

I want to use scrollTo

I work on a project in React Native and I would like to set my ScrollView position. So I search and I found we should do this with scrollTo but I have an error:
TypeError: Cannot read property 'scrollTo' of undefined
My code:
export default class Index_calendar extends Component {
componentDidMount() {
const _scrollView = this.scrollView;
_scrollView.scrollTo({x: 100});
}
render() {
return (
<ScrollView ref={scrollView => this.scrollView = scrollView}>
{this.renderCalandar()}
</ScrollView>
);
}
}
You can use the InteractionManager to solve this issue.
For instance
InteractionManager.runAfterInteractions(() => this.scroll.current.scrollTo({ x }));
Why not just scrollTo in the render method?
export default class Index_calendar extends Component {
constructor(props) {
super(props);
this.scrollView = null;
}
render() {
return (
<ScrollView ref={scrollView => {
//Sometimes ref can be null so we check it.
if(scrollView !== null && this.scrollView !== scrollView){
this.scrollView = scrollView
scrollView.scrollTo({x: 100});
}}>
{this.renderCalandar()}
</ScrollView>
);
}
}
I found the solution ! we need to use setTimeout like that :
setTimeout(() => {
this.scrollView.scrollTo({x: 100});
}, 1);
You seem to make correct reference. But I suggest to init the reference and make it less error prone:
export default class Index_calendar extends Component {
constructor(props) {
super(props);
this.scrollView = null;
}
componentDidMount() {
const _scrollView = this.scrollView;
if (_scrollView) {
_scrollView.scrollTo({x: 100});
}
}
InteractionManager.runAfterInteractions(() => {
this.scrollRef.scrollTo({
x: 0,
y: 0,
animated: true,
});
});
This worked for me to reset the scroll to top

how to show/hide icons in a react native toolbar

I need to hide a hamburger-menu/location icon on the toolbar while the login screen is active. One option I thought would work is to have the icons set to a empty string by default. And use the EventEmitter in the success callback function in my Login.js & Logout.js, and then listen for it in my toolbar component. Sending a bool to determine show/hide. I am not sure if there is a better way of doing this so I'm up for suggestions. The Emit/Listen events work as expected. The issue is how I use a variable to apply the empty string or named icon.
here is the Toolbar Component.
export default class Toolbar extends Component {
//noinspection JSUnusedGlobalSymbols
static contextTypes = {
navigator: PropTypes.object
};
//noinspection JSUnusedGlobalSymbols
static propTypes = {
onIconPress: PropTypes.func.isRequired
};
//noinspection JSUnusedGlobalSymbols
constructor(props) {
super(props);
this.state = {
title: AppStore.getState().routeName,
theme: AppStore.getState().theme,
menuIcon: '',
locationIcon: ''
};
}
emitChangeMarket() {
AppEventEmitter.emit('onClickEnableNavigation');
}
//noinspection JSUnusedGlobalSymbols
componentDidMount = () => {
AppStore.listen(this.handleAppStore);
AppEventEmitter.addListener('showIcons', this.showIcons.bind(this));
};
//noinspection JSUnusedGlobalSymbols
componentWillUnmount() {
AppStore.unlisten(this.handleAppStore);
}
handleAppStore = (store) => {
this.setState({
title: store.routeName,
theme: store.theme
});
};
showIcons(val) {
if (val === true) {
this.setState({
menuIcon: 'menu',
locationIcon: 'location-on'
});
} else {
this.setState({
menuIcon: '',
locationIcon: ''
});
}
}
render() {
let menuIcon = this.state.menuIcon;
let locationIcon = this.state.locationIcon;
const {navigator} = this.context;
const {theme} = this.state;
const {onIconPress} = this.props;
return (
<MaterialToolbar
title={navigator && navigator.currentRoute ? navigator.currentRoute.title : 'Metro Tracker Login'}
primary={theme}
icon={navigator && navigator.isChild ? 'keyboard-backspace' : {menuIcon}}
onIconPress={() => navigator && navigator.isChild ? navigator.back() : onIconPress()}
actions={[{
icon: {locationIcon},
onPress: this.emitChangeMarket.bind(this)
}]}
rightIconStyle={{
margin: 10
}}
/>
);
}
}
The warning message I get is the:
Invalid prop icon of type object supplied to toolbar, expected a string.
how can I pass a string while wrapped in variable brackets?
Or if easier how can I hide/show the entire toolbar? either way works.
Try removing the brackets around menuIcon and locationIcon:
...
icon={navigator && navigator.isChild ? 'keyboard-backspace' : menuIcon}
...
icon: locationIcon,
...