简体   繁体   中英

Conditional rendering in React if an array is empty

I'm building a cinema listings project. There is a block with information about each film and then the film times below it.

I have 2 dropdown menus - one to select a film, one to select a date. I'm using ternary operators to render the results but can't get it so that if a film doesn't have any showings on a particular day, the block of information about the film is hidden when that date is selected.

I'll just post an example for one of the films.

Here's the json file that the listing information is taken from -

[
    {   
        "id": "film1",
        "filmTitle": "Knives Out",
        "paragraphText": "Lorem ipsum dolor sit amet",
        "mon": ["12:00", "15:00", "19:00"],
        "tue": ["13:10", "16:30", "19:00", "21:00"]
    }
]

Here's part of the js file with one of the film listings -

    constructor(props){
        super(props);
        this.state = {
            filmListings: [],
            selectedFilm: "allFilms",
            selectedDate: "allDates"
        }
        this.handleChange = this.handleChange.bind(this);
    }

    handleChange(event){
        const {name, value} = event.target 
        this.setState({ 
            [name]: value 
        });
    }


    componentDidMount() {
        const FilmListings = require("./components/booking/filmTimesData.json");
        this.setState({ filmListings: FilmListings })
    }

    render() {
        const filmsArray = require("./components/booking/filmTimesData.json");
        const selectedFilm = this.state.selectedFilm
        const selectedDate = this.state.selectedDate


        return (
            <form id="searchForm">
                <div id="filmDateContainer">
                    <div className="searchOption">
                        <h2>Film:</h2>
                        <img src={FilmSearch} alt="film icon"/>
                        <select 
                            name="selectedFilm" 
                            value={this.state.selectedFilm}
                            onChange={this.handleChange}
                            className="dropdown"
                        >
                            <option value="allFilms">All Films</option>
                            <option value="film1">Knives Out</option>
                            <option value="film2">Judy and Punch</option>
                            <option value="film3">Harriet</option>
                        </select>
                    </div>
                    <h2 id="or">OR</h2>
                    <div className="searchOption">
                        <h2>Date:</h2>
                        <img src={DateSearch} alt="date icon"/>
                        <select 
                            name="selectedDate" 
                            value={this.state.selectedDate}
                            onChange={this.handleChange}
                            className="dropdown" 
                        >
                            <option value="mon">Monday 2nd December</option>
                            <option value="tue">Tuesday 3rd December</option>
                            <option value="wed">Wednesday 4th December</option>
                        </select>
                    </div>
                </div>

                        <div>
                            {(selectedFilm === "film1" || selectedFilm === "allFilms") ?
                            <FilmInfo filmTitle={filmsArray[0].filmTitle} paragraphText={filmsArray[0].paragraphText}/> : " "}

                            {(selectedDate === "mon" || selectedDate === "allDates") 
                                && (selectedFilm === "film1" || selectedFilm === "allFilms") ? 
                                <Mon day={filmsArray[0]}/> : " "
                            }


                            {(selectedDate === "tue" || selectedDate === "allDates") 
                                && (selectedFilm === "film1" || selectedFilm === "allFilms") ?
                                <Tue day={filmsArray[0]}/> : " "
                            }


                            {(selectedDate === "wed" || selectedDate === "allDates") 
                                && (selectedFilm === "film1" || selectedFilm === "allFilms") ?
                                <Wed day={filmsArray[0]}/> : " "
                            }
                        </div>
            </form>
        );
    }
}

In this example, there's no showing of the film on a Wednesday so how can I get the info block for the film not to show when Wednesday is selected from the dropdown list?

I think that trying to limit it to one film item as opposed to looping over multiple ones has unfortunately made things more complicated, not less. It seems like you're working backwards from the conclusion that, "some films have a Wed showing, therefore we need to conditionally render a Wed component". In other words, starting with the difference between the data points rather than what they have in common.

We could write some convoluted conditional that checks whether a particular property exists, but it will be very brittle and you'll probably end up throwing it out anyway once you move on to mapping everything. It makes more sense to just be agnostic about which specific properties each film object has and allow the data to flow through your component more naturally.

Starting with your JSON file, group the showings data into a set of more discrete properties. You'll now be able to easily access and loop over the showings rather than trying to access each one individually by name.

[
    {   
        "id": "film1",
        "filmTitle": "Knives Out",
        "paragraphText": "Lorem ipsum dolor sit amet",
        "showings": [
            {"date": "mon", "times": ["12:00", "15:00", "19:00"]},
            {"date": "tue", "times": ["13:10", "16:30", "19:00", "21:00"]}
        ]
    }
]

On to the component. Let's start by getting rid of the unnecessary lifecycle method, and some render variables. Since your data is coming from a static, local file, you can just import it at the top of the component and include it directly in the constructor . It only makes sense to use componentDidMount when processing data that is not immediately accessible on mount (eg it is coming from a remote resource, or waiting on some other component in the tree). I'm using import syntax here as it should be available to you in a boilerplate React environment.

import filmData from "./components/booking/filmTimesData.json";

class FilmListings extends React.Component {
    constructor(props){
        super(props);
        this.state = {
            filmListings: filmData,
            selectedFilm: "allFilms",
            selectedDate: "allDates"
        };
        this.handleChange = this.handleChange.bind(this);
    }

