Skip to content

Contact sales

By filling out this form and clicking submit, you acknowledge our privacy policy.

How to Pass New Server Data to react.js Components

Mar 12, 2020 • 10 Minute Read

Introduction

This guide will show how to pass new server data to your components and make a web app more interactive using realtime data.

The Use Case

Let's say you have a component that displays a timeline feed similar to Twitter, and we want to display the data sent to the server in realtime.

The <Timeline /> component would look as follows.

      class Timeline extends Component {
    constructor(props) {
        super(props);
        this.state = {
            timeline: []
        }
    }

    componentDidMount() {
        this.setState({timeline: this.props.initialData})
    }

    render() {
        return (
            <div className="timeline-container">
                {this.state.timeline.map(post => (
                    <div className="post" key={post.id}>
                        <UserInfo user={post.user}>
                        <p className="feed">{post.feed}</p>
                    </div>
                ))}
            </div>
        )
    }
}
    

Simple, for now--its only job is to display the initial data fetched by the app. Later, this guide will explain how to modify the component to display new feeds.

The <UserInfo /> component is a fundamental component to display the user's avatar and name.

The <Timeline /> component will be wrapped by the <App /> component, which will fetch the initial data and pass it as the initialData prop.

      class App extends Component {
  constructor(props) {
    super(props);
    this.state = {
      initialData: []
    };
  }

  async componentDidMount() {
    const res = await fetch(SERVER_URL);
    const data = await res.json();
    this.setState({ initialData: data });
  }

  render() {
      if(initialData.length == 0) return <Loader>
      return <Timeline initialData={this.state.initialData} />
  }
}
    

Until the data is fetched from the server, the <App /> component will return a <Loader /> component. To get the feeds from the server, this example uses the fetch() method, but you can use axios as well. Both serve the same purpose. (Check out this Guide on Axios versus Fetch to learn more.)

At this point, the <App /> component is only fetching and passing the initial data to the <Timeline /> component. The next step is to update the <App /> component to request that the server periodically check for new data using the setInterval() method. This is a pseudo-realtime pattern, but it's crucial to understand how to handle new data before going realtime. Later on in this Guide, Sarus will be used to get the data in realtime.

      class App extends Component {
  constructor(props) {
    super(props);
    this.state = {
      initialData: [],
      newData: []
    };
  }

  async componentDidMount() {
    const res = await fetch(SERVER_URL);
    const data = await res.json();
    this.setState({ initialData: data });
    this.timer = setInterval(() => {
        let lastId = null;
        const {initialData, newData} = this.state;
        if(initialData.length > 0) {
            if(newData.length > 0) lastId = newData[0].id
            else lastId = initialData[0].id
        }
        const res = await fetch(`${SERVER_URL}/${lastId}`);
        const data = await res.json();
        this.setState({ newData: data });
    }, 8000);
  }

  componentWillUnmount() {
      clearInterval(this.timer);
  }

  render() {
      if(initialData.length == 0) return <Loader>
      return <Timeline initialData={this.state.initialData} newData={this.state.newData}/>
  }
}
    

This example will fetch the new data every eight seconds. Considering that this server has an endpoint that will return feeds after a specific ID, first find out the lastId in the timeline and pass that as a route parameter to the server endpoint.

Using the componentDidUpdate() Lifecycle Method

Next, modify the <Timeline /> component to handle the new incoming feeds.

      class Timeline extends Component {
    constructor(props) {
        super(props);
        this.state = {
            timeline: []
        }
    }

    componentDidMount() {
        this.setState({timeline: this.props.initialData})
    }

    componentDidUpdate(prevProps, prevState) {
        if(prevProps.newData !== this.props.newData) {
            this.setState({ timeline: [...this.props.newData, ...this.state.timeline] })
        }
    }

    render() {
        return (
            <div className="timeline-container">
                {this.state.timeline.map(post => (
                    <div className="post" key={post.id}>
                        <UserInfo user={post.user}>
                        <p className="feed">{post.feed}</p>
                    </div>
                ))}
            </div>
        )
    }
}
    

The componentDidUpdate() method is called after componentDidMount() and is useful to perform after the prop or state changes. componentDidUpdate() takes in the previous prop and the previous state as though they are two arguments. componentDidUpdate() performs the task or changes the state after checking the condition. If the setState() function call in a condition isn't wrapped, the component will update recursively and crash the app.

Using the useEffect() and useState() Hooks

Below is an example of how to implement the same components by using React Hooks.

      const App = () => {
    const [initialData, setInitialData] = useState([]);
    const [newData, setNewData] = useState([]);

    useEffect(() => {
        const res = await fetch(SERVER_URL);
        const data = await res.json();
        setInitialData(data)
        const timer = setInterval(() => {
            let lastId = null;
            if(initialData.length > 0) {
                if(newData.length > 0) lastId = newData[0].id
                else lastId = initialData[0].id
            }
            const res = await fetch(`${SERVER_URL}/${lastId}`);
            const data = await res.json();
            setNewData(data);
        }, 8000);

        return () => clearInterval(timer);
    },[]);

    if(initialData.length == 0) return <Loader>
    return <Timeline initialData={initialData} newData={newData}/>
};
    

You must give an empty array as a second argument to useEffect() so that the function is only run once after the initial render. This way, its behavior will be the same as componentDidMount(). It is considered a best practice to clean up any side effects by returning a cleanup function to avoid memory leaks in the app.

      const Timeline = ({initialData, newData}) => {
    const [timeline, setTimeline] = useState([]);

    useEffect(() => {
        setTimeline(initialData);
    }, [])

    useEffect(() => {
        setTimeline([...newData, ...timeline])
    }, [newData])

    return (
        <div className="timeline-container">
            {timeline.map(post => (
                <div className="post" key={post.id}>
                    <UserInfo user={post.user}>
                    <p className="feed">{post.feed}</p>
                </div>
            ))}
        </div>
    )
}
    

The useEffect function can be modified to run only when there is a change in a particular variable by adding it to the array, which is passed as the second argument to the useEffect() method.

The <Timeline /> component has two useEffect hooks. One will run when the component renders initially, and the second will run after there is a change in the newData prop.

Going Realtime with Sarus

Sarus is a client-side JavaScript library for handling WebSocket connections. It is made to handle unexpected socket disconnections and acts as a wrapper around the WebSocket API.

This example will use Sarus to make the app get realtime data from the server.

First, install Sarus.

      npm i @anephenix/sarus
    

Then in the <App /> component, remove the setInterval() function as we are no longer going to poll the server for data. We will initialize and attach the WebSocket events with Sarus.

      // ..
import Sarus from '@anephenix/sarus';

const App = () => {
    const [initialData, setInitialData] = useState([]);
    const [newData, setNewData] = useState([]);

    useEffect(() => {
        const res = await fetch(SERVER_URL);
        const data = await res.json();
        setInitialData(data)
    },[]);

    const sarus = new Sarus({
        url: WEBSOCKET_URL, // normally starts with wss://
        eventListeners: {
            open: [connectionOpened],
            message: [updateTimeline],
            close: [connectionClosed],
            error: [throwError]
        }
    })

    const connectionOpened = () => console.log("Socket connection opened");

    const connectionClosed = () => console.log("Socket connection closed");

    const throwError = error => throw error;

    const updateTimeline = event => {
        const newData = JSON.parse(event.data);
        setNewData(newData);
    }

    if(initialData.length == 0) return <Loader>
    return <Timeline initialData={initialData} newData={newData}/>
};
    

To initialize the WebSocket connection, pass an object with the URL and event listeners to the Sarus constructor function, as shown in the above code block.

The message event calls the specified method when a message is received from the server. In this case, it will contain the new feed data for the timeline.

Of course, for this to work, you need to have WebSockets configured on your server that can send new feeds as the users post them.

Conclusion

In this Guide, you saw how to handle new incoming data using the componentDidMount() lifecycle method by comparing the previous and new props, and how to use the useEffect hook effectively to handle re-renders while dealing with incoming data. Finally, you saw how to make the app realtime using the Sarus WebSocket library.