What State Manager to use with Next.JS ?

What State Manager to use with Next.JS ?

Redux Toolkit, Apollo GraphQL & React Query Comparison.

A bunch of questions always come to mind when starting a new Next.JS project when it comes to choosing the right State Manager. And a lot of variables come to your mind when choosing one.

Which one is developer friendly? Which one is better for your project’s performance? Which one is more reliable and has the biggest community to support it? Which one is more asked by clients and software companies?

In this article, I will answer these questions about 3 State Managers that I’ve used, and hopefully, after reading this article you won’t wonder anymore about which one suits best your project’s needs.

* * * * *

Popularity

If you are a new React/Next developer, it’s important to choose the most demanded state management tool by software companies and clients to help you land your first job/client.

GitHub Stars

First, we’re going to start with the popularity of?GitHub?repositories.

  • RTK?has?8.5K?stars, but this number can be a bit misleading because you have to keep in mind that?Redux?has?58.7K?which makes it the most popular state management library out there, and RTK being its descendent makes it full of potential to grow its community and may one day become the most popular one.
  • Apollo Client?has?18.1K?stars, but just like RTK, you need to keep in mind that?GraphQL?has a big community too and it currently has?19k?stars.
  • React Query?has?29.8K?stars, which until now is the most popular one compared to the other state managers that we are focusing on in this article.

NPM Weekly Downloads

Another factor that we need to keep in mind when it comes to popularity, is the number of weekly downloads on the?npm?official website.

Official Documentations Monthly Visits

To get the monthly visits for each website we’re going to use a popular tool called?Similar Web?which gives you deep insights into each website’s statistics.

  • RTK?has an average of more than?500K?Visits/Month

No alt text provided for this image

No alt text provided for this image

  • React Query?has an average of more than?650K?Visits/Month

No alt text provided for this image

Results

this table sums up the statistics that we’ve analyzed and it’s up to you now to decide which one is more popular. Just keep in mind the potential for each one of them because these stats may change in the near future.

╔══════╦═══════════╦═══════════╦══════════╗

║ ║ RTK ║ Apollo client ║ React Query ║

╠══════╬═══════════╬═══════════╬══════════╣

║ GitHub ║ 8.5K 18.1K 29.8K

║ Npm ║ 2M2.5M1.4M

║ Website ║ 500K 1M650K

╚══════╩═══════════╩═══════════╩══════════╝

* * * * *

Use Case

This part may be the most important factor that I suggest you prioritize when choosing one of the 3 state managers.

RTK

Given the popularity of Redux, most developers have used it before, and thus RTK has the shortest learning curve among the rest of the state managers.

According to the?documentation:

Redux Toolkit is beneficial to all Redux users regardless of skill level or experience. It can be added at the start of a new project, or used as part of an incremental migration in an existing project.
Note that you are not?required?to use Redux Toolkit to use Redux. There are many existing applications that use other Redux wrapper libraries, or write all Redux logic “by hand”, and if you still prefer to use a different approach, go ahead!
However,?we?strongly?recommend using Redux Toolkit for all Redux apps.
Overall, whether you’re a brand new Redux user setting up your first project, or an experienced user who wants to simplify an existing application, using Redux Toolkit will make your code better and more maintainable.

Being a developer who’s familiar with redux, I agree that RTK is a lot more developer friendly and reduces the lines of code that you need to write to get the same things done.

And the most important feature that comes with it is the RTK Query which according to the documentation:

is a powerful data fetching and caching tool. It is designed to simplify common cases for loading data in a web application, eliminating the need to hand-write data fetching & caching logic yourself.

Here is a list of features that come along with RTK query that will make the decision if you’re going to work with RTK and RTK Query or not:

  • The data fetching and caching logic is built on top of Redux Toolkit’s?createSlice?and?createAsyncThunk?APIs
  • Because Redux Toolkit is UI-agnostic, RTK Query’s functionality can be used with any UI layer
  • API endpoints are defined ahead of time, including how to generate query parameters from arguments and transform responses for caching
  • RTK Query can also generate React hooks that encapsulate the entire data fetching process, provide?data?and?isLoading?fields to components, and manage the lifetime of cached data as components mount and unmount
  • RTK Query provides “cache entry lifecycle” options that enable use cases like streaming cache updates via websocket messages after fetching the initial data
  • We have early working examples of code generation of API slices from OpenAPI and GraphQL schemas
  • Finally, RTK Query is completely written in TypeScript, and is designed to provide an excellent TS usage experience

Apollo Client

  • support for caching:

At its heart, Apollo is a GraphQL implementation that helps people manage their data. They also make and maintain a GraphQL client (Apollo Client) that we can use with React frameworks like Next.js.

The Apollo Client is a state management client that allows you to manage both local and remote data with GraphQL and you can use it to fetch, cache, and modify application data.

In Apollo’s own words, “Caching a graph is no easy task, but we’ve spent two years focused on solving it.”

Apollo has invested serious amounts of time in making their caching process the most efficient and effective they can. So, why try and reinvent the wheel? If you’re fetching data in your application and working with GraphQL, then the Apollo Client is a great way of handling your data.

Another benefit is that there is minimal setup required for developers using the client. Apollo even labels it as “zero-config.” We will see this in our application example further along in the post, but to set up caching in an application.

  • loading and error states:

The Apollo Client has?a custom React Hook built into it called useQuery, and gives us inbuilt loading and error states for us to use.

While this doesn’t sound impressive, what it means for developers is that we don’t need to spend time implementing this logic ourselves, we can just take the booleans the Hook returns and change our application rendering as required.

Thanks to these inbuilt states, using the Apollo Client means we can spend less time worrying about implementing data fetching logic and focus on building our application.

React Query

The most important factor that you should consider when choosing to work with React Query, is to know that it only manages Server States.

According to the documentation:

Well, let’s start with a few important items:
React Query is a server-state library, responsible for managing asynchronous operations between your server and client
Redux, MobX, Zustand, etc. are client-state libraries that?can be used to store asynchronous data, albeit inefficiently when compared to a tool like React Query

What that means is that React Query will manage the global states that you get from the backend, but when it comes to your client's global states you’ll need to use another tool, and in my case, I found that React’s hook UseContext is the best option. But you’re free to use any other tool to store your client state.

React Query is often described as the missing data-fetching library for React. Still, in more technical terms, it makes fetching, caching, synchronizing, and updating server state in your React applications a breeze. It provides a Hook for fetching, caching, and updating asynchronous data in React without touching any “global state” like Redux. Initially, it seems like a simple library; however, it is packed with complex features which handle most of the server state management issues you might have in an application.

* * * * *

Code Examples

let’s take a look at some code examples to give you an idea of how much code you need to write with each library.

RTK

First, we’ll create a “./services” folder where we consume our APIs. Here is an example of an RTK Query:


import { createApi, fetchBaseQuery } from '@rtk-incubator/rtk-query';

// Define a service using a base URL and expected endpoints
export const pokemonApi = createApi({
  reducerPath: 'pokemonApi',
  // baseQuery: fetchBaseQuery({ baseUrl: 'https://pokeapi.co/api/v2/' }), // uncomment for brokenness repro ur welcome :)
  baseQuery: async (baseUrl, prepareHeaders, ...rest) => {
    const response = await fetch(`https://pokeapi.co/api/v2/${baseUrl}`, rest)
    return {data: await response.json()}
  },
  endpoints: (builder) => ({
    getPokemonByName: builder.query({
      query: (name) => `pokemon/${name}`,
    }),
    getPokemonList: builder.query({
      query: () => `pokemon`
    })
  }),
});

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

Then we will create our “store”:

import { configureStore } from '@reduxjs/toolkit'
import { pokemonApi } from './services/pokemon';
import { useMemo } from 'react'

let store

const initialState = {}

function initStore(preloadedState = initialState) {
  return configureStore({
    reducer: {
      // Add the generated reducer as a specific top-level slice
      [pokemonApi.reducerPath]: pokemonApi.reducer,
    },
    preloadedState,
    // Adding the api middleware enables caching, invalidation, polling,
    // and other useful features of `rtk-query`.
    middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(pokemonApi.middleware),
  });
}

export const initializeStore = (preloadedState) => {
  let _store = store ?? initStore(preloadedState)

  // After navigating to a page with an initial Redux state, merge that state
  // with the current state in the store, and create a new store
  if (preloadedState && store) {
    _store = initStore({
      ...store.getState(),
      ...preloadedState,
    })
    // Reset the current store
    store = undefined
  }

  // For SSG and SSR always create a new store
  if (typeof window === 'undefined') return _store
  // Create the store once in the client
  if (!store) store = _store

  return _store
}

export function useStore(initialState) {
  const store = useMemo(() => initializeStore(initialState), [initialState])
  return store
}

export function removeUndefined(state) {
  if (typeof state === 'undefined') return null
  if (Array.isArray(state)) return state.map(removeUndefined)
  if (typeof state === 'object' && state !== null) {
    return Object.entries(state).reduce((acc, [key, value]) => {
      return {
        ...acc,
        [key]: removeUndefined(value)
      }
    }, {})
  }

  return state
}

;        

finally, let’s use this query in our index.js page

import { useState } from "react
import { useSelector } from "react-redux"
import { pokemonApi, useGetPokemonByNameQuery } from "../services/pokemon"
import { initializeStore, removeUndefined } from "../store"

export default function Home(props) {
  const {data: pokemonList} = useSelector(pokemonApi.endpoints.getPokemonList.select())
  const [pokemon, setPokemon] = useState(props.initialPokemon)

  const {data: currentPokemon} = useGetPokemonByNameQuery(pokemon)

  return (
    <>
    <h1>Hi</h1>
  <h2>You caught {pokemon}! They can have one of these abilities: {currentPokemon.abilities.map(ab => ab.ability.name).join(', ')}</h2>
    <p>
      Catch another!
      <select value={pokemon} onChange={e => setPokemon(e.currentTarget.value)}>
        {pokemonList.results.map(pok => <option>{pok.name}</option>)}
      </select>
    </p>
    </>
  )
}

