Как обновить массив родительского состояния из дочернего компонента? - PullRequest
0 голосов
/ 08 января 2019
App
|
|----|---------------------|
     |                     |
     RadioInputForm        TextInputForm

Я пытаюсь обновить массивы в своем родительском состоянии данными из дочерних компонентов, но события не запускаются. Я уже пробовал предыдущие решения, такие как {() => callback ()} и {() => callback.bind (this)}, но, похоже, ничего не работает.

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

Вот что я пробовал до сих пор, любая помощь будет оценена:

// App.js
constructor(props) {
    super(props);

    let radioQuestions = [], textQuestions = [];
    let option = {id: "", options: {option1: false, option2: false, option3: false}, text: ""};

    for(var i = 0; i < data.questions.length; ++i) {
        textQuestions[i] = "";

        if(data.questions[i].question_type === "RadioQuestion") {
            radioQuestions[i] = option;
            radioQuestions[i].id = data.questions[i].id;
        } else radioQuestions[i] = {};
    }

    this.state = {
        textQuestions: textQuestions,
        radioQuestions: radioQuestions,
        index: 0
    };

    this.handleRadioSelect= this.handleRadioSelect.bind(this);
    this.handleRadioTextChange = this.handleRadioTextChange.bind(this);
    this.handleTextQuestionChange= this.handleTextQuestionChange.bind(this);
    this.handleBack = this.handleBack.bind(this);
    this.handleNext = this.handleNext.bind(this);
}

handleRadioSelect(e) {
    const { value } = e.target;
    const questions = this.state.radioQuestions.slice();
    const index = this.state.index;

    switch(value) {
        case "option1":
            if(questions[index].options.option1 === true)
                questions[index].options.option1 = false;
            else questions[index].options.option1 = true;
            break;
        case "option2":
            if(questions[index].options.option2 === true)
                questions[index].options.option2 = false;
            else questions[index].options.option2 = true;
            break;
        case "option3":
            if(questions[index].options.option3 === true)
                questions[index].options.option3 = false;
            else questions[index].options.option3 = true;
            break;
        default:
    }

    this.setState({
        radioQuestions: questions
    });
}

handleRadioTextChange(e) {
    const { value } = e.target;

    const questions = this.state.radioQuestions.slice()
    questions[this.state.index].text = value;

    this.setState({
        radioQuestions: questions
    })
}

handleTextQuestionChange(e) {
    const { value } = e.target;

    const answers = this.state.textAnswers.slice()
    answers[this.props.index] = value;

    this.setState({
        textAnswers: answers
    })
}

Дочерний компонент с формой Radio:

constructor(props) {
    super(props);

    this.state = {
        radioQuestions: this.props.radioQuestions
    }

    this.handleRadioTextChange = this.handleRadioTextChange.bind(this);
    this.handleRadioSelect = this.handleRadioSelect.bind(this);
}

handleRadioSelect(e) {
    const { value } = e.target;
    const questions = this.state.radioQuestions.slice();
    const index = this.state.index;

    console.log(JSON.stringify(questions[index]));

    switch(value) {
        case "option1":
            if(questions[index].options.option1 === true)
                questions[index].options.option1 = false;
            else questions[index].options.option1 = true;
            break;
        case "option2":
            if(questions[index].options.option2 === true)
                questions[index].options.option2 = false;
            else questions[index].options.option2 = true;
            break;
        case "option3":
            if(questions[index].options.option3 === true)
                questions[index].options.option3 = false;
            else questions[index].options.option3 = true;
            break;
        default:
    }

    this.props.handleRadioSelect(e);

    this.setState({
        radioQuestions: questions
    });
}

handleRadioTextChange(e) {
    const { value } = e.target;

    const questions = this.state.radioQuestions.slice()
    questions[this.props.index].text = value;

    this.setState({
        radioQuestions: questions
    })
}

