Подтвердить что ты не робот

Могут ли контейнеры с реакцией-редукцией() использовать методы lifecyle, такие как componentDidMount?

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

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

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

Кажется, что "правильным" способом было бы как-то инициировать асинхронные вызовы из контейнера . Затем, когда вызов будет возвращен, состояние будет обновлено, и контейнер получит новое состояние и, в свою очередь, передаст их своему безгражданному компоненту через mapStateToProps().

Выполнение асинхронных вызовов в mapStateToProps и mapDispatchToProps (я имею в виду фактически вызов функции async, а не возврат его как свойства) не имеет смысла.

Итак, что я закончил делать, это поместить асинхронный вызов в функцию refreshData(), открытую mapDispatchToProps(), а затем вызвать его из двух или более методов жизненного цикла React: componentDidMount and componentWillReceiveProps.

Есть ли чистый способ обновления состояния хранилища redux без применения вызовов метода жизненного цикла в каждом компоненте, который нуждается в данных async?

Должен ли я делать эти вызовы выше иерархии компонентов (тем самым уменьшая объем этой проблемы, поскольку только компоненты "верхнего уровня" должны будут прослушивать события жизненного цикла)?

Изменить:

Просто так нет путаницы, что я подразумеваю под компонентом connect() ed container, здесь очень простой пример:

import React from 'react';
import { connect } from 'react-redux';
import {action} from './actions.js';

import MyDumbComponent from './myDumbComponent.jsx';

function mapStateToProps(state)
{
  return { something: state.xxxreducer.something  };
}

function mapDispatchToProps(dispatch)
{
  return { 
       doAction: ()=>{dispatch(action())}
  };
}

const MyDumbComponentContainer = connect(
  mapStateToProps,
  mapDispatchToProps
)(MyDumbComponent);

// Uh... how can I hook into to componentDidMount()? This isn't 
// a normal React class.

export default MyDumbComponentContainer;
4b9b3361

Ответ 2

редактирование С помощью ловушек вы теперь можете реализовать обратные вызовы жизненного цикла в функциональном компоненте без сохранения состояния. Хотя это не может напрямую затрагивать все вопросы в вопросе, оно также может обойти некоторые причины желания сделать то, что было первоначально предложено.


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

оригинальный ответ

На сайте Redux есть пример, который показывает, что вам не нужно выполнять оба mapStateToProps и mapDispatchToProps. Вы можете просто использовать connect удивительности для реквизита, а также использовать класс и внедрить методы управления жизненным цикла на немой компоненте.

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

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

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

SplashContainer.js

import { connect } from 'react-redux'
import Splash from '../components/Splash'
import * as actions from '../actions'

const mapStateToProps = (state) => {
  return {
    // whatever you need here
  }
}

const mapDispatchToProps = (dispatch) => {
  return {
    onMount: () => dispatch(actions.splashMount())
  }
}

const SceneSplash = connect(
  mapStateToProps,
  mapDispatchToProps
)(Splash)

export default SceneSplash

Splash.js

import React from 'react'

class Splash extends React.Component {
  render() {
    return (
      <div className="scene splash">
      <span className="fa fa-gear fa-spin"></span>
      </div>
    )
  }

  componentDidMount() {
    const { onMount } = this.props
    onMount()
  }
}

export default Splash

Вы можете видеть, что отправка происходит в подключенном контейнере, и вы можете вообразить в вызове actions.splashMount() мы выдаем асинхронный http-запрос или выполняем другие асинхронные действия с помощью thunks или обещаний.

изменить, чтобы уточнить

Позвольте мне попытаться защитить подход. Я перечитал вопрос и не уверен на 100%, что я решаю главное после него, но потерпите меня. Если я все еще не совсем на ходу, у меня есть модифицированный подход ниже, который может быть ближе к цели.

"это должно быть заполнено при загрузке" - пример выше выполняет это

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

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

"Это означает, что я не могу использовать функциональный компонент без состояния для любого компонента, который нуждается в асинхронных данных". - в вышеприведенном примере компонент отображения не имеет состояния и функционирует. Единственная ссылка - метод жизненного цикла, вызывающий обратный вызов. Ему не нужно знать или заботиться о том, что делает этот обратный вызов. Это не тот случай, когда компонент отображения пытается стать владельцем асинхронной выборки данных - он просто позволяет коду, который обрабатывает, знать, когда произошла конкретная вещь.

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

SplashContainer.js

import { connect } from 'react-redux'
import Splash from '../components/Splash'
import * as actions from '../actions'

const mapStateToProps = (state) => {
  return {
    // whatever you need here
  }
}

const mapDispatchToProps = (dispatch) => {
  dispatch(actions.splashMount())
  return {
    // whatever else here may be needed
  }
}

const SceneSplash = connect(
  mapStateToProps,
  mapDispatchToProps
)(Splash)

export default SceneSplash

Splash.js

import React from 'react'

class Splash extends React.Component {
  // incorporate any this.props references here as desired
  render() {
    return (
      <div className="scene splash">
      <span className="fa fa-gear fa-spin"></span>
      </div>
    )
  }
}

export default Splash

Отправляя действие в mapDispatchToProps, вы позволяете коду для этого действия полностью находиться в контейнере. Фактически, вы запускаете асинхронный вызов, как только создается экземпляр контейнера, а не ожидаете, пока подключенный компонент дисплея раскрутится и будет смонтирован. Однако, если вы не можете начать асинхронный вызов до тех пор, пока не сработает componentDidMount() для компонента дисплея, я думаю, что вы по своей природе обязаны иметь код, как в моем первом примере.

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

Честно говоря, этот второй пример, хотя удаление всего кода, связанного с асинхронным действием, из компонента отображения делает меня несколько забавным, поскольку мы делаем вещи, не связанные с диспетчеризацией, в одноименном функция. И контейнеры на самом деле не имеют componentDidMount, чтобы запустить его в противном случае. Так что я немного извиваюсь и склоняюсь к первому подходу. Он не чистый в смысле "кажется правильным", но в смысле "простой 1-строчный".

Ответ 3

Отъезд redux-saga https://github.com/yelouafi/redux-saga. Это компонент промежуточного программного обеспечения redux, который создает долгоживущих наблюдателей, которые ищут конкретные действия хранилища и могут вызывать функции или функции генератора в ответ. Синтаксис генератора особенно хорош для обработки async, а у redux-saga есть несколько приятных помощников, которые позволяют вам обрабатывать асинхронный код синхронно. См. Некоторые из их примеров. https://github.com/yelouafi/redux-saga/blob/master/examples/async/src/sagas/index.js. Синтаксис генератора может быть затруднен вначале, но, основываясь на нашем опыте, этот синтаксис поддерживает чрезвычайно сложную асинхронную логику, включая debounce, cancelation и join/racing несколько запросов.

Ответ 4

Вы можете сделать это из контейнера. Просто создайте компонент, который расширяет React.Component, но назовите его "Контейнер" где-то в названии. Затем используйте container componentDidMount вместо componentDidMount в презентационном (немом) компоненте , который отображает компонент контейнера. Reducer увидит, что вы отправили действие еще и все еще обновляете состояние, чтобы ваш немой компонент смог получить эти данные.

I TDD, но даже если я не TDD, я выделяю свои немые vs компоненты контейнера через файл. Я ненавижу слишком много в одном файле, особенно если вы смешиваете немой и содержимое контейнера в том же файле, что беспорядок. Я знаю, что люди это делают, но я думаю, что это ужасно.

Я делаю это:

src/components/someDomainFolder/someComponent.js (немой компонент) src/components/someDomainFolder/someComponentContainer.js (например, вы можете использовать React-Redux.. подключили контейнер не связанный презентационный компонент.. и поэтому в someComponentContainer.js у вас есть класс реакции в этом файле, как указано, просто назовите его someComponentContainer расширяет React.Component например.

Ваши объекты mapStateToProps() и mapDispatchToProps() будут глобальными функциями компонента подключенного контейнера вне этого класса контейнера. И connect() будет отображать контейнер, который будет отображать презентационный компонент, но это позволяет сохранить все ваше поведение в вашем файле контейнера, вдали от немого кода презентационного компонента.

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

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

Ответ 5

Следующее предложение @PositiveGuy, вот пример кода, как реализовать компонент контейнера, который может использовать методы жизненного цикла. Я думаю, что это довольно чистый подход, который поддерживает разделение проблем, сохраняя компонент презентации "глупым":

import React from 'react';
import { connect } from 'react-redux'
import { myAction } from './actions/my_action_creator'
import MyPresentationComponent from './my_presentation_component'

const mapStateToProps = state => {
  return {
    myStateSlice: state.myStateSlice
  }
}

const mapDispatchToProps = dispatch => {
  return {
    myAction: () => {
      dispatch(myAction())
    }
  }
}

class Container extends React.Component {
  componentDidMount() {
    //You have lifecycle access now!!
  }

  render() {
    return(
      <MyPresentationComponent
        myStateSlice={this.props.myStateSlice}
        myAction={this.props.myAction}
      />
    )
  }
}

const ContainerComponent = connect(
  mapStateToProps,
  mapDispatchToProps
)(Container)

export default ContainerComponent

Ответ 6

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

var Parent = React.createClass({
  onClick: function(){
    dispatch(myAsyncAction());
  },
  render: function() {
    return <childComp onClick={this.onClick} />;
  }
});

var childComp = React.createClass({
    propTypes:{
    onClick: React.PropTypes.func
  },
  render: function() {
    return <Button onClick={this.props.onClick}>Click me</Button>;
  }
});

childComp является апатридом, поскольку определение onClick определяется родительским.

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

class cntWorkloadChart extends Component {

    ...

    componentWillReceiveProps(nextProps){
        if(nextProps.myStuff.isData){
            if (nextProps.myStuff.isResized) {
                this.onResizeEnd();
            }

            let temp = this.updatePrintingData(nextProps)
            this.selectedFilterData = temp.selectedFilterData;
            this.selectedProjects = temp.selectedProjects;

            let data = nextProps.workloadData.toArray();

            let spread = [];
            if(nextProps.myStuff.isSpread) { 
                spread = this.updateSelectedProjectSpread(nextProps);
                for (var i = 0; i < data.length; i++) {
                    data[i].sumBillableHrsSelectedProjects = spread[data[i].weekCode] ? Number(spread[data[i].weekCode].sumBillableHrsSelectedProjects.toFixed(1)) : 0;
                    data[i].sumCurrentBudgetHrsSelectedProjects = spread[data[i].weekCode] ? Number(spread[data[i].weekCode].sumCurrentBudgetHrsSelectedProjects.toFixed(1)) : 0;
                    data[i].sumHistoricBudgetHrsSelectedProjects = spread[data[i].weekCode] ? Number(spread[data[i].weekCode].sumHistoricBudgetHrsSelectedProjects.toFixed(1)) : 0;
                }
            }

            if (nextProps.potentialProjectSpread.length || this.props.potentialProjectSpread.length) { //nextProps.myStuff.isPpSpread) { ???? - that was undefined
                let potential = nextProps.potentialProjectSpread;
                let ppdd = _.indexBy(potential, 'weekCode');
                for (var i = 0; i < data.length; i++) {
                    data[i].sumSelectedPotentialProjects = ppdd[data[i].weekCode] ? ppdd[data[i].weekCode].sumSelectedPotentialProjects.toFixed(1) : 0;
                }
            }

            for (var i = 0; i < data.length; i++) {
                let currObj = data[i];
                currObj.sumCurrentBudgetHrs = currObj.currentBudgeted.sumWeekHours;
                currObj.sumHistoricBudgetHrs = currObj.historicBudgeted.sumWeekHours;
                currObj.fillAlpha = .6; //Default to .6 before any selections are made

                //RMW-TODO: Perhaps we should update ALL line colors this way? This would clean up zero total bars in all places
                this.updateLineColor(currObj, "sumSelectedPotentialProjects", "potentialLineColor", potentialLineColor);
                this.updateLineColor(currObj, "sumHistoricBudgetHrs", "histLineColor", histBudgetLineColor);
                this.updateLineColor(currObj, "sumHistoricBudgetHrsSelectedProjects", "histSelectedLineColor", selectedHistBudgetFillColor);


            }
            if(nextProps.myStuff.isSelectedWeek){
                let currWeekIndex = nextProps.weekIndex.index; 
                let selectedWeek = data[currWeekIndex].fillAlpha = 1.0;
            }

            if(data.length > 0){
                if(data[0].targetLinePercentages && data.length > 9) { //there are target lines and more than 10 items in the dataset
                    let tlHigh = data[0].targetLinePercentages.targetLineHigh;
                    let tlLow = data[0].targetLinePercentages.targetLineLow;
                    if (tlHigh > 0 && tlLow > 0) {
                        this.addTargetLineGraph = true; 
                        this.upperTarget = tlHigh;
                        this.lowerTarget = tlLow;
                    } 
                }
                else {
                    this.addTargetLineGraph = false;
                    this.upperTarget = null;
                    this.lowerTarget = null;
                }
            }
            this.data = this.transformStoreData(data); 
            this.containsHistorical = nextProps.workloadData.some(currObj=> currObj.historicBudgeted.projectDetails.length);
        }

    }

    ...

    render() {
        return (
            <div id="chartContainer" className="container">
                <WorkloadChart workloadData={this.props.workloadData} 
                                onClick={this.onClick} 
                                onResizeEnd={this.onResizeEnd}
                                weekIndex={this.props.weekIndex} 
                                getChartReference={this.getChartReference}
                                //projectSpread={this.props.projectSpread}
                                selectedRows={this.props.selectedRows}
                                potentialProjectSpread={this.props.potentialProjectSpread}
                                selectedCompany={this.props.selectedCompany} 
                                cascadeFilters={this.props.cascadeFilters} 
                                selectedRows={this.props.selectedRows} 
                                resized={this.props.resized} 
                                selectedFilterData={this.selectedFilterData}
                                selectedProjects={this.selectedProjects}
                                data={this.data}
                                upperTarget={this.upperTarget}
                                lowerTarget={this.lowerTarget}
                                containsHistorical={this.containsHistorical}
                                addTargetLineGraph={this.addTargetLineGraph}
        />


            </div> 
        );
    }
};

function mapStateToProps(state){
    let myValues = getChartValues(state);
    return {
        myStuff: myValues,
        workloadData: state.chartData || new Immutable.List(),
        weekIndex: state.weekIndex || null,
        //projectSpread: state.projectSpread || {},
        selectedRows: state.selectedRows || [],
        potentialProjectSpread: state.potentialProjectSpread || [],
        selectedCompany: state.companyFilter.selectedItems || null,
        brokenOutByCompany: state.workloadGrid.brokenOutByCompany || false,
        gridSortName: state.projectGridSort.name,
        gridSortOrder: state.projectGridSort.order,
        cascadeFilters: state.cascadeFilters || null,
        selectedRows: state.selectedRows || [],
        resized: state.chartResized || false,
        selectedPotentialProjects: state.selectedPotentialProjects || []
    };

}
module.exports = connect(mapStateToProps)(cntWorkloadChart);