简体   繁体   中英

Component out of sync in React Native

I'm working with my React Native project where I have like a "form" in which the user selects what day and between what hours they're available. There are two components involved:

  • The parent one: It has two time selectors and the day selector component
  • The child one: This is the day selector component. It displays the days in the week so the user can select whatever days they want.

This is the DaySelector:

import * as React from "react";
import { View, Text, StyleSheet, Button, Alert } from "react-native";
import { TouchableOpacity } from "react-native-gesture-handler";
import colors from "../assets/constants/style-variables";

interface Item {
  selected: Boolean;
  day: String;
}

function Day(props) {
  return (
    <TouchableOpacity
      onPress={props.onPress}
      style={props.selected ? styles.buttonActive : styles.button}
    >
      <Text>{props.day}</Text>
    </TouchableOpacity>
  );
}

export default class DaySelector extends React.Component<
  { onChange },
  { selectedItems: undefined | Item[]; items: Item[] }
> {
  constructor(props) {
    super(props);
    this.state = {
      selectedItems: [],
      items: [
        {
          selected: false,
          day: "Lu",
        },
        {
          selected: false,
          day: "Ma",
        },
        {
          selected: false,
          day: "Mi",
        },
        {
          selected: false,
          day: "Ju",
        },
        {
          selected: false,
          day: "Vi",
        },
        {
          selected: false,
          day: "Sa",
        },
        {
          selected: false,
          day: "Do",
        },
      ],
    };
  }

  onPress = (index) => {
    let items = [...this.state.items];
    let item = { ...items[index] };
    item.selected = !item.selected;
    items[index] = item;
    this.setState({ items });
    this.setState((prevState) => ({
      selectedItems: prevState.items.filter((i) => i.selected),
    }));
    this.props.onChange(this.state.selectedItems);
  };
  render() {
    return (
      <View>
        <View style={styles.container}>
          {this.state.items.map((item, index) => {
            return (
              <Day
                key={index}
                selected={item.selected}
                day={item.day}
                onPress={this.onPress.bind(this, index)}
              />
            );
          })}
        </View>
      </View>
    );
  }
}

const styles = StyleSheet.create({
  container: {
    flexDirection: "row",
  },
  button: {
    margin: 5,
    borderColor: colors.lightGray,
    borderWidth: 2,
    borderRadius: 40,
    padding: 7,
  },
  buttonActive: {
    margin: 5,
    borderColor: colors.primary,
    borderWidth: 2,
    backgroundColor: colors.primary,
    borderRadius: 50,
    padding: 7,
  },
});

As you can see, it collects all the days selected and passes them into the onChange prop. That way I can access the results in my parent component, which is this one:

import React, { useState, useCallback, useReducer } from "react";
import { useMutation } from "@apollo/react-hooks";
import { REGISTER } from "../Queries";
import Loader from "../components/Loader";
import DateTimePicker from "@react-native-community/datetimepicker";
import { View, StyleSheet } from "react-native";
import colors from "../assets/constants/style-variables";
import { TouchableOpacity } from "react-native-gesture-handler";
import PrimaryText from "../assets/constants/PrimaryText";
import DaySelector from "../components/DaySelector";

function reducer(state, action) {
  switch (action.type) {
    case "refresh-days":
      return {
        dayList: action.value,
      };
    default:
      return state;
  }
}