render() {
    return (
        <div className="form-group">
            <input type="radio" name="option1"
                  value="option1"
                  onChange = { this.state.handleRadioSelect }
                  defaultChecked={ this.state.radioQuestions[this.props.index].options.option1 } /> Option 1<br />
            <input type="radio" name="option2"
                  value="option2"
                  onChange = { this.state.handleRadioSelect }
                  defaultChecked={ this.state.radioQuestions[this.props.index].options.option2 } /> Option 2<br />
            <input type="radio" name="option3"
                  value="option3"
                  onChange = { this.state.handleRadioSelect }
                  defaultChecked={ this.state.radioQuestions[this.props.index].options.option3 } /> Option 3<br />
            <textarea rows="1" cols="40" id="answer" name="answer"
                className ="form-control input-default"
                onChange={ this.state.handleRadioTextChange }
                defaultValue = { this.state.radioQuestions[this.props.index].text } />
        </div>
    );
}

Повторный код (который есть и в родительском, и в дочернем) был из моих попыток решить проблему. Наконец, вот компонент формы ввода текста:

constructor(props) {
    super(props);

    this.state = {
        textAnswers: this.props.textQuestions,
    };

    this.handleTextQuestionChange = this.handleTextQuestionChange.bind(this);
}

handleTextQuestionChange(e) {
    const { value } = e.target;

    const answers = this.state.textAnswers.slice()
    answers[this.props.index] = value;

    this.props.handleTextQuestionChange(e);

    this.setState({
        textAnswers: answers
    })
}

render() {
    return (
        <div className="form-group">
            <textarea rows="4" cols="50" id="answer" name="answer"
                className ="form-control input-default" onChange = { this.state.handleTextQuestionChange  }
                defaultValue = { this.state.textAnswers[this.props.index] } />
        </div>
    );
}

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

Ответы [ 2 ]

0 голосов
/ 09 января 2019

Краткий ответ и рабочие коды и коробка: https://codesandbox.io/s/21q3j32qyy Чтобы понять, в чем проблема, я много чего переработал и удалил дублированный код.

Длинный ответ ниже ...

Сначала краткий обзор проблемы

У нас есть объект, который должен исходить из API или любого другого объекта, который содержит список вопросов для пользователя и одну или дополнительную информацию, например заголовок опроса.

Прямо сейчас образцы данных извлекаются в App.js с помощью:

var data = require("./payload.json");

Законно, даже если, используя ES6, его можно записать (для соответствия остальной части кода) следующим образом:

import data from "./payload.json"

Если посмотреть в файл payload.json, можно ли увидеть, что есть 3 возможных типа вопросов:

  1. с 3-мя опциями, которые ведут себя как флажки (даже если в вашем коде они радио) и текст;
  2. только с текстом;
  3. с загрузчиком изображений;

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

Пользователь также должен иметь возможность перемещаться по его / ее ответам.

Цель

Мне показалось, что вам в основном ... нужна эта форма для работы и сохранения всех ответов пользователя в состоянии (я полагаю, что вы должны отправить их в API когда-нибудь).

App.js

Это основной компонент приложения или, по крайней мере, образца.

Здесь мы должны:

  1. принимать / обрабатывать вопросы, полученные откуда-то;
  2. сделать текущий вопрос;
  3. возможность перемещаться по вопросам;
  4. обрабатывать ответы пользователей;

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

Вместо этого я подумал создать уникальный массив пустых ответов с разными полями для каждого типа вопроса. Поэтому я создал файл Answers.js, в котором можно обрабатывать несколько вещей.

Во-первых, у меня есть константы, содержащие поля (и их значения по умолчанию) для различных типов вопросов:

// Answers.js    

const RADIO_FIELDS = {
  options: {
    option1: false,
    option2: false,
    option3: false
  }
};

const TEXT_FIELDS = {
  text: ""
};

const UPLOAD_FIELDS = {
  file: "",
  imageUrl: ""
};

Я также создал другой файл Question.js, в котором я определяю некоторые служебные константы для работы с типами вопросов, определенными в ваших данных:

// Question.js

export const RADIO_QUESTION_TYPE = "RadioQuestion";
export const TEXT_QUESTION_TYPE = "TextQuestion";
export const UPLOAD_QUESTION_TYPE = "FileUpload";

export const QuestionTypes = {
  RADIO: RADIO_QUESTION_TYPE,
  TEXT: TEXT_QUESTION_TYPE,
  UPLOAD: UPLOAD_QUESTION_TYPE
};

