Source:  Twitter logo

In my React Native project, I am using React Hooks in a functional component to set the state on my Redux store. The value from the store I am then trying to pass into a child component by way of a prop called timestamp

The problem is, the child component takes advantage of the DateTimePicker component (from react-native-modal-datetime-picker) and when it initially renders the timestamp prop is undefined, which causes it to error. The result can be seen by the following console logs:

undefined
undefined
Object {
  "date": "2019-08-20",
  "full": 2019-08-20T12:21:52.874Z,
  "time": "01:21:52",
}
Object {
  "date": "20.08.2019",
  "time": "01:21 pm",
}

The component is rendered twice. First, the props are undefined, then they are populated.

How can I force the props to be loaded before the first render?

I don't want to call the state directly within the child component as this will be reused for various use cases.

This is the parent component:

import React, { useEffect } from 'react';
import { View } from 'react-native';
import { useSelector, useDispatch } from 'react-redux';
import DateInput from '../../../components/DateTimeInput';
import * as actions from '../../../redux/actions';
import moment from 'moment';

const ReportParamsScreen = props => {
    const report        = useSelector(state => state.report);
    const dispatch = useDispatch();

    useEffect(() => { 
        dispatch(actions.setReportParams({ 
            start_date  : moment().subtract(30, "days"),
            end_date    : moment()
        }))
    }, [dispatch]);

    return (
        <View style={styles.row}>
            <View style={styles.column}>
                <DateInput 
                    mode="date"
                    timestamp={report.params.start_date}
                />
            </View>
            <View style={styles.column}>
                <DateInput 
                    mode="date"
                    timestamp={report.params.start_date}
                />
            </View>
        </View>
    )
};

export default ReportParamsScreen;

This is the child component:

import React, { useEffect, useState } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { Body, Button, Text, Icon } from "native-base";
import DateTimePicker from "react-native-modal-datetime-picker";

const DateInput = props => {
    const [isPickerVisible, setIsPickerVisible] = useState(false);

    const showDateTimePicker = () => {
        setIsPickerVisible(true);
    };

    const hideDateTimePicker = () => {
        setIsPickerVisible(false);
    };

    const handleDateTimePicked = dateTime => {
        console.log(dateTime)
        hideDateTimePicker();
    };

    console.log(props.timestamp.value);
    console.log(props.timestamp.labels); // These are the logs referened above.

    return (
        <Body>
            <Button transparent onPress={showDateTimePicker}>
                <Icon
                    type="MaterialIcons"
                    name={props.mode == 'time' ? 'access-time' : 'date-range' }
                />
                <Text>{props.timestamp.labels[props.mode]}</Text>
            </Button>
            <DateTimePicker
                date={props.timestamp.value.date}
                mode={props.mode}
                isVisible={isPickerVisible}
                onConfirm={handleDateTimePicked}
                onCancel={hideDateTimePicker}
            />
        </Body>
    );
}

export default DateInput;

In this case you have two solutions, both will depends what is better for you or your project.

The first solution is set the start_date and end_date as initial state on reducers about Report, but I don't know this make sense for you business/rules, isn't the best solution, but is the solution "easier".

import React, { useEffect } from 'react';
import { View } from 'react-native';
import { useSelector, useDispatch } from 'react-redux';
import DateInput from '../../../components/DateTimeInput';
import * as actions from '../../../redux/actions';
import moment from 'moment';

const ReportParamsScreen = props => {
    const report        = useSelector(state => state.report);
    const dispatch = useDispatch();

    useEffect(() => { 
        dispatch(actions.setReportParams({ 
            start_date  : moment().subtract(30, "days"),
            end_date    : moment()
        }))
    }, [dispatch]);

    if (!report.params.start_date) {
      return null
    }

    return (
        <View style={styles.row}>
            <View style={styles.column}>
                <DateInput 
                    mode="date"
                    timestamp={report.params.start_date}
                />
            </View>
            <View style={styles.column}>
                <DateInput 
                    mode="date"
                    timestamp={report.params.start_date}
                />
            </View>
        </View>
    )
};

export default ReportParamsScreen;

The second solution is verify that report.params.start_date is undefined on ReportParamsScreen, if is true, return a null on component while the date is null or return a component that represent like Loading, is a good practices.

Example:

import React, { useEffect } from 'react';
import { View } from 'react-native';
import { useSelector, useDispatch } from 'react-redux';
import DateInput from '../../../components/DateTimeInput';
import * as actions from '../../../redux/actions';
import moment from 'moment';

const ReportParamsScreen = props => {
    const report        = useSelector(state => state.report);
    const dispatch = useDispatch();

    useEffect(() => { 
        dispatch(actions.setReportParams({ 
            start_date  : moment().subtract(30, "days"),
            end_date    : moment()
        }))
    }, [dispatch]);

    if (!report.params.start_date) {
      return <Loading />
    }

    return (
        <View style={styles.row}>
            <View style={styles.column}>
                <DateInput 
                    mode="date"
                    timestamp={report.params.start_date}
                />
            </View>
            <View style={styles.column}>
                <DateInput 
                    mode="date"
                    timestamp={report.params.start_date}
                />
            </View>
        </View>
    )
};

export default ReportParamsScreen;

This situation ever happen because useEffect execute only after render.

Reference:

What does useEffect do? By using this Hook, you tell React that your component needs to do something after render. React will remember the function you passed (we’ll refer to it as our “effect”), and call it later after performing the DOM updates. In this effect, we set the document title, but we could also perform data fetching or call some other imperative API.

Link: https://reactjs.org/docs/hooks-effect.html

4 users liked answer #0dislike answer #04
Fuechter profile pic
Fuechter

You can set up an initial state for default reducer, the initial state might look like below

const initialState = {
  report: {
     params: { 
      start_date: moment().subtract(30, "days"),
      end_date: moment()
    }
  }
}

function rootReducer(state = initialState, action){
   //...
}

in the parent component, the report is undefined is because there are no initial state, the state has been set only after first render, because the useEffect only been called after rendered.

or you can give a default value for DateInput.

in parent component


const ReportParamsScreen = props => {
    const report        = useSelector(state => state.report);
    const dispatch = useDispatch();

    useEffect(() => { 
        dispatch(actions.setReportParams({ 
            start_date  : moment().subtract(30, "days"),
            end_date    : moment()
        }))
    }, [dispatch]);

    const getStartDate = () => {
      if(report && report.params && report.params.start_date) {
         return report.params.start_date
      }
      return moment()
    }
    return (
        <View style={styles.row}>
            <View style={styles.column}>
                <DateInput 
                    mode="date"
                    timestamp={getStartDate()}
                />
            </View>
            ...
        </View>
    )
};

export default ReportParamsScreen;
0 users liked answer #1dislike answer #10
Elantris profile pic
Elantris

Copyright © 2022 QueryThreads

All content on Query Threads is licensed under the Creative Commons Attribution-ShareAlike 3.0 license (CC BY-SA 3.0).