Fetching data

We're getting close to having a fully functional end-to-end rendering solution for our React application. The last remaining issue is state, more specifically, data that comes from some API endpoint. Our components need to be able to fetch this data on the server just as they would on the client so that they can generate the appropriate content. We also have to pass the initial state, along with the initial rendered content, to the browser. Otherwise, our code won't know when something has changed after the first render.

To implement this, I'm going to introduce the Flux concept for holding state. Flux is a huge topic that goes well beyond the scope of this book. Just know this: a store is something that holds application state and, when it's changed, React components are notified. Let's implement a basic store before we do anything else:

import EventEmitter from 'events'; 
import { fromJS } from 'immutable'; 
 
// A store is a simple state container that 
// emits change events when the state is updated. 
class Store extends EventEmitter { 
  // If "window" is defined, 
  // it means we're on the client and that we can 
  // expect "INITIAL_STATE" to be there. Otherwise, 
  // we're on the server and we need to set the initial 
  // state that's sent to the client. 
  data = fromJS( 
    typeof window !== 'undefined' ? 
      window.INITIAL_STATE : 
      { firstContent: { items: [] } } 
  ) 
 
  // Getter for "Immutable.js" state data... 
  get state() { 
    return this.data; 
  } 
 
  // Setter for "Immutable.js" state data... 
  set state(data) { 
    this.data = data; 
    this.emit('change', data); 
  } 
} 
 
export default new Store(); 

When the state changes, a change event is emitted. The initial state of the store is set, based on the environment we're in. If we're on the client, we're looking for an INITIAL_STATE object. This is the initial state that is sent from the server, so this store will be used in both the browser and in Node.js.

Now, let's take a look at one of the components that needs API data in order to render. It's going to use the store to coordinate its backend rendering with its frontend rendering:

import React, { Component } from 'react'; 
 
import store from '../store'; 
import FirstContent from './FirstContent'; 
 
class FirstContentContainer extends Component { 
  // Static method that fetches data from an API 
  // endpoint for instances of this component. 
  static fetchData = () => 
    new Promise( 
      resolve => 
        setTimeout(() => { 
          resolve(['One', 'Two', 'Three']); 
        }, 1000) 
    ).then((result) => { 
      // We have to make sure that the data is set properly 
      // in the store before returning the promise. 
      store.state = store.state 
        .updateIn( 
          ['firstContent', 'items'], 
          items => items 
            .clear() 
            .push(...result) 
        ); 
 
      return result; 
    }); 
 
  // The default state of this component comes 
  // from the "store". 
  state = { 
    data: store.state.get('firstContent'), 
  } 
 
  // Getter for "Immutable.js" state data... 
  get data() { 
    return this.state.data; 
  } 
 
  // Setter for "Immutable.js" state data... 
  set data(data) { 
    this.setState({ data }); 
  } 
 
  componentDidMount() { 
    // When the component mounts, we want to listen 
    // changes in store state and re-render when 
    // they happen. 
    store.on('change', () => { 
      this.data = store.state.get('firstContent'); 
    }); 
 
    const items = this.data.get('items'); 
 
    // If the state hasn't been fetched yet, fetch it. 
    if (items.isEmpty()) { 
      FirstContentContainer.fetchData(); 
    } 
  } 
 
  render() { 
    return ( 
      <FirstContent {...this.data.toJS()} /> 
    ); 
  } 
} 
 
export default FirstContentContainer; 

As you can see, the initial state of the component comes from the store. The FirstContent component is then able to render its list, even though it's empty at first. When the component is mounted, it sets up a listener for the store. When the store changes state, it causes this component to re-render because it's calling setState().

There's also a fetchData() static method defined on this component, and this declares the API dependencies this component has. Its job is to return a promise that's resolved when the API call returns and the store state has been updated. The fetchData() method is used by this component when it's mounted in the DOM, if there's no data yet. Otherwise, it means that the server used this method to fetch the state before it was rendered. Let's turn our attention to the server now to see how this is done.

First, we have a helper function that fetches the component data we need for a given request:

// Given a list of components returned from react-router 
// "match()", find their data dependencies and return a 
// promise that's resolved when all component data has 
// been fetched. 
const fetchComponentData = (components) => 
  Promise.all( 
    components 
      .reduce((result, i) => { 
        // If the component is an object, it's 
        // the "components" property of a route. In this 
        // example, it's the "header" and "content" 
        // components. So, we need to iterate over the 
        // the object values to see if any of the components 
        // has a "fetchData()" method. 
        if (typeof i === 'object') { 
          for (const k of Object.keys(i)) { 
            if (i[k].hasOwnProperty('fetchData')) { 
              result.push(i[k]); 
            } 
          } 
        // Otherwise, we assume that the item is a component, 
        // and simply check if it has a "fetchData()" method. 
        } else if (i && i.fetchData) { 
          result.push(i); 
        } 
        return result; 
      }, []) 
      // Call "fetchData()" on all the components, mapping 
      // the promises to "Promise.all()". 
      .map(i => i.fetchData()) 
  ); 

The components argument comes from the match() call. These are all the components that need to be rendered, so this function iterates over them and checks if each one has a fetchData() method. If it does, then the promise that it returns is added to the result.

Now, let's take a look at the request handler that uses this function:

app.get('/*', (req, res) => { 
  match({ 
    routes, 
    location: req.url, 
  }, (err, redirect, props) => { 
    if (err) { 
      res.status(500).send(err.message); 
    } else if (redirect) { 
      res.redirect( 
        302, 
        `${redirect.pathname}${redirect.search}` 
      ); 
    } else if (props) { 
      // If a route match is found, we pass  
      // "props.components" to "fetchComponentData()".  
      // Only when this resolves do we render the  
      // components because we know the store has all  
      // the necessary component data now. 
      fetchComponentData(props.components).then(() => { 
        const rendered = renderToString(( 
          <RouterContext {...props} /> 
        )); 
 
        res.send(doc(rendered, store.state.toJS())); 
      }); 
    } else { 
      res.status(404).send('Not Found'); 
    } 
  }); 
}); 

This code is mostly the same as it has been throughout this chapter with one important change. It will now wait for fetchComponentData() to resolve before rendering anything. At this point, if there are any components with fetchData() methods, the store will be populated with their data.

For example, hitting the /first URL will cause Node.js to fetch data that the FirstContentContainer depends on, and set up the initial store state. Here's what this page looks like:

Fetching data

The only thing left to do is to make sure that this initial store state is serialized and passed to the browser somehow.

// In addition to the rendered component "content", 
// this function now accepts the initial "state". 
// This is set on "window.INITIAL_STATE" so that 
// React can determine when the first change after 
// the initial render happens. 
const doc = (content, state) => 
  ` 
  <!doctype html> 
  <html> 
    <head> 
      <title>Fetching Data</title> 
      <script> 
        window.INITIAL_STATE = ${JSON.stringify(state)}; 
      </script> 
      <script src="/static/main-bundle.js" defer></script> 
    </head> 
    <body> 
      <div id="app">${content}</div> 
    </body> 
  </html> 
  `; 

As you can see, the window.INITIAL_STATE value is passed a serialized version of the store state. Then, the client will rebuild this state. This is how we're able to avoid so many network calls, because we already have what we need in the store.

If you were to open the /second URL, you'll see something that looks like this:

Fetching data

Clicking this link, unsurprisingly, takes you to the first page. This will result in a new network call (mocked in this example) because the components on that page haven't been loaded yet.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.138.33.87