Установлены утилиты, в файле Answers.js я создал метод, который возвращает простой объект «шаблон ответа», начиная с вопроса. Чтобы получить результат, я просто проверяю тип вопроса и объединяю некоторые общие поля со специальными полями типа вопроса:

// Answers.js

const fromQuestion = question => {
  const baseAnswer = {
    id: question.id,
    type: question.question_type
  };

  switch (question.question_type) {
    case QuestionTypes.RADIO:
      return Object.assign({}, baseAnswer, RADIO_FIELDS, TEXT_FIELDS);
    case QuestionTypes.TEXT:
      return Object.assign({}, baseAnswer, TEXT_FIELDS);
    case QuestionTypes.UPLOAD:
      return Object.assign({}, baseAnswer, UPLOAD_FIELDS);
    default:
      return null;
  }
};

Здесь я принял решение об обработке вопросов неизвестного типа: возвращение нулевого значения в случае переключателя по умолчанию отфильтрует эти вопросы от отображения.

Опять же, я написал очень маленький служебный метод, который выполняет эту проверку:

// Answers.js

const isValid = answer => answer !== null;

Наконец, метод сопоставляет все вопросы со всеми (действительными) «шаблонами ответов», а затем экспортируется как простой объект (просто потому, что мне нравится синтаксис, который мы получим в App.js компоненте когда мы будем использовать этот метод):

// Answers.js

const from = questions =>
  (questions || []).map(fromQuestion).filter(isValid);

export default {
  from
};

На данный момент мы можем импортировать Answers.js в App.js и установить переменную состояния следующим образом:

// App.js

...
import Answers from './Answers';

export default class App extends React.Component {
  state = {
    answers: Answers.from(data.questions),
    index: 0
  };

  ...

У нас будет этот объект внутри свойства answers:

[
  {
    id: 2501,
    type: "RadioQuestion",
    options: {
      option1: false,
      option2: false,
      option3: false
    },
    text: ""
  }, {
    id: 2447,
    type: "TextQuestion",
    text: ""
  }, {
  ...

Хорошо, у нас есть структура данных и простой способ получить ее из необработанных данных.

Теперь мы должны правильно задать вопрос. В вашем коде бывает так:

// App.js, render method

...

render() {

  const question = () => {
    switch (data.questions[this.state.index].question_type) {
      case "RadioQuestion":
        return (
          <RadioQuestion
            radioQuestion={this.state.radioQuestions[this.state.index]}
            handleRadio={this.handleRadio}
          />
        );
      case "TextQuestion":
        return (
          <TextQuestion
            textQuestion={this.state.textQuestions[this.state.index]}
            handleText={this.handleText}
          />
        );
      case "FileUpload":
        return <FileUpload index={this.state.index} />;
      default:
        return <h2>Unknown Question Format</h2>;
    }
  };

  return (

  ...
  </label>

  { question() }

  <div className="form-group input-margin-top">
  ...

Вы выполняете метод, который генерирует некоторый контент для рендеринга ... это не более чем компонент!

Итак, чтобы сделать это способом React, в файле Question.js я определил компонент Question, похожий на ваш:

// Question.js

const Question = ({ type, ...props }) => {
  switch (type) {
    case RADIO_QUESTION_TYPE:
      return <RadioQuestion {...props} />;
    case TEXT_QUESTION_TYPE:
      return <TextQuestion {...props} />;
    case UPLOAD_QUESTION_TYPE:
      return <FileUpload {...props} />;
    default:
      return <h1>Unknown Question Format</h1>;
  }
};

export default Question;

Я деструктурирую тип prop и создаю другой компонент, просто передавая все остальные реквизиты.

ПРОДОЛЖЕНИЕ ПРОДОЛЖИТЬ РЕДАКТИРОВАТЬ (я боюсь все потерять!)

0 голосов
/ 08 января 2019

Я думаю, что главная проблема здесь в том, что ваше состояние содержит массив с объектами, которые не клонируются с использованием функции Array.slice. Обратите внимание, что эта функция выполняет только поверхностное копирование массива, поэтому объекты внутри него остаются неизменными при переходах между состояниями.

Я рекомендую использовать ImmutableJS для обработки мутаций состояния глубоких объектов.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...