Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to load child component with props on first render with React Hooks

Tags:

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;
like image 811
Sheixt Avatar asked Sep 19 '19 12:09

Sheixt


1 Answers

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

like image 117
Fuechter Avatar answered Oct 08 '22 16:10

Fuechter