What State Manager to use with Next.JS ?
Ala Ben Aicha
3+ years of exp | Freelance Fullstack JS Developer (React, Node, React-Native)
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.
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.
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 ║ 2M ║ 2.5M ║1.4M ║
║ Website ║ 500K ║ 1M ║650K ║
╚══════╩═══════════╩═══════════╩══════════╝
* * * * *
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!
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:
Apollo Client
领英推荐
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.
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: