Реагируйте с Redux: дочерний компонент не рендерится после изменения реквизита (даже если они не равны)

Я создаю приложение с React Native, используя Redux для управления состоянием. Ниже я опубликую свой код для всех задействованных компонентов и редуктора, но так как это будет много, позвольте мне сначала описать проблему в нескольких предложениях.

У меня есть неизменный редуктор для моих объектов, который называется "waitercalls". У меня есть экран (HomeScreen), который делает два компонента. Каждый компонент является <FlatList /> объектов. Объекты (waitercalls) передаются им через реквизиты его родителем (HomeScreen). HomeScreen подключен к Redux через React-Redux connect() и получает объекты ('waitercalls') через селектор, созданный с помощью Re-Select.

Пункты левого списка могут быть нажаты, и при нажатии отправка события в редуктор. Здесь возникает проблема. При нажатии элемента левого списка корректно обновляется левый список render()). Правильный список не рендерится, хотя он получает те же реквизиты.

Почему левый список перерисовывается, а правый нет? Редуктор является неизменяемым, селектор тоже, и даже длина списка меняется с одного на ноль, что должно исключить возможность поверхностного равенства.

А теперь код:

waitercallsReducer:

import { createSelector } from "reselect";

const initialState = {};

const waitercallsReducer = (state = initialState, action) => {
  if (action.payload && action.payload.entities && action.payload.entities.waitercalls) {
    return {
      ...state,
      ...action.payload.entities.waitercalls
    };
  } else {
    return state;
  }
};

export default waitercallsReducer;

export const getAllWaitercallsNormalizedSelector = state => state.waitercalls;
export const getAllWaitercallsSelector = createSelector(
  getAllWaitercallsNormalizedSelector,
  waitercalls => Object.values(waitercalls)
);

export const getAllActiveWaitercallsSelector = createSelector(
  getAllWaitercallsSelector,
  waitercalls => waitercalls.filter(waitercall => !waitercall.done)
);

Создатели действий:

import { setValues } from "../core/core";

// feature name
export const WAITERCALLS = "[Waitercalls]";

// action creators
export const setValues = (values, type) => ({
  type: `SET ${type}`,
  payload: values,
  meta: { feature: type }
});
export const setWaitercalls = waitercalls => setValues(waitercalls, WAITERCALLS);

Домашний экран:

import React, { Component } from "react";
import { View, TouchableOpacity } from "react-native";
import { SafeAreaView } from "react-navigation";
import { connect } from "react-redux";
import { Icon } from "react-native-elements";
import PropTypes from "prop-types";

// ... I've omitted all the imports so that it's shorter

export class HomeScreen extends Component {
  // ... I've omitted navigationOptions and propTypes

  render() {
    const {
      checkins,
      customChoiceItems,
      menuItemPrices,
      menuItems,
      orders,
      pickedRestaurant,
      tables,
      waitercalls
    } = this.props;
    console.log("Rendering HomeScreen");
    return (
      <SafeAreaView style={styles.container}>
        <View style={styles.activeOrders}>
          <OrdersList
            checkins={checkins}
            customChoiceItems={customChoiceItems}
            menuItemPrices={menuItemPrices}
            menuItems={menuItems}
            orders={orders}
            restaurantSlug={pickedRestaurant.slug}
            tables={tables}
            waitercalls={waitercalls}
          />
        </View>
        <View style={styles.tableOvewView}>
          <TableOverview
            checkins={checkins}
            orders={orders}
            tables={tables}
            waitercalls={waitercalls}
          />
        </View>
      </SafeAreaView>
    );
  }
}

const mapStateToProps = state => ({
  checkins: getAllCheckinsSelector(state),
  customChoiceItems: getAllCustomChoiceItemsNormalizedSelector(state),
  menuItemPrices: getAllMenuItemPricesNormalizedSelector(state),
  menuItems: getAllMenuItemsNormalizedSelector(state),
  orders: getActiveOrdersSelector(state),
  pickedRestaurant: getPickedRestaurantSelector(state),
  tables: getAllTablesSelector(state),
  waitercalls: getAllActiveWaitercallsSelector(state)
});

export default connect(mapStateToProps)(HomeScreen);

OrdersList (как вы можете видеть, OrdersList также позволяет нажимать на заказы, что отображает то же ошибочное поведение, что и отсутствие повторного отображения TableOverView), который является левым списком с кликабельным <ListItem /> s.

import React, { PureComponent } from "react";
import { FlatList, Image, Text } from "react-native";
import { ListItem } from "react-native-elements";
import { connect } from "react-redux";
import PropTypes from "prop-types";

// ... omitted imports

export class OrdersList extends PureComponent {
  // omitted propTypes

  keyExtractor = item => item.uuid;

  registerItem = item => {
    // Remember the order status, in case the request fails.
    const { restaurantSlug, setOrders } = this.props;
    const itemStatus = item.orderStatus;
    const data = {
      restaurant_slug: restaurantSlug,
      order_status: "registered",
      order_uuid: item.uuid
    };

    setOrders({
      entities: { orders: { [item.uuid]: { ...item, orderStatus: data.order_status } } }
    });
    postOrderStatusCreate(data)
      .then(() => {})
      .catch(err => {
        // If the request fails, revert the order status change and display an alert!
        alert(err);
        setOrders({ entities: { orders: { [item.uuid]: { ...item, orderStatus: itemStatus } } } });
      });
  };

