React: State Management and Data Fetching for Medium/Large Projects

React: State Management and Data Fetching for Medium/Large Projects

Rethink how you set up React application for your project/team.

Introduction

When setting up a React application for small to enterprise-scale projects one of the topics that the team discusses is the library they will use for managing the state and fetching data from the server.

I am going to limit this discussion to team managed projects and I aim to show you how to set up a robust React application that would take even new hires, junior developers, and other developers moving from a different team within your organization a fraction of the time they currently spend trying to figure out what changes which UI and what API handler does what.

I have used swr, react-query, and most recently @reduxjs/toolkit query for data fetching; and I think that RTK Query is what small to large teams need for state management and data fetching for a scaling application for the following reasons:

  • Handles data fetching and caching like the libraries you are used to.
  • APIs are defined explicitly as a service and wired like slices in the redux store

The last point is the most important feature for me and with this feature, you can have all your APIs separated into different services with defined endpoints. These endpoints are very readable to the extent that even newbies understand what handler is making which calls.

I don't think the redux-toolkit team emphasized very much the ease of use and onboarding others to projects wired up with RTK Query.

Let's Get Dirty, Shall We?

I will use the Hacker News API to show you how to set up your application, it's pretty much the same process for any size of application albeit the features that you would need to explore on your own to achieve certain behaviour in your application.

Project Requirements

  • We will use Hacker News API
  • Fetch the top stories (lazy loaded or paginated)
  • Fetch stories by Id
  • View full story
  • Show the sweetness of RTK Query

Let's Get Start

If you may, please get your command prompt started. We will use vite as our build tool (it is blazing fast!)

Install vite and follow the prompt to scaffold a react application. We are using react.js not .ts

 npm init vite@latest

Follow the instructions and npm run dev. You should see this page on localhost:3000

Screenshot (1).png

We are all set!

Create Components

In src/ create a components/ folder and create these files news-list.jsx, news-card.jsx, and nav.jsx. Create another folder in src/ styles/ and move all CSS files into it and add a new nav.css file.

In nav.jsx

import logo from '../logo.svg'
import '../styles/nav.css'

export default function Nav() {
    return (
        <header className="header">
            <div className="brand">
                <img src={ logo } className="logo" alt="logo" width={ 60 } height={ 60 } />
                <h1>Hacker News</h1>
            </div>
        </header>

    )
}

In your App.jsx

import Nav from './components/nav'

function App() {

  return (
    <div className="App">
      <Nav />
    </div>
  )
}

export default App

In the newly created nav.css file

.brand{
  display: flex;
  justify-content: flex-start;
  align-items: center;
  gap: 1em;
  color:white;
}
.header{
  max-height: 60px;
  padding: 1em 2em;
  background: rgb(2, 2, 19);
}

You should have this result

image.png

Let's flesh out the news-list and news-card to see what our app looks like.

In news-card.jsx

import '../styles/news-card.css'

export default function NewsCard() {
    return (
        <a href="">
            <p className="title">
                Rock redux in your team
            </p>
            <div className="c-body">
                <p> <span>By: </span>Chris Two</p>
                <p> <span>Posted On:  </span>23:3:04</p>
            </div>
            <div className="c-footer">
                <p>943<span> Comments</span></p>
                <p>443 <span> Votes</span></p>
            </div>

        </a>
    )
}

In news-card.css add these

.card{
   display: block;
    text-decoration: none;
    padding: 1.2em;
    margin: 1em auto;
    background-color:rgb(0, 32, 92) ;
    color: white;
    box-shadow: 0 3px 6px 1px gray;
}

.card p{
 margin-block-start: .4em;
 margin-block-end: .3em;

}

.card span{
    color: rgb(142, 142, 192);
}


.c-body, .c-footer{
    display: flex;
    justify-content: space-between;
    align-items: center;
}

Next is the news-list.jsx

import '../styles/news-list.css'
import NewsCard from './news-card'

export default function NewsList() {
    return (
        <div className="list">
            <NewsCard />
            <NewsCard />
            <NewsCard />
            <NewsCard />
        </div>
    )
}

We need to populate the list with dummy cards to make sense of what will happen when we fetch data from the server. In news-list.css

.list{
    margin: auto;
    width: 40%;
    height: 70vh;
    overflow-y: auto;
    scroll-behavior: smooth;
}

You should have this result

image.png

We have implemented the UI, now it's time to fetch data from the server.

Wiring With RTK Query

