Test Category

Test Blog Post

Starter template for writing out a blog post using MDX/JSX and Next.js.

No Name Exists

Abdullah Muhammad

Published on May 17, 20265 min read 1 views

Share:
Article Cover Image

Introduction

When working with React.js, we often create state (component-level and global) and need to figure out ways of working with it.

Thankfully, there are several options out there, but it can be confusing to know which option(s) work best for a given scenario.

In the past, we looked at the Redux/Redux Toolkit for state management (we will briefly touch on it again), but today, we will focus on other strategies such as React hooks, the React Query library, and local storage which can be used to optimize state management.

This tutorial will not be the typical code overview and demo setup used in the past, but more-so, a walkthrough of the different ways one can use the libraries and tools we will be covering.

I will be using snippets of code from a cryptocurrency project I launched and continue to add features to.

Data Persistence and Redux/Redux Toolkit Refresher

Redux is an ancillary library which works nicely in tandem with React.js. Global state management becomes easier and there are three main components in Redux we looked at:

  • Store — Global object combining different pieces of state
  • Reducers — Pure functions which change state
  • Actions — Functions that can be dispatched to request state changes

When working with Redux Toolkit, we found that we could incorporate all three of these things into a function known as createSlice().

As the name implies, this function can be used to create and manage pieces of state within the global store.

All your actions, initial state, reducers, and thunk functions can be managed with this single function.

You can configure the store and wrap your application to access the store using the <Provider> tag and configureStore() function.

We can also persist data using local storage provided that we do not store any sensitive information which can be retrieved from the web browser.

The following gist illustrates this perfectly:

GitHub GistJavaScript
import { createSlice } from '@reduxjs/toolkit';

// Persist storage or set to a default value
let walletAddressState = localStorage.getItem('walletAddress') === null ? 
                            { walletAddress: '' } : 
                            { walletAddress: localStorage.getItem('walletAddress') };


// Create slice of global data to represent wallet address
const walletAddressReducer = createSlice({
    name: 'wallet',
    initialState: walletAddressState,
    reducers: {
        updateAddress: (state, action) => {
            localStorage.setItem('walletAddress', action.payload);
            state.walletAddress = action.payload;
        },
        resetAddress: (state) => {
            localStorage.setItem('walletAddress', '');
            state.walletAddress = ''
        }
    }
});

// Export update and reset address functions as actions
// Export the reducer function

export const { updateAddress, resetAddress } = walletAddressReducer.actions;
export default walletAddressReducer.reducer;
createSlice() function allowing for the creation of state, action, and reducer functions

The react-redux package also offers hooks for working with global state such as useDispatch() and useSelector(). These can be used by all components in your web application to view and update state stored in the global store.

As we saw in the React-Redux tutorial, working with Redux for the first time can be complicated and wrapping your head around it can be difficult. You might also feel like this is a lot of work to do for such a simple task.

You do not need to use Redux. In most cases, with the alternatives we will be looking at, you will not need to.

React Context API

The React Context API is a great alternative to Redux in that it offers similar functionality. Redux takes a centralized approach to state management using a global store.

The Context API allows for state management on a component level basis.

Unlike Redux, where everything is managed and accessed in a centralized way, the Context API follows a parent-child(ren) relationship.

In the past, we have looked at passing down state from component to component with the help of props.

The Context API does the same thing (passing down state), but it allows for state to be broadcast across components making it readily available for use instead of relying on props to relay information.

This is done through the use of a context provider which wraps the components which will use this state.

We can create this provider using the createContext() function from the React library and wrapping the components using a <Context.Provider> tag using the same name used to create the context.

An attribute named value is assigned to this tag which holds the state to be broadcast.

We can then access this state anywhere in the child components with the use of the useContext() hook.

Remember, we are not creating any state with the createContext() function, but rather a provider which will encapsulate where this state can be viewed and accessed.

You can have as many providers as you like, but React will look for the closest one up the DOM tree to find which one is relevant for that particular child component.

This can be a welcoming alternative to Redux should you feel it is not necessary.

Just know that for large, complex applications, it would probably be better to stick with Redux. However, for small applications, Context API works best.

It would be pointless to do an extensive example here as the official React documentation provides a great explanation of how one can work with the Context API.

useReducer() Hook

Aside from working with useState() for state management, React allows you to work with complex state and manage it using a hook known as useReducer().

As the name implies, this hook uses the Redux store/reducer/action setup and allows for complex state management at a component level.

There can be instances where in certain components, you are using quite a bit of useState() hooks to manage state. You can combine all these in a single state store so to speak and define actions and reducers which will manage those pieces of state.

The following is the classic counter example which is used to explain Redux, but with the useReducer() hook instead:

GitHub GistJavaScript
import { useReducer } from 'react';

import './App.css';

let countReducer = (state, action) => {
  const { type } = action;
  
  if (type === 'increment') {
    return {
      clicks: state.clicks + 1
    }
  }
    else if (type === 'decrement'){
    return {
      clicks: state.clicks - 1
    }
  }
}

const App = () => {
  const [state, dispatch] = useReducer(countReducer, { clicks: 0 });
  
  return (
    <div className="App">
      <h1>This is the Click Page!</h1>
      <h6>{ state.clicks }</h6>
      <button onClick={() => dispatch({ type: 'increment' })}>+</button>
      <button onClick={() => dispatch({ type: 'decrement' })}>-</button>
    </div>
  );
}


export default App;
useReducer() hook for a data store and a reducer which handles state changes based on different actions

useReducer() takes in a reducer as the first argument which will be used to change state based on action calls and the second argument is the state store.

We can make use of the dispatch() function which fires the different actions to update state and we can access state using state. Both of these are given to us by useReducer().

It is like mini-Redux, all-in-one action!

React Query

React Query (now officially known as TanStack Query) is a great ancillary library to use when working with client-side requests.

What is great about this library is that it offers data caching and optimization out-of-the-box which greatly reduces your codebase and offers performance incentives.

Like Redux and the Context API, you will need to wrap your application with the <QueryClientProvider> and provide a client using queryClient().

This will enable you to use TanStack Query and its rich features anywhere in your application.

There are two important hooks you need to know when working with TanStack Query:

  • useQuery() — Enables efficient data fetching
  • useMutation() — Enables efficient data changing

When working with APIs/Data fetching, we are either fetching data to display (GET) or running requests to change it (POST, PUT, DELETE) somewhere in some way.

Managing state is called into question because there is a delay when data is fetched and loaded. One must account for this and conditionally render data to take care of this scenario.


useQuery() Hook

By incorporating the useQuery() hook, we get boolean properties such as isLoading, isError, and isSuccess to easily determine what stage of the data fetching process the application is in. We can use these properties to determine what needs to be rendered at any given time.

The useQuery() hook takes in a queryKey parameter as well as a queryFn parameter which takes in a function that handles the data fetching process.

The queryKey takes in an array which uniquely describes a particular query. You can pass in state to this array to describe different kinds of queries keys at certain points in time and useQuery() will go ahead and re-fetch data for that particular key.

This query is then cached by TanStack and will re-run again on certain conditions (such as stale time, data/window changes, and so on) all provided out-of-the-box.

This leads to efficient development because redundant calls are removed and your application stays up to date with the latest data. Here is an example of working with the useQuery() hook:

GitHub GistJavaScript
import React from 'react';
import { useQuery } from '@tanstack/react-query';
import { metricsNavbarEthPrice } from '../../UtilFunctions/metricsNavbarEthPrice';
import { metricsNavbarGasPrice } from '../../UtilFunctions/metricsNavbarGasPrice';

const MetricsNavbar = () => {
    // Setting up query to fetch Ethereum Price
    const ethPriceQuery = useQuery({
        queryKey: ['eth price'],
        queryFn: metricsNavbarEthPrice
    });

    // Setting up query to fetch Ethereum gas price
    const gasPriceQuery = useQuery({
        queryKey: ['gas price'],
        queryFn: metricsNavbarGasPrice
    });

    if ( ethPriceQuery.isLoading || gasPriceQuery.isLoading ){
        return <div>Loading...</div>
    }
    else if (ethPriceQuery.isError || gasPriceQuery.isError){
        return <div>Error Fetching data</div>
    }
    else if (ethPriceQuery.isSuccess && gasPriceQuery.isSuccess){

        // Adding another Navbar containing ETH price and gas information
        let price = ethPriceQuery.data[Object.keys(ethPriceQuery.data)[0]].ethereum;
        let gas = gasPriceQuery.data[0].information;

        return (
            <nav className="navbar navbar-expand-lg navbar-light bg-dark">
                <div className="container-fluid">
                    <div>
                        <ul className="navbar-nav me-auto mb-2 mb-lg-0">
                            <li className="nav-item"> 
                                <p style={{ paddingLeft: '1rem', display: 'inline', color: 'white', marginTop: '1rem'}}>ETH Price: <b>{ price == null ? "Loading" : "$" + price.usd.toFixed(2) }</b></p>
                            </li>
                            <li className="nav-item">
                                <p style={{ paddingLeft: '1rem', display: 'inline', color: 'white' }}>24-Hr % Chg:</p>
                                <p style={{ color: price.usd_24h_change < 0 ? 'red' : 'lightgreen', marginTop: '1rem', display: 'inline' }}>
                                    <b>{ price.usd_24h_change > 0 ? " +" + price.usd_24h_change.toFixed(2) + "%" : " " + price.usd_24h_change.toFixed(2) + "%" }</b>
                                </p> 
                            </li>
                            <li className="nav-item">
                                <p style={{ paddingLeft: '1rem', display: 'inline', color: 'white' }}>Gas Price:</p>
                                <p style={{ display: 'inline', color: 'white' }}><b>{ " " + gas.maxPrice + " " }Gwei</b></p>
                            </li>
                        </ul>
                    </div>
                </div>
            </nav>
        )
    }
}

export default MetricsNavbar;
useQuery() hook for fetching and storing Ethereum price data

As you can see, we are using useQuery() to fetch and store Ethereum price + gas data.

We are using one key in each of the two cases (an array containing a lone string) and using the built-in properties provided by useQuery() to render data. Below is the function that fetches data for one of them:

GitHub GistJavaScript
import axios from 'axios';

export const metricsNavbarEthPrice = async () => {
    const COINGECKO_URL = "https://api.coingecko.com/api/v3";
    const QUERY_STRING_ETHEREUM = "?ids=ethereum&vs_currencies=usd&include_24hr_change=true";
    const API_ENDPOINT = "/simple/price";

    let ethPricedata = [];

    let response = await axios.get(COINGECKO_URL + API_ENDPOINT + QUERY_STRING_ETHEREUM); // Fetch Ethereum data

    if (response.status !== 200){
        throw new Error("Could not fetch data related to the Ethereum network"); // Throw error if no success
    }
    else {
        ethPricedata.push(response.data);
    }

    return ethPricedata;
}
metricsNavbarEthPrice() function fetching data using the CoinGecko API

Notice how we are taking into account, success and error conditions. It is these that are evaluated by the useQuery() hook and allows us to work with the isSuccess, isError, and isLoading properties to load data.

We do not need to do anything else, TanStack query will take of the rest. Pretty neat eh?!

There is a lot more you can do with useQuery(), but this is the gist of it.


useMutation() Hook

The useMutation() hook is very similar to the useQuery() hook as it offers similar properties to determine if a request to data change is pending, successful or failed.

We can pass in a mutationKey parameter to uniquely identify a particular query and a mutationFn as well which handles the actual request to change data.

There are other functions which we can pass in such as onSuccess, onError, and retry to determine what to do when certain scenarios arise.

Here is the official documentation which dives deeper into using the useMutation() hook for working with data changes, but this was the gist of it.

Conclusion

All in all, we covered at great length, the many different ways one can go about state management with React.js.

We explored alternatives to using the Redux/Redux Toolkit and found that it is not always necessary to use it for state management.

We looked at various React hooks for working with state, the React Context API, and did a deep dive into query optimization and data caching using the Tanstack Query library.

Remember, there is no right or wrong answer. There are plenty of options out there that assist with state management. You can use one or a combination of these tools for development.

In the list below, you will find links to the official React.js, TanStack Query, and Redux Toolkit websites:

I hope you enjoyed this article on React State Management and look forward to more in the future.

Thank you!

No Name

Abdullah Muhammad

Blogger. Software Engineer. Designer.

Subscribe to the newsletter

Get new articles, code samples, and project updates delivered straight to your inbox.