export async function getServerSideProps() {
  const store = initializeStore()
  await store.dispatch(pokemonApi.endpoints.getPokemonList.initiate())
  const {data: pokemonList} = pokemonApi.endpoints.getPokemonList.select()(store.getState())
  const initialPokemon = pokemonList.results[0].name

  await store.dispatch(pokemonApi.endpoints.getPokemonByName.initiate(initialPokemon))

  // queryRef.unsubscribe() // I am not sure if something like this is necessary

  return { props: { initialReduxState: removeUndefined(store.getState()), initialPokemon } }
}
        

Apollo Client

The first step is to create an apollo client provider like this:

// ./apollo-client.j

import { ApolloClient, InMemoryCache } from "@apollo/client";

const client = new ApolloClient({
    uri: "https://countries.trevorblades.com",
    cache: new InMemoryCache(),
});

export default client;

s        

And then we wrap with it our _app.js

import { ApolloProvider } from "@apollo/client";
import client from "../apollo-client";

import "../styles/globals.css";

function MyApp({ Component, pageProps }) {
  return (
    <ApolloProvider client={client}>
      <Component {...pageProps} />
    </ApolloProvider>
  );
}

export default MyApp;        

And now we can use queries like this in our index.js file:

// pages/index.j

export async function getServerSideProps() {
  const { data } = await client.query({
    query: gql`
      query Countries {
        countries {
          code
          name
          emoji
        }
      }
    `,
  });

  return {
    props: {
      countries: data.countries.slice(0, 4),
    },
  };
}

s        

And generally, we create a specific folder for queries and another one for mutations and you export this part of the code from there:

import { gql } from '@apollo/client'
export const GET_COUNTRIES =
   gql`      
      query Countries {        
         countries {          
            code          
            name          
            emoji        
         }      
      }    
   `,        

React Query

First, let’s make your?QueryClient?globally available for your application.

import React from "react"
import type { AppProps } from "next/app";
import { QueryClient, QueryClientProvider, Hydrate } from "react-query";
import { ReactQueryDevtools } from "react-query/devtools";
import "../styles.css";

function MyApp({ Component, pageProps }: AppProps) {
  const [queryClient] = React.useState(() => new QueryClient());
  return (
    // Provide the client to your App
    <QueryClientProvider client={queryClient}>
      <Hydrate state={pageProps.dehydratedState}>
        <Component {...pageProps} />
        <ReactQueryDevtools initialIsOpen={false} />
      </Hydrate>
    </QueryClientProvider>
  );
}

export default MyApp;


;        

Read more about?Hydration here.

And finally, let’s costume define our query and use it like this:

import React from "react"
import axios from "axios";
import type { GetStaticProps, GetStaticPaths } from "next";
import { useQuery, QueryClient, dehydrate } from "react-query";
import { useRouter } from "next/router";
import PokemonCard from "../../components/PokemonCard";

const fetchPokemon = (id: string) =>
  axios
    .get(`https://pokeapi.co/api/v2/pokemon/${id}/`)
    .then(({ data }) => data);

export default function Pokemon() {
  const router = useRouter();
  const pokemonID = typeof router.query?.id === "string" ? router.query.id : "";

  const { isSuccess, data: pokemon, isLoading, isError } = useQuery(
    ["getPokemon", pokemonID],
    () => fetchPokemon(pokemonID),
    {
      enabled: pokemonID.length > 0,
      staleTime: Infinity
    }
  );

  if (isSuccess) {
    return (
      <div className="container">
        <PokemonCard
          name={pokemon.name}
          image={pokemon.sprites?.other?.["official-artwork"]?.front_default}
          weight={pokemon.weight}
          xp={pokemon.base_experience}
          abilities={pokemon.abilities?.map((item) => item.ability.name)}
        />
      </div>
    );
  }

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

  if (isError) {
    return (
      <div className="center">
        We couldn't find your pokemon{" "}
        <span role="img" aria-label="sad">
          ??
        </span>
      </div>
    );
  }

  return <></>;
}

export const getStaticProps: GetStaticProps = async (context) => {
  const id = context.params?.id as string;
  const queryClient = new QueryClient();

  await queryClient.prefetchQuery(["getPokemon", id], () => fetchPokemon(id));

  return {
    props: {
      dehydratedState: dehydrate(queryClient)
    }
  };
};

export const getStaticPaths: GetStaticPaths = async () => {
  return {
    paths: [],
    fallback: "blocking"
  };
};

;        

* * * * *

Conclusion

Here is what I recommend if you want to choose one of these 3 state managers/data fetching libraries:

  • If you are familiar with Redux, and you’re working on a big project with several global states and a lot of data fetching, then go for RTK it’ll be the easiest one to learn.
  • If the backend was made with GraphQL, then apollo client is your best choice without hesitation. Although you can also use it with any backend technology.
  • If you want the best developer experience, I would suggest choosing React Query for its simplicity and you can work with the UseContext hook to create your global client states.

要查看或添加评论,请登录

社区洞察

其他会员也浏览了