Let's connect RTK Query to our application.

npm install @reduxjs/toolkit

Create another folder in src/, services/, and add a new file hackernews.js. RTK Query is an addon in @reduxjs/toolkit. Let's define Hacker News API in hackernews.js.

// Need to use the React-specific entry point to import createApi
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'

// Define a service using a base URL and expected endpoints
export const hackerNewsApi = createApi({
    reducerPath: 'hackerNewsApi',
    baseQuery: fetchBaseQuery({ baseUrl: 'https://hacker-news.firebaseio.com/v0/' }),
    endpoints: (builder) => ({
        getTopStories: builder.query({
            query: () => `topstories.json`,
        }),
        getStoryById: builder.query({
            query: (id) => `item/${id}.json?print=pretty`
        })
    }),
})

// Export hooks for usage in functional components, which are
// auto-generated based on the defined endpoints
export const { useGetTopStoriesQuery, useGetStoryByIdQuery } = hackerNewsApi

Create another file in src/ name it store.js. This is the central store for our app's redux states.

An RTKQ service generates a "slice reducer" that should be included in the Redux root reducer and a custom middleware that handles the data fetching. Both need to be added to the Redux store. - Official doc.

In store.js

import { configureStore } from '@reduxjs/toolkit'
import { setupListeners } from '@reduxjs/toolkit/query'
import { hackerNewsApi } from './services/hackernews'


export const store = configureStore({
    reducer: {
        // Add the generated reducer as a specific top-level slice
        [hackerNewsApi.reducerPath]: hackerNewsApi.reducer,
    },
    // Adding the api middleware enables caching, invalidation, polling,
    // and other useful features of `rtk-query`.
    middleware: (getDefaultMiddleware) =>
        getDefaultMiddleware().concat(hackerNewsApi.middleware),
})

// optional, but required for refetchOnFocus/refetchOnReconnect behaviors
// see `setupListeners` docs - takes an optional callback as the 2nd arg for customization
setupListeners(store.dispatch)

Now connect redux-toolkit to your application. Before that, install react-redux.

npm i react-redux

In main.jsx

import React from 'react'
import ReactDOM from 'react-dom'
import './styles/index.css'
import App from './App'
import { store } from './store'
import { Provider } from 'react-redux'

ReactDOM.render(
  <React.StrictMode>
    <Provider store={ store }>
      <App />
    </Provider>
  </React.StrictMode>,
  document.getElementById('root')
)

The provider exposes the application to all the components below main.js, in other words, any component can access the query methods we destructured from the hackerNewsApi.

Data Fetching in Components

In news-list.jsx

import { useGetTopStoriesQuery } from '../services/hackernews'
import '../styles/news-list.css'
import NewsCard from './news-card'


export default function NewsList() {
    const { data, isFetching, isLoading, isError } = useGetTopStoriesQuery();


    if (isLoading) {
        return <div className="container">Loading...</div>
    }
    if (isError) {
        return <div className="container">An error occured. </div>
    }

    console.log(data)
    return (
        <div>
            { isFetching && <div className="container">Fetching data...</div> }
            <div className="list">
                <NewsCard />
                <NewsCard />
                <NewsCard />
                <NewsCard />
            </div>
        </div>
    )
}

Check your browser console and see that the fetchBaseQuery actually fetched your data and cached it. You can view the cached data using redux browser devtool.

Let's modify news-list.jsx

import { useGetTopStoriesQuery } from '../services/hackernews'
import '../styles/news-list.css'
import NewsCard from './news-card'


export default function NewsList() {
    const { data, isFetching, isLoading, isError } = useGetTopStoriesQuery();


    if (isLoading) {
        return <div className="container">Loading...</div>
    }
    if (isError) {
        return <div className="container">An error occured. </div>
    }

    return (
        <div>
            { isFetching && <div className="container">Fetching data...</div> }
            <div className="list">
                { data.map((storyId, index) => (<NewsCard storyId={ storyId } key={ index } />)) }

            </div>
        </div>
    )
}

Next, we modify news-card.jsx to use the storyId for getting a story by its ID

import { useGetStoryByIdQuery } from '../services/hackernews'
import '../styles/news-card.css'

export default function NewsCard({ storyId }) {
    const { data, isLoading, error } = useGetStoryByIdQuery(storyId);

    if (isLoading) {
        return <div className="container">Loading ...</div>
    }

    if (error) {
        return <div className="container">Error { error }</div>
    }

    return (
         <a className="card" target="_blank" href={ `${data.url}` } >
            <p className="title">
                { data.title }
            </p>
            <div className="c-body">
                <p> <span>By: </span>{ data.by }</p>
                <p> <span>Posted On:  </span>{ new Date(data.time).getMinutes() }mins ago</p>
            </div>
            <div className="c-footer">
                <p>{ data.descendants}<span> Comments</span></p>
                <p>{data.score} <span> Votes</span></p>
            </div>

        </a>
    )
}

Expected result

image.png

You can see for yourself that even the newest intern that has little knowledge of React and a few data fetching libraries knows exactly what hook(s) to call to fetch certain data. You can have another service with defined endpoints that anyone can jump on and easily start working with. Try to add a weather API service to this project.

Bonus

However, we are fetching 500 items in our news list, depending on the device used, this is very large and would be slow. Let's add pagination to our application so that the user can scroll through the data in pages.

Install react-paginate library

npm i react-paginate

Update news-list.jsx with these lines of code

import ReactPaginate from 'react-paginate';
import { useState, useEffect } from 'react';
import { useGetTopStoriesQuery } from '../services/hackernews'
import '../styles/news-list.css'
import NewsCard from './news-card'



export default function NewsList({ itemsPerPage }) {

    const [currentItems, setCurrentItems] = useState(null);
    const [pageCount, setPageCount] = useState(0);
    const [itemOffset, setItemOffset] = useState(0);


    const { data, isFetching, isLoading, isError } = useGetTopStoriesQuery();


    useEffect(() => {
        // Fetch items from another resources.
        const endOffset = itemOffset + itemsPerPage;
        console.log(`Loading items from ${itemOffset} to ${endOffset}`);
        setCurrentItems(data.slice(itemOffset, endOffset));
        setPageCount(Math.ceil(data.length / itemsPerPage));
    }, [itemOffset, itemsPerPage]);


    const handlePageClick = (event) => {
        const newOffset = (event.selected * itemsPerPage) % data.length;
        console.log(
            `User requested page number ${event.selected}, which is offset ${newOffset}`
        );
        setItemOffset(newOffset);
    };

    if (isLoading) {
        return <div className="container">Loading...</div>
    }
    if (isError) {
        return <div className="container">An error occured. </div>
    }

    return (
        <div className="content">
            { isFetching && <div className="container">Fetching data...</div> }
            <div className="list">
                { currentItems.map((storyId, index) => (<NewsCard storyId={ storyId } key={ index } />)) }

            </div>
            <ReactPaginate
                breakLabel="..."
                nextLabel="next >"
                onPageChange={ handlePageClick }
                pageRangeDisplayed={ 3 }
                pageCount={ pageCount }
                previousLabel="< previous"
                renderOnZeroPageCount={ null }
                containerClassName="p-container"
                activeClassName="active-page"
                previousClassName="previous-page"
                nextClassName="next-page"
                pageClassName="page"
                pageLinkClassName="page-link"
                breakClassName="break"
                activeLinkClassName="active-page-link"

            />
        </div>
    )
}

Also update news-list.css

...

.content{
  margin: auto;
}
.p-container{
    margin:1em auto;
    padding: 1em;
    display: flex;
    width: 40%;
}

.active-page{
    color: white;
    background-color: black;
    list-style: none;
}

.page{
    margin: .3em;
    color: black;
    background-color: whitesmoke;
    list-style: none;
}

.page-link{
    list-style: none;
    padding: .234em .3em;
    border: 1px solid black;
    background-color: rgb(187, 187, 183);
}
.break{
    list-style: none;
}

.active-page-link{
     list-style: none;
     color: white;
    background-color: rgb(0, 32, 92);
}

.next-page, .previous-page{
    background-color: rgb(0, 32, 92);
    color: white;
    padding: 4px 5px;
    font-size: 16px;
     list-style: none;
}
...

And finally the App.jsx

import Nav from './components/nav'
import NewsList from './components/news-list'


function App() {

  return (
    <div>
      <Nav />
      <NewsList itemsPerPage={40} />
    </div>
  )
}

export default App

Result of our update

image.png

In conclusion, this tutorial has shown you the tip of the iceberg on how RTK Query can ease setting up a react application's state management and data fetching. They don't need to be in separate files and you can transform the data you fetched before caching, prefetch data, pool data at intervals, etc.

This is my own opinion, what do you think about RTK Query and other data fetching libraries, I'd like to hear your own opinion in the comment section and learn.