Author avatar

Gaurav Singhal

How to Pass New Server Data to react.js Components

Gaurav Singhal

  • Mar 12, 2020
  • 10 Min read
  • 17,913 Views
  • Mar 12, 2020
  • 10 Min read
  • 17,913 Views
Web Development
Front End Web Development
Client-side Framework
React

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.

1class Timeline extends Component {
2    constructor(props) {
3        super(props);
4        this.state = {
5            timeline: []
6        }
7    }
8
9    componentDidMount() {
10        this.setState({timeline: this.props.initialData})
11    }
12
13    render() {
14        return (
15            <div className="timeline-container">
16                {this.state.timeline.map(post => (
17                    <div className="post" key={post.id}>
18                        <UserInfo user={post.user}>
19                        <p className="feed">{post.feed}</p>
20                    </div>
21                ))}
22            </div>
23        )
24    }
25}
jsx

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.

1class App extends Component {
2  constructor(props) {
3    super(props);
4    this.state = {
5      initialData: []
6    };
7  }
8
9  async componentDidMount() {
10    const res = await fetch(SERVER_URL);
11    const data = await res.json();
12    this.setState({ initialData: data });
13  }
14
15  render() {
16      if(initialData.length == 0) return <Loader>
17      return <Timeline initialData={this.state.initialData} />
18  }
19}
jsx

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.

1class App extends Component {
2  constructor(props) {
3    super(props);
4    this.state = {
5      initialData: [],
6      newData: []
7    };
8  }
9
10  async componentDidMount() {
11    const res = await fetch(SERVER_URL);
12    const data = await res.json();
13    this.setState({ initialData: data });
14    this.timer = setInterval(() => {
15        let lastId = null;
16        const {initialData, newData} = this.state;
17        if(initialData.length > 0) {
18            if(newData.length > 0) lastId = newData[0].id
19            else lastId = initialData[0].id
20        }
21        const res = await fetch(`${SERVER_URL}/${lastId}`);
22        const data = await res.json();
23        this.setState({ newData: data });
24    }, 8000);
25  }
26
27  componentWillUnmount() {
28      clearInterval(this.timer);
29  }
30
31  render() {
32      if(initialData.length == 0) return <Loader>
33      return <Timeline initialData={this.state.initialData} newData={this.state.newData}/>
34  }
35}
jsx

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.

1class Timeline extends Component {
2    constructor(props) {
3        super(props);
4        this.state = {
5            timeline: []
6        }
7    }
8
9    componentDidMount() {
10        this.setState({timeline: this.props.initialData})
11    }
12
13    componentDidUpdate(prevProps, prevState) {
14        if(prevProps.newData !== this.props.newData) {
15            this.setState({ timeline: [...this.props.newData, ...this.state.timeline] })
16        }
17    }
18
19    render() {
20        return (
21            <div className="timeline-container">
22                {this.state.timeline.map(post => (
23                    <div className="post" key={post.id}>
24                        <UserInfo user={post.user}>
25                        <p className="feed">{post.feed}</p>
26                    </div>
27                ))}
28            </div>
29        )
30    }
31}
jsx

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.

1const App = () => {
2    const [initialData, setInitialData] = useState([]);
3    const [newData, setNewData] = useState([]);
4
5    useEffect(() => {
6        const res = await fetch(SERVER_URL);
7        const data = await res.json();
8        setInitialData(data)
9        const timer = setInterval(() => {
10            let lastId = null;
11            if(initialData.length > 0) {
12                if(newData.length > 0) lastId = newData[0].id
13                else lastId = initialData[0].id
14            }
15            const res = await fetch(`${SERVER_URL}/${lastId}`);
16            const data = await res.json();
17            setNewData(data);
18        }, 8000);
19
20        return () => clearInterval(timer);
21    },[]);
22
23    if(initialData.length == 0) return <Loader>
24    return <Timeline initialData={initialData} newData={newData}/>
25};
jsx

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.

1const Timeline = ({initialData, newData}) => {
2    const [timeline, setTimeline] = useState([]);
3
4    useEffect(() => {
5        setTimeline(initialData);
6    }, [])
7
8    useEffect(() => {
9        setTimeline([...newData, ...timeline])
10    }, [newData])
11
12    return (
13        <div className="timeline-container">
14            {timeline.map(post => (
15                <div className="post" key={post.id}>
16                    <UserInfo user={post.user}>
17                    <p className="feed">{post.feed}</p>
18                </div>
19            ))}
20        </div>
21    )
22}
jsx

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.

1npm i @anephenix/sarus
console

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.

1// ..
2import Sarus from '@anephenix/sarus';
3
4const App = () => {
5    const [initialData, setInitialData] = useState([]);
6    const [newData, setNewData] = useState([]);
7
8    useEffect(() => {
9        const res = await fetch(SERVER_URL);
10        const data = await res.json();
11        setInitialData(data)
12    },[]);
13
14    const sarus = new Sarus({
15        url: WEBSOCKET_URL, // normally starts with wss://
16        eventListeners: {
17            open: [connectionOpened],
18            message: [updateTimeline],
19            close: [connectionClosed],
20            error: [throwError]
21        }
22    })
23
24    const connectionOpened = () => console.log("Socket connection opened");
25
26    const connectionClosed = () => console.log("Socket connection closed");
27
28    const throwError = error => throw error;
29
30    const updateTimeline = event => {
31        const newData = JSON.parse(event.data);
32        setNewData(newData);
33    }
34
35    if(initialData.length == 0) return <Loader>
36    return <Timeline initialData={initialData} newData={newData}/>
37};
jsx

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.