    handleChange(event){
        const {name, value} = event.target 
        this.setState({ 
            [name]: value 
        });
    }

    render() {
        const { filmListings, selectedFilm, selectedDate } = this.state;

        return (
            ...
        );
    }
}

Now the render function. I'm going to leave the filmDateContainer alone because it's essentially fine, although of course you'll want to map over the options from the filmListings instead of hardcoding, which should become clearer after this next bit.

We're going to replace the entire div after filmDateContainer with this mapping function, which first loops over each film object to make sure it does not conflict with selectedFilm . It then loops over that film object's showings to make sure each one in turn does not conflit with selectedDate . Essentially, we've created a staggered filter which nests the data by order of importance (crucially, as you may now notice, by the same order that it is structured in the JSON file).

{filmListings.map(film => 
  selectedFilm === "allFilms" || selectedFilm === film.id ? (
    <div key={film.id}>
      <FilmInfo filmTitle={film.filmTitle} paragraphText={film.paragraphText}/>
      {film.showings.map(showing => 
        selectedDate === "allDates" || selectedDate === showing.date ? (
          <Day showing={showing} key={film.id + showing.date}/>
      ) : null)}
    </div>
) : null)}

Remember to assign appropriate, unique keys each time you render a mapping function so that React can keep track of everything. The nesting is also already quite dense so you might want to think about separating each map into its own variable which returns that little snippet of JSX it's responsible for, especially if you have any extra conditions to consider or properties to map.

If you have any questions about any of this drop a comment and I'll try my best to explain.

Edit:

I think the best way to handle the more complex conditional logic you are after is to write some selectors. This is a concept you'll get more familiar with when you move on to more complicated state management, but it can be used just as effectively here by adding some simple functions before your render return. As a bonus, it's also going to clean up the render return which is quite nice because it was already starting to look a bit ugly. By combining selectors you can start to reason about your state in a more logical, readable way, and define common operations which produce specific outcomes.

I also got rid of the ternaries and went for pure && evaluation since it's a bit cleaner.

render() {
  const { filmListings, selectedFilm, selectedDate } = this.state;

  const allFilms = selectedFilm === 'allFilms';
  const allDates = selectedDate === 'allDates';
  const validDate = date => allDates || selectedDate === date;
  const filmHasDate = showings => showings.some(showing => validDate(showing.date));
  const validFilm = (id, showings) => (allFilms || selectedFilm === id) && filmHasDate(showings);

  return (
    <form>
      ...
      {filmListings.map(film => validFilm(film.id, film.showings) && (
        <div key={film.id}>
          <FilmInfo filmTitle={film.filmTitle} paragraphText={film.paragraphText}/>
          {film.showings.map(showing => validDate(showing.date) && (
            <Day showing={showing} key={film.id + showing.date}/>
          ))}
        </div>
      ))}
    </form>
  );
}

One of the drawbacks to this approach is that you are doing all these calculations every time the component rerenders. Depending on the kind of application you have, how often the component rerenders, and how large the input array is, that could limit performance, but in the vast majority of cases its not going to be an issue.

The alternative would be to do these calculations once, only when handleChange is called, and store the result in a new state variable (eg filteredListings ) which you could then map over directly in your render function. However, then you've got duplicate state on your hands with the same data in multiple places, which can be a headache to reason about and synchronise when your data set gets to be any considerable size. Just something to think about!

In your example, you could simply do:

{((selectedDate === "wed" && filmsArray[0][selectedDate]) || selectedDate === "allDates") 
    && (selectedFilm === "film1" || selectedFilm === "allFilms")
    &&
        <Wed day={filmsArray[0]}/>
}

It would check that your film object actually has a "wed" key and conditionally render the Wed component.

Note that I ditched the ? because if the result of

((selectedDate === "wed" && filmsArray[0][selectedDate]) || selectedDate === "allDates") 
    && (selectedFilm === "film1" || selectedFilm === "allFilms")

is false, the component would not be rendered. I just find it cleaner.

You could do this for each of your cases.

Your code would then look like:

<div>
    {(selectedFilm === "film1" || selectedFilm === "allFilms") &&
        <FilmInfo filmTitle={filmsArray[0].filmTitle} paragraphText={filmsArray[0].paragraphText}/>
    }

    {((selectedDate === "mon" && filmsArray[0][selectedDate]) || selectedDate === "allDates") 
        && (selectedFilm === "film1" || selectedFilm === "allFilms")
        &&
            <Mon day={filmsArray[0]}/>
    }

    {((selectedDate === "tue" && filmsArray[0][selectedDate]) || selectedDate === "allDates") 
        && (selectedFilm === "film1" || selectedFilm === "allFilms")
        &&
            <Tue day={filmsArray[0]}/>
    }

    {((selectedDate === "wed" && filmsArray[0][selectedDate]) || selectedDate === "allDates") 
        && (selectedFilm === "film1" || selectedFilm === "allFilms")
        &&
            <Wed day={filmsArray[0]}/>
    }
</div>

But I do think that you should re-architecture your solution to have a simpler and more robust way to display the desired component. Something in the line of @lawrencee-witt suggestion.

The final solution would be heavily influenced by the control you have over that JSON file.

I can create a small CodePen example if you want more info.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM