Search code examples
reactjsreact-reduxconditional-rendering

react-redux: Rendering a component after an API call


I am building an app which uses user input and shows number of recipes and they can click on recipe card to view ingredients as well. Every time they click on recipe card I make an API call to get appropriate recipe ingredient. But I am not able to figure out how to show the component which contains the recipe ingredients. I tried with conditional routing and conditional rendering as well but couldn't find the solution.

Recipe_Template.js

  export class RecipeTemplate extends Component {
  renderRecipe = recipeData => {
    return recipeData.recipes.map(recipeName => {
      return (
        <div className="container">
          <div className="row">
            <div className="col-xs-12 col-sm-12 col-md-12 col-lg-12">
              <a
                href={recipeName.source_url}
                target="_blank"
                onClick={() => {
                  this.props.fetchRecipeId(recipeName.recipe_id);
                }}
              >
                <img
                  src={recipeName.image_url}
                  className="mx-auto d-block img-fluid img-thumbnail"
                  alt={recipeName.title}
                />
                <span>
                  <h3>{recipeName.title}</h3>
                </span>
              </a>
              <span}>
                <h3>{recipeName.publisher}</h3>
              </span>
            </div>
          </div>
        </div>
      );
    });
  };

  render() {
    return (
      <React.Fragment>
        {this.props.recipe.map(this.renderRecipe)}
      </React.Fragment>
    );
  }
}

Recipe_Detail.js

class RecipeDetail extends Component {
  renderRecipeDetail(recipeData) {
    return recipeData.recipe.ingredients.map(recipeIngredient => {
      return <li key={recipeIngredient}>recipeIngredient</li>;
    });
  }

  render() {
    if (this.props.recipeId === null) {
      return <div>Loading...</div>;
    }
    return <ul>{this.props.recipeId.map(this.renderRecipeDetail)}</ul>;
  }
}

function mapStateToProps({ recipeId }) {
  return { recipeId };
}

export default connect(mapStateToProps)(RecipeDetail);

Solution

  • Not entirely sure why you would need Redux here (unless it's being shared among other nested components), but I'm fairly certain you can just utilize React state.


    One approach would be to configure your routes as such:

        <Route path="/recipes" component={Recipes} />
        <Route path="/recipe/:id" component={ShowRecipe} />
    

    When the user sends a query, gets some results, and you display all matching recipes to a Recipes component. Each recipe then has a name (and other associated displayable data) and a clickable link:

     <Link to={`/recipe/id?recipeId=${recipeId}`}>View {recipeName} Recipe</Link>
    

    which for simplicity sake might look like:

    <ul>
      <Link to="/recipe/id?recipeId=08861626">View Prosciutto Bruschetta Recipe</Link>
      <Link to="/recipe/id?recipeId=04326743">View Pasta Bundt Loaf Recipe</Link>
      ...etc
    </ul>
    

    When the user clicks on the link, react-router sends the user to the ShowRecipe component with a unique recipeId.

    ShowRecipe then makes another AJAX request to get the recipe details:

    ShowRecipe.js

    export default class ShowRecipe extends Component {
      state = { recipeDetail: '' }
    
      componentDidMount = () => {
         const { recipeId } = this.props.location.query; // <== only natively available in react-router v3
    
         fetch(`http://someAPI/recipe/id?recipeId=${recipeId}`)
          .then(response => response.json())
          .then(json => this.setState({ recipeDetail: json }));
      }
    
      render = () => (
        !this.state.recipeDetails
          ? <div>Loading...</div>
          : <ul>
             {this.state.recipeDetail.map(ingredient => (
               <li key={ingredient}>ingredient</li>
             )}
            </ul>
      ) 
    }
    

    Another approach:

    Have the recipeDetails stored and available within the original fetched recipes JSON. Then map over the recipes and create multiple <Card key={recipeId} recipeName={recipeName} recipeDetail={recipeDetail} /> components for each recipe.

    which for simplicity sake might look like:

    <div>
      {this.state.recipes.map(({recipeId, recipeName, recipeDetail}), => (
        <Card key={recipeId} recipeName={recipeName} recipeDetail={recipeDetail} />
      )}
    </div>
    

    Then each individual Card has it's own state:

    Card.js

    export default class Card extends Component {
          state = { showDetails: '' }
    
          toggleShowDetails = () => this.setState(prevState => ({ showDetails: !this.state.showDetails }))     
    
          render = () => (
            <div>
              <h1>{this.props.recipeName} Recipe</h1>
              <button onClick={toggleShowDetails}> {`${!this.state.showDetails ? "Show" : "Hide"} Recipe<button>
              { this.state.showDetails &&
                 <ul>
                  {this.props.recipeDetail.map(ingredient => (
                   <li key={ingredient}>ingredient</li>
                  )}
                </ul>
              }
          ) 
        }
    

    Therefore, by default the recipeDetail is already there, but hidden. However, when a user clicks the Card's button, it will toggle the Card's showDetails state to true/false to display/hide the recipe detail.