React application design: parent component that controls access to backend and needs to keep its state...











up vote
0
down vote

favorite












I'm not satisfied with a solution I came up for a React application, since I'm new with the technology but I'm seeing some code that feels like anti-patterns. I have a parent component that communicates to the backend through React Context (using the Context for service layer) and needs to update its state when its child components change something about it. I'll simplify it below:



ParentComponent.jsx



import React, { Component } from 'react';
import Search from './search';
import { ActionTypes } from 'context/searchProvider';
import { PropTypes } from 'prop-types';

export default class ParentComponent extends Component {
static propTypes = {
dispatch: PropTypes.func.isRequired,
items: PropTypes.array.isRequired,
history: PropTypes.object.isRequired,
errors: PropTypes.object.isRequired,
location: PropTypes.object.isRequired,
};

constructor(props) {
super(props);
this.state = {
item: {
fieldOne: '',
fieldTwo: '',
...
fieldN: ''
},
fieldN1: '',
selectField: [{ text: 'My Text', value: '1' }, { text: 'Another Text', value: '2' }],
selectList: selectList,
errors: props.errors,
searchParams: () => ({
fieldOne: this.state.fieldOne,
fieldTwo: this.state.fieldTwo,
fieldThree: this.state.fieldThree
}),
};
}

updateFieldOne(fieldOneUpdated) {
this.setState({ fieldOne: fieldOneUpdated });
}

updateFieldTwo(fieldTwoUpdated) {
this.setState({ fieldTwo: fieldTwoUpdated });
}


// One update method for each field
...

validateForm() {
if (this.state.fieldOne === 'Some value') {
this.setState({
errors: {
fieldTwo: this.validateFieldOne(this.state.fieldOne),
fieldThree: this.validateFieldOne(this.state.fieldThree)
}
});
} else {
this.setState({
errors: {
fieldFour: this.validateFieldFour(this.state.fieldFour),
fieldFive this.validateFieldFive(this.state.fieldFive)
}
});
}
}

// One validate method for fields that need to be validated
...

sendSearch() {
if (this.state.fieldOne === 'USA') {
if (this.state.errors.fieldTwo.length === 0 && this.state.errors.fieldThree.length === 0 &&) {
this.dispatchDetails();
}
} else if (this.state.errors.fieldFour.length === 0 && this.state.errors.fieldFive.length === 0) {
this.dispatchDetails();
}
}

dispatchDetails() {
const params = this.state.searchParams();
this.props.dispatch({ type: ActionTypes.POSTSEARCH }, params, () => {
if (Object.keys(this.props.errors).length) {
this.setState({ errors: this.props.errors });
} else {
this.props.history.push('/nextPage?' + utilities.convertObjectToQueryString(params));
}
});
}

render() {
return (
<div
className={!this.state.showRecentSearches ? 'container-fluid no-recent-searches' : 'container-fluid'}
id="content"
>
{
this.state.showRecentSearches ?
<RecentSearches data={this.props.recentItems} dispatch={this.props.dispatch} history={this.props.history} />
:
null
}
<Search
item={this.state}
fieldOne={this.updateFieldOne}
fieldTwo={this.updateFieldTwo}
fieldThree={this.updatefieldThree}
...
selectList={this.state.selectList}
secondSelectList={this.state.selectField}
errors={this.state.errors}
doSearch={this.validateForm}
history={this.props.history}
/>
</div>
);
}
};


Search.jsx



import React, { Component } from 'react';
import Search from './search';
import { ActionTypes } from 'context/searchProvider';
import { PropTypes } from 'prop-types';

export default class Search extends Component {
static propTypes = {
item: PropTypes.object.isRequired,
fieldOne: PropTypes.func.isRequired,
fieldTwo: PropTypes.func.isRequired,
...
}

render() {
const fieldContent =
this.props.fieldOne === 'Some value' ? (
<FieldsSection
item={this.props.item}
updateFieldOne={this.props.fieldOne}
updateFieldTwo={this.props.fieldTwo}
...
errors={this.props.errors}
/>
) : (
<DifferentFieldsSection
selectList={this.props.selectList}
selection={this.props.selection}
onChange={this.props.updateFieldFour}
errors={this.props.errors}
/>
);
return fieldContent;
}
}


FieldsSection.jsx (DifferentFieldsSection is similar)



import React, { Component } from 'react';
import {
NumberInputControl,
DatePickerInputControl,
SelectControl,
TextInputControl,
} from 'components/common/reactBootstrapWrappers';

export default class FieldsSection extends Component {
static propTypes = {
item: PropTypes.object.isRequired,
updateFieldOne: PropTypes.func.isRequired,
updateFieldTwo: PropTypes.func.isRequired,
updatecity: PropTypes.func.isRequired,
...
errors: PropTypes.array.isRequired
};

render() {
const {
item,
updateFieldOne,
updateaddress2,
updateFieldTwo,
...
errors,
} = this.props;

const stateInfoDetail = this.getstateInfoDetail();

return (
<div>
<TextInputControl
ref={(input) => {
value={item.fieldOne}
onChange={updateFieldOne}
...
errors={errors.fieldOne}
/>
<TextInputControl

value={item.fieldTwo}
onChange={updateFieldTwo}
...
errors={errors.fieldTwo}
/>

// One control for each field passed to the component
...
);
}
}


Explanation



ParentComponent is the component that renders the page and communicates with the backend. It receives a dispatch property from the Routes.jsx components (which uses react-router) by being wrapped around a Context Provider and consumer like so:



@withRouter
export default class Routes extends Component {
render() {
return (
<div>
<SearchProvider>
<SearchConsumer>
{
{ dispatch, errors } =>
(
<Switch>
<Route path="/search" component={ParentComponent} dispatch={dispatch} ... />
</Switch>
)
}
</SearchConsumer>
</SearchProvider>
</div>
);
}
}


It validates the fields that are completed in either FieldsSection component and posts them to the back-end in the dispatchDetails() method. In order to do the validation, it needs to keep its state updated. In order to do that, it injects update functions (e.g updateFieldOne()) to its child component Search, which in turn injects it to either FieldsSection component. Those update functions are then called by one of the components that wrap react-bootstrap controls when the user modifies their value.



The logic that renders the FieldsSection components is complicated enough that it gets wrapped in the Search component, which is also used elsewhere in other components, while the ParentComponent also renders other elements that I excluded for simplicity's sake. But in those components, you also need to inject those update functions to keep the respective parent component's state updated. Also, the parent components are the only ones that get to consume the dispatch function to POST or GET information to the backend.



The SearchConsumer code isn't pretty neither:



import React, { Component } from 'react';
import { PropTypes } from 'prop-types';

import api from 'apis/api';

const Context = React.createContext();

export const ActionTypes = {
GETDATA: 'GETDATA',
POSTSEARCH: 'POSTSEARCH',
... // Other methods
};

export class SearchProvider extends Component {
static propTypes = {
children: PropTypes.any,
};

static defaultProps = {
children: ,
};

constructor() {
super();
this.reducer = this.reducer.bind(this);
}

state = {
items: ,
dispatch: (action, params, callback) => {
this.reducer(action, params, callback);
},
result: {},
errors: {},
};

reducer = (action, params, callback) => {
switch (action.type) {
case ActionTypes.GETDATA:
this.getData();
break;
case ActionTypes.POSTSEARCH:
this.postSearch(params, callback);
break;
... // Other methods
}
};

getData = () => {
api
.getItems()
.then((result) => {
if (result) {
this.setState({ items: result.Results });
}
})
.catch(() => {})
}

postSearch = () => {
api
.postSearch()
.then((result) => {
if (result) {
if (result.Results.length === 1) {
this.setState({ result: result.Results[0], errors: {} }, callback);
}
}
})
.catch((error) => {
if (error && error.FieldErrors) {
this.setState({ errors: error.FieldErrors, result: {} }, callback);
}
})
}

... // Other methods


render() {
return <Context.Provider value={this.state}>{this.props.children}</Context.Provider>;
}

}

export const SearchConsumer = Context.Consumer;


I'm sure the successive injections of the update functions and the way the SearchConsumer and SearchProvider are built are wrong or disencouraged, at least. But I don't know what alternatives I have.



Thank you.










share|improve this question


























    up vote
    0
    down vote

    favorite












    I'm not satisfied with a solution I came up for a React application, since I'm new with the technology but I'm seeing some code that feels like anti-patterns. I have a parent component that communicates to the backend through React Context (using the Context for service layer) and needs to update its state when its child components change something about it. I'll simplify it below:



    ParentComponent.jsx



    import React, { Component } from 'react';
    import Search from './search';
    import { ActionTypes } from 'context/searchProvider';
    import { PropTypes } from 'prop-types';

    export default class ParentComponent extends Component {
    static propTypes = {
    dispatch: PropTypes.func.isRequired,
    items: PropTypes.array.isRequired,
    history: PropTypes.object.isRequired,
    errors: PropTypes.object.isRequired,
    location: PropTypes.object.isRequired,
    };

    constructor(props) {
    super(props);
    this.state = {
    item: {
    fieldOne: '',
    fieldTwo: '',
    ...
    fieldN: ''
    },
    fieldN1: '',
    selectField: [{ text: 'My Text', value: '1' }, { text: 'Another Text', value: '2' }],
    selectList: selectList,
    errors: props.errors,
    searchParams: () => ({
    fieldOne: this.state.fieldOne,
    fieldTwo: this.state.fieldTwo,
    fieldThree: this.state.fieldThree
    }),
    };
    }

    updateFieldOne(fieldOneUpdated) {
    this.setState({ fieldOne: fieldOneUpdated });
    }

    updateFieldTwo(fieldTwoUpdated) {
    this.setState({ fieldTwo: fieldTwoUpdated });
    }


    // One update method for each field
    ...

    validateForm() {
    if (this.state.fieldOne === 'Some value') {
    this.setState({
    errors: {
    fieldTwo: this.validateFieldOne(this.state.fieldOne),
    fieldThree: this.validateFieldOne(this.state.fieldThree)
    }
    });
    } else {
    this.setState({
    errors: {
    fieldFour: this.validateFieldFour(this.state.fieldFour),
    fieldFive this.validateFieldFive(this.state.fieldFive)
    }
    });
    }
    }

    // One validate method for fields that need to be validated
    ...

    sendSearch() {
    if (this.state.fieldOne === 'USA') {
    if (this.state.errors.fieldTwo.length === 0 && this.state.errors.fieldThree.length === 0 &&) {
    this.dispatchDetails();
    }
    } else if (this.state.errors.fieldFour.length === 0 && this.state.errors.fieldFive.length === 0) {
    this.dispatchDetails();
    }
    }

    dispatchDetails() {
    const params = this.state.searchParams();
    this.props.dispatch({ type: ActionTypes.POSTSEARCH }, params, () => {
    if (Object.keys(this.props.errors).length) {
    this.setState({ errors: this.props.errors });
    } else {
    this.props.history.push('/nextPage?' + utilities.convertObjectToQueryString(params));
    }
    });
    }

    render() {
    return (
    <div
    className={!this.state.showRecentSearches ? 'container-fluid no-recent-searches' : 'container-fluid'}
    id="content"
    >
    {
    this.state.showRecentSearches ?
    <RecentSearches data={this.props.recentItems} dispatch={this.props.dispatch} history={this.props.history} />
    :
    null
    }
    <Search
    item={this.state}
    fieldOne={this.updateFieldOne}
    fieldTwo={this.updateFieldTwo}
    fieldThree={this.updatefieldThree}
    ...
    selectList={this.state.selectList}
    secondSelectList={this.state.selectField}
    errors={this.state.errors}
    doSearch={this.validateForm}
    history={this.props.history}
    />
    </div>
    );
    }
    };


    Search.jsx



    import React, { Component } from 'react';
    import Search from './search';
    import { ActionTypes } from 'context/searchProvider';
    import { PropTypes } from 'prop-types';

    export default class Search extends Component {
    static propTypes = {
    item: PropTypes.object.isRequired,
    fieldOne: PropTypes.func.isRequired,
    fieldTwo: PropTypes.func.isRequired,
    ...
    }

    render() {
    const fieldContent =
    this.props.fieldOne === 'Some value' ? (
    <FieldsSection
    item={this.props.item}
    updateFieldOne={this.props.fieldOne}
    updateFieldTwo={this.props.fieldTwo}
    ...
    errors={this.props.errors}
    />
    ) : (
    <DifferentFieldsSection
    selectList={this.props.selectList}
    selection={this.props.selection}
    onChange={this.props.updateFieldFour}
    errors={this.props.errors}
    />
    );
    return fieldContent;
    }
    }


    FieldsSection.jsx (DifferentFieldsSection is similar)



    import React, { Component } from 'react';
    import {
    NumberInputControl,
    DatePickerInputControl,
    SelectControl,
    TextInputControl,
    } from 'components/common/reactBootstrapWrappers';

    export default class FieldsSection extends Component {
    static propTypes = {
    item: PropTypes.object.isRequired,
    updateFieldOne: PropTypes.func.isRequired,
    updateFieldTwo: PropTypes.func.isRequired,
    updatecity: PropTypes.func.isRequired,
    ...
    errors: PropTypes.array.isRequired
    };

    render() {
    const {
    item,
    updateFieldOne,
    updateaddress2,
    updateFieldTwo,
    ...
    errors,
    } = this.props;

    const stateInfoDetail = this.getstateInfoDetail();

    return (
    <div>
    <TextInputControl
    ref={(input) => {
    value={item.fieldOne}
    onChange={updateFieldOne}
    ...
    errors={errors.fieldOne}
    />
    <TextInputControl

    value={item.fieldTwo}
    onChange={updateFieldTwo}
    ...
    errors={errors.fieldTwo}
    />

    // One control for each field passed to the component
    ...
    );
    }
    }


    Explanation



    ParentComponent is the component that renders the page and communicates with the backend. It receives a dispatch property from the Routes.jsx components (which uses react-router) by being wrapped around a Context Provider and consumer like so:



    @withRouter
    export default class Routes extends Component {
    render() {
    return (
    <div>
    <SearchProvider>
    <SearchConsumer>
    {
    { dispatch, errors } =>
    (
    <Switch>
    <Route path="/search" component={ParentComponent} dispatch={dispatch} ... />
    </Switch>
    )
    }
    </SearchConsumer>
    </SearchProvider>
    </div>
    );
    }
    }


    It validates the fields that are completed in either FieldsSection component and posts them to the back-end in the dispatchDetails() method. In order to do the validation, it needs to keep its state updated. In order to do that, it injects update functions (e.g updateFieldOne()) to its child component Search, which in turn injects it to either FieldsSection component. Those update functions are then called by one of the components that wrap react-bootstrap controls when the user modifies their value.



    The logic that renders the FieldsSection components is complicated enough that it gets wrapped in the Search component, which is also used elsewhere in other components, while the ParentComponent also renders other elements that I excluded for simplicity's sake. But in those components, you also need to inject those update functions to keep the respective parent component's state updated. Also, the parent components are the only ones that get to consume the dispatch function to POST or GET information to the backend.



    The SearchConsumer code isn't pretty neither:



    import React, { Component } from 'react';
    import { PropTypes } from 'prop-types';

    import api from 'apis/api';

    const Context = React.createContext();

    export const ActionTypes = {
    GETDATA: 'GETDATA',
    POSTSEARCH: 'POSTSEARCH',
    ... // Other methods
    };

    export class SearchProvider extends Component {
    static propTypes = {
    children: PropTypes.any,
    };

    static defaultProps = {
    children: ,
    };

    constructor() {
    super();
    this.reducer = this.reducer.bind(this);
    }

    state = {
    items: ,
    dispatch: (action, params, callback) => {
    this.reducer(action, params, callback);
    },
    result: {},
    errors: {},
    };

    reducer = (action, params, callback) => {
    switch (action.type) {
    case ActionTypes.GETDATA:
    this.getData();
    break;
    case ActionTypes.POSTSEARCH:
    this.postSearch(params, callback);
    break;
    ... // Other methods
    }
    };

    getData = () => {
    api
    .getItems()
    .then((result) => {
    if (result) {
    this.setState({ items: result.Results });
    }
    })
    .catch(() => {})
    }

    postSearch = () => {
    api
    .postSearch()
    .then((result) => {
    if (result) {
    if (result.Results.length === 1) {
    this.setState({ result: result.Results[0], errors: {} }, callback);
    }
    }
    })
    .catch((error) => {
    if (error && error.FieldErrors) {
    this.setState({ errors: error.FieldErrors, result: {} }, callback);
    }
    })
    }

    ... // Other methods


    render() {
    return <Context.Provider value={this.state}>{this.props.children}</Context.Provider>;
    }

    }

    export const SearchConsumer = Context.Consumer;


    I'm sure the successive injections of the update functions and the way the SearchConsumer and SearchProvider are built are wrong or disencouraged, at least. But I don't know what alternatives I have.



    Thank you.










    share|improve this question
























      up vote
      0
      down vote

      favorite









      up vote
      0
      down vote

      favorite











      I'm not satisfied with a solution I came up for a React application, since I'm new with the technology but I'm seeing some code that feels like anti-patterns. I have a parent component that communicates to the backend through React Context (using the Context for service layer) and needs to update its state when its child components change something about it. I'll simplify it below:



      ParentComponent.jsx



      import React, { Component } from 'react';
      import Search from './search';
      import { ActionTypes } from 'context/searchProvider';
      import { PropTypes } from 'prop-types';

      export default class ParentComponent extends Component {
      static propTypes = {
      dispatch: PropTypes.func.isRequired,
      items: PropTypes.array.isRequired,
      history: PropTypes.object.isRequired,
      errors: PropTypes.object.isRequired,
      location: PropTypes.object.isRequired,
      };

      constructor(props) {
      super(props);
      this.state = {
      item: {
      fieldOne: '',
      fieldTwo: '',
      ...
      fieldN: ''
      },
      fieldN1: '',
      selectField: [{ text: 'My Text', value: '1' }, { text: 'Another Text', value: '2' }],
      selectList: selectList,
      errors: props.errors,
      searchParams: () => ({
      fieldOne: this.state.fieldOne,
      fieldTwo: this.state.fieldTwo,
      fieldThree: this.state.fieldThree
      }),
      };
      }

      updateFieldOne(fieldOneUpdated) {
      this.setState({ fieldOne: fieldOneUpdated });
      }

      updateFieldTwo(fieldTwoUpdated) {
      this.setState({ fieldTwo: fieldTwoUpdated });
      }


      // One update method for each field
      ...

      validateForm() {
      if (this.state.fieldOne === 'Some value') {
      this.setState({
      errors: {
      fieldTwo: this.validateFieldOne(this.state.fieldOne),
      fieldThree: this.validateFieldOne(this.state.fieldThree)
      }
      });
      } else {
      this.setState({
      errors: {
      fieldFour: this.validateFieldFour(this.state.fieldFour),
      fieldFive this.validateFieldFive(this.state.fieldFive)
      }
      });
      }
      }

      // One validate method for fields that need to be validated
      ...

      sendSearch() {
      if (this.state.fieldOne === 'USA') {
      if (this.state.errors.fieldTwo.length === 0 && this.state.errors.fieldThree.length === 0 &&) {
      this.dispatchDetails();
      }
      } else if (this.state.errors.fieldFour.length === 0 && this.state.errors.fieldFive.length === 0) {
      this.dispatchDetails();
      }
      }

      dispatchDetails() {
      const params = this.state.searchParams();
      this.props.dispatch({ type: ActionTypes.POSTSEARCH }, params, () => {
      if (Object.keys(this.props.errors).length) {
      this.setState({ errors: this.props.errors });
      } else {
      this.props.history.push('/nextPage?' + utilities.convertObjectToQueryString(params));
      }
      });
      }

      render() {
      return (
      <div
      className={!this.state.showRecentSearches ? 'container-fluid no-recent-searches' : 'container-fluid'}
      id="content"
      >
      {
      this.state.showRecentSearches ?
      <RecentSearches data={this.props.recentItems} dispatch={this.props.dispatch} history={this.props.history} />
      :
      null
      }
      <Search
      item={this.state}
      fieldOne={this.updateFieldOne}
      fieldTwo={this.updateFieldTwo}
      fieldThree={this.updatefieldThree}
      ...
      selectList={this.state.selectList}
      secondSelectList={this.state.selectField}
      errors={this.state.errors}
      doSearch={this.validateForm}
      history={this.props.history}
      />
      </div>
      );
      }
      };


      Search.jsx



      import React, { Component } from 'react';
      import Search from './search';
      import { ActionTypes } from 'context/searchProvider';
      import { PropTypes } from 'prop-types';

      export default class Search extends Component {
      static propTypes = {
      item: PropTypes.object.isRequired,
      fieldOne: PropTypes.func.isRequired,
      fieldTwo: PropTypes.func.isRequired,
      ...
      }

      render() {
      const fieldContent =
      this.props.fieldOne === 'Some value' ? (
      <FieldsSection
      item={this.props.item}
      updateFieldOne={this.props.fieldOne}
      updateFieldTwo={this.props.fieldTwo}
      ...
      errors={this.props.errors}
      />
      ) : (
      <DifferentFieldsSection
      selectList={this.props.selectList}
      selection={this.props.selection}
      onChange={this.props.updateFieldFour}
      errors={this.props.errors}
      />
      );
      return fieldContent;
      }
      }


      FieldsSection.jsx (DifferentFieldsSection is similar)



      import React, { Component } from 'react';
      import {
      NumberInputControl,
      DatePickerInputControl,
      SelectControl,
      TextInputControl,
      } from 'components/common/reactBootstrapWrappers';

      export default class FieldsSection extends Component {
      static propTypes = {
      item: PropTypes.object.isRequired,
      updateFieldOne: PropTypes.func.isRequired,
      updateFieldTwo: PropTypes.func.isRequired,
      updatecity: PropTypes.func.isRequired,
      ...
      errors: PropTypes.array.isRequired
      };

      render() {
      const {
      item,
      updateFieldOne,
      updateaddress2,
      updateFieldTwo,
      ...
      errors,
      } = this.props;

      const stateInfoDetail = this.getstateInfoDetail();

      return (
      <div>
      <TextInputControl
      ref={(input) => {
      value={item.fieldOne}
      onChange={updateFieldOne}
      ...
      errors={errors.fieldOne}
      />
      <TextInputControl

      value={item.fieldTwo}
      onChange={updateFieldTwo}
      ...
      errors={errors.fieldTwo}
      />

      // One control for each field passed to the component
      ...
      );
      }
      }


      Explanation



      ParentComponent is the component that renders the page and communicates with the backend. It receives a dispatch property from the Routes.jsx components (which uses react-router) by being wrapped around a Context Provider and consumer like so:



      @withRouter
      export default class Routes extends Component {
      render() {
      return (
      <div>
      <SearchProvider>
      <SearchConsumer>
      {
      { dispatch, errors } =>
      (
      <Switch>
      <Route path="/search" component={ParentComponent} dispatch={dispatch} ... />
      </Switch>
      )
      }
      </SearchConsumer>
      </SearchProvider>
      </div>
      );
      }
      }


      It validates the fields that are completed in either FieldsSection component and posts them to the back-end in the dispatchDetails() method. In order to do the validation, it needs to keep its state updated. In order to do that, it injects update functions (e.g updateFieldOne()) to its child component Search, which in turn injects it to either FieldsSection component. Those update functions are then called by one of the components that wrap react-bootstrap controls when the user modifies their value.



      The logic that renders the FieldsSection components is complicated enough that it gets wrapped in the Search component, which is also used elsewhere in other components, while the ParentComponent also renders other elements that I excluded for simplicity's sake. But in those components, you also need to inject those update functions to keep the respective parent component's state updated. Also, the parent components are the only ones that get to consume the dispatch function to POST or GET information to the backend.



      The SearchConsumer code isn't pretty neither:



      import React, { Component } from 'react';
      import { PropTypes } from 'prop-types';

      import api from 'apis/api';

      const Context = React.createContext();

      export const ActionTypes = {
      GETDATA: 'GETDATA',
      POSTSEARCH: 'POSTSEARCH',
      ... // Other methods
      };

      export class SearchProvider extends Component {
      static propTypes = {
      children: PropTypes.any,
      };

      static defaultProps = {
      children: ,
      };

      constructor() {
      super();
      this.reducer = this.reducer.bind(this);
      }

      state = {
      items: ,
      dispatch: (action, params, callback) => {
      this.reducer(action, params, callback);
      },
      result: {},
      errors: {},
      };

      reducer = (action, params, callback) => {
      switch (action.type) {
      case ActionTypes.GETDATA:
      this.getData();
      break;
      case ActionTypes.POSTSEARCH:
      this.postSearch(params, callback);
      break;
      ... // Other methods
      }
      };

      getData = () => {
      api
      .getItems()
      .then((result) => {
      if (result) {
      this.setState({ items: result.Results });
      }
      })
      .catch(() => {})
      }

      postSearch = () => {
      api
      .postSearch()
      .then((result) => {
      if (result) {
      if (result.Results.length === 1) {
      this.setState({ result: result.Results[0], errors: {} }, callback);
      }
      }
      })
      .catch((error) => {
      if (error && error.FieldErrors) {
      this.setState({ errors: error.FieldErrors, result: {} }, callback);
      }
      })
      }

      ... // Other methods


      render() {
      return <Context.Provider value={this.state}>{this.props.children}</Context.Provider>;
      }

      }

      export const SearchConsumer = Context.Consumer;


      I'm sure the successive injections of the update functions and the way the SearchConsumer and SearchProvider are built are wrong or disencouraged, at least. But I don't know what alternatives I have.



      Thank you.










      share|improve this question













      I'm not satisfied with a solution I came up for a React application, since I'm new with the technology but I'm seeing some code that feels like anti-patterns. I have a parent component that communicates to the backend through React Context (using the Context for service layer) and needs to update its state when its child components change something about it. I'll simplify it below:



      ParentComponent.jsx



      import React, { Component } from 'react';
      import Search from './search';
      import { ActionTypes } from 'context/searchProvider';
      import { PropTypes } from 'prop-types';

      export default class ParentComponent extends Component {
      static propTypes = {
      dispatch: PropTypes.func.isRequired,
      items: PropTypes.array.isRequired,
      history: PropTypes.object.isRequired,
      errors: PropTypes.object.isRequired,
      location: PropTypes.object.isRequired,
      };

      constructor(props) {
      super(props);
      this.state = {
      item: {
      fieldOne: '',
      fieldTwo: '',
      ...
      fieldN: ''
      },
      fieldN1: '',
      selectField: [{ text: 'My Text', value: '1' }, { text: 'Another Text', value: '2' }],
      selectList: selectList,
      errors: props.errors,
      searchParams: () => ({
      fieldOne: this.state.fieldOne,
      fieldTwo: this.state.fieldTwo,
      fieldThree: this.state.fieldThree
      }),
      };
      }

      updateFieldOne(fieldOneUpdated) {
      this.setState({ fieldOne: fieldOneUpdated });
      }

      updateFieldTwo(fieldTwoUpdated) {
      this.setState({ fieldTwo: fieldTwoUpdated });
      }


      // One update method for each field
      ...

      validateForm() {
      if (this.state.fieldOne === 'Some value') {
      this.setState({
      errors: {
      fieldTwo: this.validateFieldOne(this.state.fieldOne),
      fieldThree: this.validateFieldOne(this.state.fieldThree)
      }
      });
      } else {
      this.setState({
      errors: {
      fieldFour: this.validateFieldFour(this.state.fieldFour),
      fieldFive this.validateFieldFive(this.state.fieldFive)
      }
      });
      }
      }

      // One validate method for fields that need to be validated
      ...

      sendSearch() {
      if (this.state.fieldOne === 'USA') {
      if (this.state.errors.fieldTwo.length === 0 && this.state.errors.fieldThree.length === 0 &&) {
      this.dispatchDetails();
      }
      } else if (this.state.errors.fieldFour.length === 0 && this.state.errors.fieldFive.length === 0) {
      this.dispatchDetails();
      }
      }

      dispatchDetails() {
      const params = this.state.searchParams();
      this.props.dispatch({ type: ActionTypes.POSTSEARCH }, params, () => {
      if (Object.keys(this.props.errors).length) {
      this.setState({ errors: this.props.errors });
      } else {
      this.props.history.push('/nextPage?' + utilities.convertObjectToQueryString(params));
      }
      });
      }

      render() {
      return (
      <div
      className={!this.state.showRecentSearches ? 'container-fluid no-recent-searches' : 'container-fluid'}
      id="content"
      >
      {
      this.state.showRecentSearches ?
      <RecentSearches data={this.props.recentItems} dispatch={this.props.dispatch} history={this.props.history} />
      :
      null
      }
      <Search
      item={this.state}
      fieldOne={this.updateFieldOne}
      fieldTwo={this.updateFieldTwo}
      fieldThree={this.updatefieldThree}
      ...
      selectList={this.state.selectList}
      secondSelectList={this.state.selectField}
      errors={this.state.errors}
      doSearch={this.validateForm}
      history={this.props.history}
      />
      </div>
      );
      }
      };


      Search.jsx



      import React, { Component } from 'react';
      import Search from './search';
      import { ActionTypes } from 'context/searchProvider';
      import { PropTypes } from 'prop-types';

      export default class Search extends Component {
      static propTypes = {
      item: PropTypes.object.isRequired,
      fieldOne: PropTypes.func.isRequired,
      fieldTwo: PropTypes.func.isRequired,
      ...
      }

      render() {
      const fieldContent =
      this.props.fieldOne === 'Some value' ? (
      <FieldsSection
      item={this.props.item}
      updateFieldOne={this.props.fieldOne}
      updateFieldTwo={this.props.fieldTwo}
      ...
      errors={this.props.errors}
      />
      ) : (
      <DifferentFieldsSection
      selectList={this.props.selectList}
      selection={this.props.selection}
      onChange={this.props.updateFieldFour}
      errors={this.props.errors}
      />
      );
      return fieldContent;
      }
      }


      FieldsSection.jsx (DifferentFieldsSection is similar)



      import React, { Component } from 'react';
      import {
      NumberInputControl,
      DatePickerInputControl,
      SelectControl,
      TextInputControl,
      } from 'components/common/reactBootstrapWrappers';

      export default class FieldsSection extends Component {
      static propTypes = {
      item: PropTypes.object.isRequired,
      updateFieldOne: PropTypes.func.isRequired,
      updateFieldTwo: PropTypes.func.isRequired,
      updatecity: PropTypes.func.isRequired,
      ...
      errors: PropTypes.array.isRequired
      };

      render() {
      const {
      item,
      updateFieldOne,
      updateaddress2,
      updateFieldTwo,
      ...
      errors,
      } = this.props;

      const stateInfoDetail = this.getstateInfoDetail();

      return (
      <div>
      <TextInputControl
      ref={(input) => {
      value={item.fieldOne}
      onChange={updateFieldOne}
      ...
      errors={errors.fieldOne}
      />
      <TextInputControl

      value={item.fieldTwo}
      onChange={updateFieldTwo}
      ...
      errors={errors.fieldTwo}
      />

      // One control for each field passed to the component
      ...
      );
      }
      }


      Explanation



      ParentComponent is the component that renders the page and communicates with the backend. It receives a dispatch property from the Routes.jsx components (which uses react-router) by being wrapped around a Context Provider and consumer like so:



      @withRouter
      export default class Routes extends Component {
      render() {
      return (
      <div>
      <SearchProvider>
      <SearchConsumer>
      {
      { dispatch, errors } =>
      (
      <Switch>
      <Route path="/search" component={ParentComponent} dispatch={dispatch} ... />
      </Switch>
      )
      }
      </SearchConsumer>
      </SearchProvider>
      </div>
      );
      }
      }


      It validates the fields that are completed in either FieldsSection component and posts them to the back-end in the dispatchDetails() method. In order to do the validation, it needs to keep its state updated. In order to do that, it injects update functions (e.g updateFieldOne()) to its child component Search, which in turn injects it to either FieldsSection component. Those update functions are then called by one of the components that wrap react-bootstrap controls when the user modifies their value.



      The logic that renders the FieldsSection components is complicated enough that it gets wrapped in the Search component, which is also used elsewhere in other components, while the ParentComponent also renders other elements that I excluded for simplicity's sake. But in those components, you also need to inject those update functions to keep the respective parent component's state updated. Also, the parent components are the only ones that get to consume the dispatch function to POST or GET information to the backend.



      The SearchConsumer code isn't pretty neither:



      import React, { Component } from 'react';
      import { PropTypes } from 'prop-types';

      import api from 'apis/api';

      const Context = React.createContext();

      export const ActionTypes = {
      GETDATA: 'GETDATA',
      POSTSEARCH: 'POSTSEARCH',
      ... // Other methods
      };

      export class SearchProvider extends Component {
      static propTypes = {
      children: PropTypes.any,
      };

      static defaultProps = {
      children: ,
      };

      constructor() {
      super();
      this.reducer = this.reducer.bind(this);
      }

      state = {
      items: ,
      dispatch: (action, params, callback) => {
      this.reducer(action, params, callback);
      },
      result: {},
      errors: {},
      };

      reducer = (action, params, callback) => {
      switch (action.type) {
      case ActionTypes.GETDATA:
      this.getData();
      break;
      case ActionTypes.POSTSEARCH:
      this.postSearch(params, callback);
      break;
      ... // Other methods
      }
      };

      getData = () => {
      api
      .getItems()
      .then((result) => {
      if (result) {
      this.setState({ items: result.Results });
      }
      })
      .catch(() => {})
      }

      postSearch = () => {
      api
      .postSearch()
      .then((result) => {
      if (result) {
      if (result.Results.length === 1) {
      this.setState({ result: result.Results[0], errors: {} }, callback);
      }
      }
      })
      .catch((error) => {
      if (error && error.FieldErrors) {
      this.setState({ errors: error.FieldErrors, result: {} }, callback);
      }
      })
      }

      ... // Other methods


      render() {
      return <Context.Provider value={this.state}>{this.props.children}</Context.Provider>;
      }

      }

      export const SearchConsumer = Context.Consumer;


      I'm sure the successive injections of the update functions and the way the SearchConsumer and SearchProvider are built are wrong or disencouraged, at least. But I don't know what alternatives I have.



      Thank you.







      javascript react.js






      share|improve this question













      share|improve this question











      share|improve this question




      share|improve this question










      asked Nov 13 at 13:51









      Heathcliff

      1184




      1184



























          active

          oldest

          votes











          Your Answer





          StackExchange.ifUsing("editor", function () {
          return StackExchange.using("mathjaxEditing", function () {
          StackExchange.MarkdownEditor.creationCallbacks.add(function (editor, postfix) {
          StackExchange.mathjaxEditing.prepareWmdForMathJax(editor, postfix, [["\$", "\$"]]);
          });
          });
          }, "mathjax-editing");

          StackExchange.ifUsing("editor", function () {
          StackExchange.using("externalEditor", function () {
          StackExchange.using("snippets", function () {
          StackExchange.snippets.init();
          });
          });
          }, "code-snippets");

          StackExchange.ready(function() {
          var channelOptions = {
          tags: "".split(" "),
          id: "196"
          };
          initTagRenderer("".split(" "), "".split(" "), channelOptions);

          StackExchange.using("externalEditor", function() {
          // Have to fire editor after snippets, if snippets enabled
          if (StackExchange.settings.snippets.snippetsEnabled) {
          StackExchange.using("snippets", function() {
          createEditor();
          });
          }
          else {
          createEditor();
          }
          });

          function createEditor() {
          StackExchange.prepareEditor({
          heartbeatType: 'answer',
          convertImagesToLinks: false,
          noModals: true,
          showLowRepImageUploadWarning: true,
          reputationToPostImages: null,
          bindNavPrevention: true,
          postfix: "",
          imageUploader: {
          brandingHtml: "Powered by u003ca class="icon-imgur-white" href="https://imgur.com/"u003eu003c/au003e",
          contentPolicyHtml: "User contributions licensed under u003ca href="https://creativecommons.org/licenses/by-sa/3.0/"u003ecc by-sa 3.0 with attribution requiredu003c/au003e u003ca href="https://stackoverflow.com/legal/content-policy"u003e(content policy)u003c/au003e",
          allowUrls: true
          },
          onDemand: true,
          discardSelector: ".discard-answer"
          ,immediatelyShowMarkdownHelp:true
          });


          }
          });














           

          draft saved


          draft discarded


















          StackExchange.ready(
          function () {
          StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fcodereview.stackexchange.com%2fquestions%2f207557%2freact-application-design-parent-component-that-controls-access-to-backend-and-n%23new-answer', 'question_page');
          }
          );

          Post as a guest















          Required, but never shown






























          active

          oldest

          votes













          active

          oldest

          votes









          active

          oldest

          votes






          active

          oldest

          votes
















           

          draft saved


          draft discarded



















































           


          draft saved


          draft discarded














          StackExchange.ready(
          function () {
          StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fcodereview.stackexchange.com%2fquestions%2f207557%2freact-application-design-parent-component-that-controls-access-to-backend-and-n%23new-answer', 'question_page');
          }
          );

          Post as a guest















          Required, but never shown





















































          Required, but never shown














          Required, but never shown












          Required, but never shown







          Required, but never shown

































          Required, but never shown














          Required, but never shown












          Required, but never shown







          Required, but never shown







          Popular posts from this blog

          Terni

          A new problem with tex4ht and tikz

          Sun Ra