  answerWaitercall = item => {
    const { restaurantSlug, setWaitercalls } = this.props;
    const data = {
      done: true,
      restaurant_slug: restaurantSlug
    };
    setWaitercalls({ entities: { waitercalls: { [item.uuid]: { ...item, done: true } } } });
    putUpdateWaitercall(item.uuid, data)
      .then(() => {})
      .catch(err => {
        alert(err);
        setWaitercalls({ entities: { waitercalls: { [item.uuid]: { ...item, done: false } } } });
      });
  };

  renderItem = ({ item }) => {
    const { checkins, customChoiceItems, menuItemPrices, menuItems, tables } = this.props;
    return item.menuItem ? (
      <ListItem
        title={`${item.amount}x ${menuItems[item.menuItem].name}`}
        leftElement={
          <Text style={styles.amount}>
            {tables.find(table => table.checkins.includes(item.checkin)).tableNumber}
          </Text>
        }
        rightTitle={`${
          menuItemPrices[item.menuItemPrice].label
            ? menuItemPrices[item.menuItemPrice].label
            : menuItemPrices[item.menuItemPrice].size
              ? menuItemPrices[item.menuItemPrice].size.size +
                menuItemPrices[item.menuItemPrice].size.unit
              : ""
        }`}
        subtitle={`${
          item.customChoiceItems.length > 0
            ? item.customChoiceItems.reduce((acc, customChoiceItem, index, arr) => {
                acc += customChoiceItems[customChoiceItem].name;
                acc += index < arr.length - 1 || item.wish ? "\n" : "";
                return acc;
              }, "")
            : null
        }${item.wish ? "\n" + item.wish : ""}`}
        onPress={() => this.registerItem(item)}
        containerStyle={styles.alignTop}
        bottomDivider={true}
      />
    ) : (
      <ListItem
        title={
          item.waitercallType === "bill"
            ? SCREEN_TEXT_HOME_BILL_CALLED
            : SCREEN_TEXT_HOME_SERVICE_ASKED
        }
        leftElement={
          <Text style={styles.amount}>
            {
              tables.find(table =>
                table.checkins.includes(
                  checkins.find(checkin => checkin.consumer === item.consumer).uuid
                )
              ).tableNumber
            }
          </Text>
        }
        rightIcon={{
          type: "ionicon",
          name: item.waitercallType === "bill" ? "logo-euro" : "ios-help-circle-outline"
        }}
        onPress={() => this.answerWaitercall(item)}
        bottomDivider={true}
      />
    );
  };

  render() {
    const { orders, waitercalls } = this.props;
    return (
      <FlatList
        keyExtractor={this.keyExtractor}
        data={[...orders, ...waitercalls]}
        renderItem={this.renderItem}
        // ... omitted ListHeader and ListEmpty properties
      />
    );
  }
}

export default connect(
  null,
  { setOrders, setWaitercalls }
)(OrdersList);

TableOverview, который является правильным <FlatList />:

import React, { Component } from "react";
import { FlatList } from "react-native";
import PropTypes from "prop-types";

// ... omitted imports

export class TableOverview extends Component {
  // ... omitted propTypes

  keyExtractor = item => item.uuid;

  renderItem = ({ item }) => {
    const { checkins, orders, waitercalls } = this.props;
    if (item.invisible) return <Table table={item} />;
    console.log("Rendering TableOverview");
    return (
      <Table
        table={item}
        hasActiveOrders={orders.some(order => item.userOrders.includes(order.uuid))}
        billWanted={item.checkins.some(checkin =>
          waitercalls.some(
            waitercall =>
              waitercall.waitercallType === "bill" &&
              waitercall.consumer ===
                checkins.find(checkinObj => checkinObj.uuid === checkin).consumer
          )
        )}
        serviceWanted={item.checkins.some(checkin =>
          waitercalls.some(
            waitercall =>
              waitercall.waitercallType === "waiter" &&
              waitercall.consumer ===
                checkins.find(checkinObj => checkinObj.uuid === checkin).consumer
          )
        )}
      />
    );
  };

  formatData = (data, numColumns) => {
    const numberOfFullRows = Math.floor(data.length / numColumns);

    let numberOfElementsLastRow = data.length - numberOfFullRows * numColumns;
    while (numberOfElementsLastRow !== numColumns && numberOfElementsLastRow !== 0) {
      data.push({ uuid: `blank-${numberOfElementsLastRow}`, invisible: true });
      numberOfElementsLastRow++;
    }

    return data;
  };

  render() {
    const { tables } = this.props;
    return (
      <FlatList
        style={styles.container}
        keyExtractor={this.keyExtractor}
        data={this.formatData(tables, NUM_COLUMNS)}
        renderItem={this.renderItem}
        numColumns={NUM_COLUMNS}
      />
    );
  }
}

export default TableOverview;

1 ответ

Я нашел решение!

Список не переиздавался, потому что <FlatList /> смотрел только на столы, а не на ожидание.

Я должен был добавить следующее свойство к <FlatList />:

extraData={[...checkins, ...orders, ...waitercalls]}
Другие вопросы по тегам