export default function ScheduleSelect({ navigation, route }) {
  const [loadingRegistration, setLoadingRegistration] = useState(null);

  const [showTimePicker1, setShowTimePicker1] = useState(false);
  const [showTimePicker2, setShowTimePicker2] = useState(false);

  const [time1, setTime1] = useState(null);
  const [time2, setTime2] = useState(null);

  const [{ dayList }, dispatch] = useReducer(reducer, { dayList: [] });

  const show1 = () => {
    setShowTimePicker1(true);
  };

  const show2 = () => {
    setShowTimePicker2(true);
  };

  const handleTimePick1 = (_event, selectedTime) => {
    setShowTimePicker1(false);
    if (selectedTime !== undefined) {
      setTime1(selectedTime);
    }
  };

  const handleTimePick2 = (_event, selectedTime) => {
    setShowTimePicker2(false);
    if (selectedTime !== undefined) {
      setTime2(selectedTime);
    }
  };

  return (
    <View>
      <View style={styles.container}>
        <PrimaryText fontSize={20} margin={22} textAlign={"center"}>
          Recibirás turnos en estos horarios:
        </PrimaryText>
        <View style={styles.cardHolder}>
          <View style={styles.card}>
            <View style={styles.timeHelper}>
              <PrimaryText textAlign={"center"}>Desde</PrimaryText>
            </View>
            {time1 && (
              <TouchableOpacity onPress={show1}>
                <View style={styles.timeSelected}>
                  <PrimaryText textAlign={"center"} fontSize={36}>
                    {`${time1?.getHours()}:${
                      time1.getMinutes() < 10
                        ? "0" + time1.getMinutes()
                        : time1.getMinutes()
                    }`}
                  </PrimaryText>
                </View>
              </TouchableOpacity>
            )}
            {!time1 && (
              <TouchableOpacity style={styles.clockEdit} onPress={show1}>
                <PrimaryText
                  fontSize={36}
                  letterSpacing={2}
                  textAlign={"center"}
                >
                  --:--
                </PrimaryText>
              </TouchableOpacity>
            )}
            {showTimePicker1 && (
              <DateTimePicker
                value={new Date()}
                testID="dateTimePicker"
                mode={"time"}
                is24Hour={true}
                display="default"
                onChange={handleTimePick1}
              />
            )}
          </View>
          <PrimaryText fontSize={36} margin={7}>
            -
          </PrimaryText>
          <View style={styles.card}>
            <View style={styles.timeHelper}>
              <PrimaryText textAlign={"center"}>Hasta</PrimaryText>
            </View>
            {time2 && (
              <TouchableOpacity onPress={show2}>
                <View style={styles.timeSelected}>
                  <PrimaryText textAlign={"center"} fontSize={36}>
                    {`${time2?.getHours()}:${
                      time2.getMinutes() < 10
                        ? "0" + time2.getMinutes()
                        : time2.getMinutes()
                    }`}
                  </PrimaryText>
                </View>
              </TouchableOpacity>
            )}
            {!time2 && (
              <TouchableOpacity style={styles.clockEdit} onPress={show2}>
                <PrimaryText
                  fontSize={36}
                  letterSpacing={2}
                  textAlign={"center"}
                >
                  --:--
                </PrimaryText>
              </TouchableOpacity>
            )}
            {showTimePicker2 && (
              <DateTimePicker
                value={new Date()}
                testID="dateTimePicker"
                mode={"time"}
                is24Hour={true}
                display="default"
                onChange={handleTimePick2}
              />
            )}
          </View>
        </View>
        {time2?.getTime() < time1?.getTime() && (
          <PrimaryText color={colors.warning}>
            Por favor, seleccioná horarios válidos
          </PrimaryText>
        )}
        <PrimaryText margin={5}> Los días: </PrimaryText>
        <View style={styles.card}>
          <DaySelector
            onChange={(value) => dispatch({ type: "refresh-days", value })}
          />
        </View>
      </View>
      <View style={styles.buttonWrapper}>
        <TouchableOpacity
          style={styles.button}
          onPress={() => {
            console.log(dayList);
          }}
        >
          <PrimaryText color={colors.iceWhite}>SIGUIENTE</PrimaryText>
        </TouchableOpacity>
      </View>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    height: "88%",
    justifyContent: "center",
    paddingTop: 15,
    alignItems: "center",
  },
  clockEdit: {
    padding: 7,
  },
  button: {
    alignSelf: "flex-end",
    backgroundColor: colors.primary,
    padding: 7,
    margin: 15,
    borderRadius: 15,
  },
  buttonWrapper: {
    justifyContent: "flex-end",
    alignItems: "flex-end",
    paddingRight: 15,
  },
  card: {
    flexDirection: "row",
    justifyContent: "center",
    minWidth: "35%",
    backgroundColor: colors.iceWhite,
    borderRadius: 10,
    padding: 20,
    marginBottom: 10,
    shadowColor: "#000",
    shadowOffset: {
      width: 0,
      height: 2,
    },
    shadowOpacity: 0.25,
    shadowRadius: 3.84,
    elevation: 5,
  },
  cardHolder: {
    flexDirection: "row",
    alignItems: "center",
  },
  timeHelper: {
    justifyContent: "center",
    position: "absolute",
    marginTop: 5,
  },
  timeSelected: {
    alignItems: "center",
    justifyContent: "center",
  },
  editIcon: {
    right: 3,
    top: 3,
    position: "absolute",
  },
});

The problem is that when I console.log the value of the onChange prop, I get the items out of sync. To try and solve this, you see that I used a reducer, since the task is dependent of the result of the action. However, it does not change anything and still returns "outdated" information.

How can I sync both of the components?

This problems might come from your setState in your first component. you are calling the setState and then use the state straight after. But setState is asynchronous. So when you call your onChange, the state is still the previous one.

The prototype of setState is

setState(callback: (currentState) => newState | newState, afterSetState: (newState) => void)

So you can use the afterSetState callback to receive the new state and call your props.onChange

Also you are calling setState twice, but you could call it once and manipulate the state to return all in one.

However, if I could give you an advice, it'd be better controlling the state from the parent instead of the child. It avoids you calling a setState in the child and call the onChange in the parent which will set the state somewhere else again.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM