Могут ли контейнеры с реакцией-редукцией() использовать методы 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;
Ответы
Ответ 1
Джейми Диксон написал пакет, чтобы сделать это!
https://github.com/JamieDixon/react-lifecycle-component
Использование будет выглядеть так:
const mapDispatchToProps = {
componentDidMount: getAllTehDatas
}
...
export default connectWithLifecycle(mapStateToProps, mapDispatchToProps)(WrappedComponent)
Ответ 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);