React redux обновление состояния вложенного массива

#reactjs #redux #react-redux

#reactjs #сокращение #реагировать-redux #redux #react-redux

Вопрос:

Я пытаюсь изучить React (с помощью redux), поэтому я создаю приложение, в котором я могу создавать планы тренировок, добавлять к ним тренировки, а затем добавлять упражнения к тренировке.

PlanListComponent

 import { Button, Card, Typography } from "@material-ui/core/";
import { makeStyles } from "@material-ui/core/styles";
import DeleteIcon from "@material-ui/icons/Delete";
import React, { useEffect } from "react";
import { useDispatch, useSelector } from "react-redux";
import { deletePlan, getPlans } from "../actions/plansActions";
import AddWorkouts from "./AddWorkouts";

const useStyles = makeStyles((theme) => ({}));

function PlansList() {
  const classes = useStyles();

  const { plans } = useSelector((state) => state.plans);
  const dispatch = useDispatch();

  useEffect(() => {
    dispatch(getPlans());
  }, [dispatch]);

  return (
    <div>
      {plans.map((plan) => (
        <Card key={plan._id}>
          <Typography>{plan.name}</Typography>
          <Typography>{plan._id}</Typography>
          <div>
            {plan.workouts.map((workout) => (
              <li key={workout._id}>{workout.description}</li>
            ))}
          </div>
          <AddWorkouts plan={plan} />
          <Button onClick={() => dispatch(deletePlan(plan._id))}>
            <DeleteIcon /> Delete
          </Button>
        </Card>
      ))}
    </div>
  );
}

export default PlansList;
  

Мой компонент PlansList отображает карточку для каждого плана. В этой карточке отображается список тренировок для каждой тренировки в этом плане. После добавления тренировки в план компонент PlansList не выполняет повторный запуск. Добавленная тренировка отображается только после обновления страницы. Я предполагаю, что это происходит потому, что мне нужно обновить состояние вложенного массива workout, чтобы заставить React повторно отображать мой компонент.

Это мои действия и редукторы для добавления тренировки в план. Полезная нагрузка, которую я отправляю в своем действии, представляет собой массив объектов.

Экшен

 export const addWorkouts = (workouts, planId) => (dispatch, getState) => {
  axios
    .post(`/workouts/${planId}`, workouts, tokenConfig(getState))
    .then(res =>
      dispatch({
        type: ADD_WORKOUTS,
        id: planId
        payload: res.data
      }));
}
  

Редуктор

 const initialState = {
  plans: [{
    workouts: []
  }],
  isLoading: false,
};

export default function (state = initialState, action) {
  switch (action.type) {
    case ADD_WORKOUTS:
      return {
        ...state,
        plans: {
          // guessing i should find the right plan by id here
          ...state.plans,
          workouts: {
            ...state.plans.workouts,
            workouts: state.plans.workouts.concat(action.payload)
          }
        }
      };
    default:
      return state;
  }
  

Я видел много руководств о том, как обновить состояние вложенных массивов, и попробовал несколько разных вещей, но, похоже, я не могу найти здесь правильное решение.

У кого-нибудь есть какие-нибудь идеи, как исправить эту проблему?

Ответ №1:

Ваш редуктор не обновляет правильные вещи. Вы фактически преобразуете свойство workouts , которое было массивом, в объект, который имеет свойство workouts , которое является массивом. Это совсем не то, что вы хотите сделать.

Поиск и обновление элемента массива в redux — это не проблема, когда легко иметь plans объект, который вводится идентификатором плана. Имеет ли значение порядок планов? Если это произойдет, я бы все равно сохранил планы с ключом id, но у меня было бы отдельное значение в состоянии, в котором хранится упорядоченный массив идентификаторов планов.

То, что я предлагаю, выглядит следующим образом:

 const initialState = {
  plans: {}, // you could call this plansById
  isLoading: false,
  planOrder: [] // totally optional and only needed if the order matters when selecting all plans
};

export default function (state = initialState, action) {
  switch (action.type) {
    case ADD_WORKOUTS:
      const existingPlan = state.plans[action.id] || {};
      return {
        ...state,
        plans: {
          ...state.plans,
          [action.id]: {
            ...existingPlan,
            workouts: ( existingPlan.workouts || [] ).concat(action.payload)
          }
        }
      };
    default:
      return state;
  }
  

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

 function planReducer (planState = { workouts: []}, action ) {
  switch (action.type) {
    case ADD_WORKOUTS:
      return {
        ...planState,
        workouts: planState.workouts.concat(action.payload)
      };
      default:
        return planState;
    }
}

function rootReducer (state = initialState, action) {
  /**
   * you can use a `switch` statement and list all actions which modify an individual plan
   * or use an `if` statement and check for something, like if the action has a `planId` property 
   */
  switch (action.type) {
    case ADD_WORKOUTS:
    case OTHER_ACTION:
      return {
        ...state,
        plans: {
          ...state.plans,
          [action.id]: planReducer(state.plans[action.id], action),
        }
      };
    default:
      return state;
  }
}
  

С помощью предложенных редукторов вам не нужно вносить какие-либо изменения в свое действие, но вам нужно изменить свой селектор, потому plans что он больше не является массивом. Ваш селектор становится (state) => Object.values(state.plans) .

Если вы сохранили конкретный заказ плана, вы должны выбрать заказ и сопоставить его с отдельными планами: (state) => state.planOrder.map( id => state.plans[id] ) .