חכו שניה עם הקוד הגנרי

21/05/2018

באחד התרגילים בקורס React ביקשתי לכתוב קוד שיציג מידע מתוך אתר swapi. האתר, למי שלא מכיר, מספק מידע על דמויות וסרטי מלחמת הכוכבים והקוד שביקשתי לכתוב כלל שני פקדים: Person שמציג מידע על דמות ו Film שמציג מידע על סרט.

על פניו נראה שיש הרבה מן המשותף בין שני הפקדים: שניהם צריכים לפנות לשרת, להביא JSON Object עם המידע ולהציג אותו. לכן טבעי שאם ננסה לכתוב קוד גנרי לפני שכתבנו שורת קוד אחת נוכל להעלות טיעונים לכל מיני סוגים של חלוקות ואבסטרקציות וכולם יהיו די משכנעים.

בדיוק בגלל זה אני אוהב קודם לכתוב את הקוד בגירסא הלא גנרית שלו. הנה Person ו Film לצורך הדוגמא:

class Person extends React.Component {
  constructor(props) {
    super(props);
    this.state = { loading: false };
  }

  componentDidUpdate(prevProps, prevState) {
    if (prevProps.id !== this.props.id) {
      this.fetchData();
    }
  }

  componentDidMount() {
    this.fetchData();
  }

  fetchData() {    
    this.setState({ loading: true });

    const { id } = this.props;
    $.get(`https://swapi.co/api/people/${id}`).then(res => {
      this.setState({
        data: res,
        loading: false,
      })
    });
  }

  renderLoading() {
    return <p>Loading data for id: {this.props.id}</p>
  }

  renderData() {
    const { data } = this.state;
    if (!data) { return false; }

    return (
      <div>
        <p><b>Name:</b> {data.name}</p>
        <p><b>Gender:</b> {data.gender}</p>
      </div>
    )
  }

  render() {
    if (this.state.loading) {
      return this.renderLoading();
    } else {
      return this.renderData();
    }
  }
}

class Film extends React.Component {
  constructor(props) {
    super(props);
    this.state = { loading: false };
  }

  componentDidUpdate(prevProps, prevState) {
    if (prevProps.id !== this.props.id) {
      this.fetchData();
    }
  }

  componentDidMount() {
    this.fetchData();
  }

  fetchData() {    
    this.setState({ loading: true });

    const { id } = this.props;
    $.get(`https://swapi.co/api/films/${id}`).then(res => {
      this.setState({
        data: res,
        loading: false,
      })
    });
  }

  renderLoading() {
    return <p>Loading data for id: {this.props.id}</p>
  }

  renderData() {
    const { data } = this.state;
    if (!data) { return false; }

    return (
      <div>
        <p><b>Title:</b> {data.title}</p>
        <p><b>Director:</b> {data.director}</p>
      </div>
    )
  }

  render() {
    if (this.state.loading) {
      return this.renderLoading();
    } else {
      return this.renderData();
    }
  }
}

כמה שהם דומים! וזה מצוין כי עכשיו אפשר להתחיל להוציא את החלקים המשותפים למבנה משותף ולהשאיר לכל אחד רק את החלק הרלוונטי עבורו.

מאחר ורוב הקוד המשותף נמצא בפונקציות מחזור החיים הצעד הכי הגיוני הוא להשתמש ב Higer Order Component. כך יראה הקוד של שני הפקדים לאחר העדכון:

@swapiClient('https://swapi.co/api/people')
class Person extends React.Component {
  render() {
    const { data } = this.props;
    if (!data) { return false; }

    return (
      <div>
        <p><b>Name:</b> {data.name}</p>
        <p><b>Gender:</b> {data.gender}</p>
      </div>
    )
  }
}

@swapiClient('https://swapi.co/api/films')
class Film extends React.Component {
  render() {
    const { data } = this.props;
    if (!data) { return false; }

    return (
      <div>
        <p><b>Title:</b> {data.title}</p>
        <p><b>Director:</b> {data.director}</p>
      </div>
    )
  } 
}

ברגע שזיהינו את החלקים המשותפים קל הרבה יותר להוציא החוצה את המשותף למקום אחד ולהשאיר רק את השונה בפקדים עצמם. זה אגב הקוד עבור ה Higher Order Component לקוד המשותף:

function swapiClient(baseUrl) {
  return function(Component) {
    return class SwapiClient extends React.Component {
      constructor(props) {
        super(props);
        this.state = { loading: false };
      }

      componentDidUpdate(prevProps, prevState) {
        if (prevProps.id !== this.props.id) {
          this.fetchData();
        }
      }

      componentDidMount() {
        this.fetchData();
      }

      fetchData() {    
        this.setState({ loading: true });

        const { id } = this.props;
        $.get(`${baseUrl}/${id}`).then(res => {
          this.setState({
            data: res,
            loading: false,
          })
        });
      }

      renderLoading() {
        return <p>Loading data for id: {this.props.id}</p>
      }

      render() {
        const { data } = this.state;

        if (this.state.loading) {
          return this.renderLoading();
        } else {
          return <Component data={data} />
        }
      }
    }
  }
}

ויש גם גירסא חיה בקודפן למי שרוצה לשחק עם זה ולשנות חלק מהשורות: