Chapter 5: Managing Local and Global States in Next.js

State management is one of the central parts of any React application, Next.js apps included. When talking about state, we refer to those dynamic pieces of information that allow us to create highly interactive user interfaces (UIs), making our customers' experience as beautiful and enjoyable as possible.

Thinking about modern websites, we can spot state changes in many parts of the UI: switching from light to dark theme means that we're changing the UI theme state, filling an e-commerce form with our shipping information means that we're changing that form state, even clicking on a simple button can potentially change a local state, as it can lead our UI to react in many different ways, depending on how the developers decided to manage that state update.

Even though state management allows us to create beautiful interactions inside our applications, it comes with some extra complexities. Many developers have come up with very different solutions to manage them, allowing us to manage the application state in more straightforward and organized ways.

Talking about React specifically, since the first versions of the library, we had access to the class components, where the class kept a local state, allowing us to interact with it via the setState method. With more modern React versions (>16.8.0), that process has been simplified with the introduction of React Hooks, including the useState Hook.

The most significant difficulty in managing state in a React application is that the data flow should be unidirectional, meaning that we can pass a given state as a prop to a child component, but we cannot do the same with a parent element. That means that local state management can be effortless thanks to class components and Hooks, but global state management can become really convoluted.

In this chapter, we will take a look at two different approaches for managing the global application state. First, we will see how to use React Context APIs; then, we will rewrite the application using Redux, which will let us understand how to initialize an external library for state management both on the client and server side.

We will look in detail at the following topics:

  • Local state management
  • Managing the application state via the Context APIs
  • Managing the application state via Redux

By the end of this chapter, you will learn the differences between local and global state management. You will also learn how to manage the global application state using the React built-in Context APIs or an external library such as Redux.

Technical requirements

To run the code examples in this chapter, you need to have both Node.js and npm installed on your local machine. If you prefer, you can use an online IDE such as https://repl.it or https://codesandbox.io, as they both support Next.js, and you don't need to install any dependency on your computer.

As with the other chapters, you can find the code base for this chapter on GitHub: https://github.com/PacktPublishing/Real-World-Next.js.

Local state management

When talking about local state management, we're referring to application state that is component-scoped. We can summarize that concept with an elementary Counter component:

import React, { useState } from "react";

function Counter({ initialCount = 0 }) {

  const [count, setCount] = useState(initialCount);

  return (

    <div>

      <b>Count is: {count}</b><br />

      <button onClick={() => setCount(count + 1)}>

        Increment +

      </button>

      <button onClick={() => setCount(count - 1)}>

        Decrement -

      </button>

    </div>

  )

}

export default Counter;

When we click on the Increment button, we will add 1 to the current count value. Vice-versa, we will subtract 1 to that value when we click on the Decrement button; nothing special!

But while it's easy for a parent component to pass an initialCount value as a prop for the Counter element, it can be way more challenging to do the opposite: passing the current count value to the parent component. There are many cases where we need to manage just the local state, and the React useState Hook can be an excellent way to do so. Those cases can include (but are not limited to) the following:

  • Atom components: As seen in Chapter 4, Organizing the Code Base and Fetching Data in Next.js, atoms are the most essential React components we can encounter, and they're likely to manage little local states only. More complex states can be delegated to molecules or organisms in many cases.
  • Loading states: When fetching external data on the client side, we always have a moment when we have neither some data nor an error, as we're still waiting for the HTTP request to complete. We can decide to handle that by setting a loading state to true until the fetch request is completed to display a nice loading spinner on the UI.

React Hooks such as useState and useReducer make local state management effortless, and most of the time, you don't need any external library to handle it.

Things can change once you need to maintain a global application state across all of your components. A typical example could be an e-commerce website, where once you add an item to the shopping cart, you may want to display the number of products you're buying with an icon inside your navigation bar.

We will talk about this specific example right in the next section.

Global state management

When talking about the global application state, we refer to a state shared between all the components for a given web application that is, therefore, reachable and modifiable by any component.

As seen in the previous section, the React data flow is unidirectional, meaning that components can pass data to their children components, but not to their parents (unlike Vue or Angular). That makes our components less error prone, easier to debug, and more efficient, but adds extra complexity: by default, there cannot be a global state.

Let's take a look at the following scenario:

Figure 5.1 – A link between product cards and items in the cart

Figure 5.1 – A link between product cards and items in the cart

In the web application shown in the preceding screenshot, we want to display many products and let our users put them in the shopping cart. The biggest problem here is that there's no link between the data shown in the navigation bar and the product cards, and it can be non-trivial to update the number of products in the cart as soon as the user clicks on the "add" button for a given product. And what if we want to keep this information on page change? It would be lost as soon as the single card components get unmounted with their local state.

Today, many libraries make it a bit easier to manage those situations: Redux, Recoil, and MobX are just some of the most popular solutions, but there are also other approaches. In fact, with React Hooks' introduction, we can use the Context APIs for managing the global application state without the need for external libraries. There's also a less popular approach that I'd like to take into consideration: using Apollo Client (and its in-memory cache). That would change the way we think of our state and gives us a formal query language for interacting with the global application data. If you're interested in that approach, I'd highly recommend reading the official Apollo GraphQL tutorial: https://www.apollographql.com/docs/react/local-state/local-state-management.

Starting from the next section, we will be building a very minimal storefront, just like the one we saw in the previous figure. Once the user adds one or more products to the shopping cart, we will update the count inside of the navigation bar. Once the user decides to proceed with the checkout, we will need to display the selected products on the checkout page.

Using the Context APIs

With React v16.3.0, released back in 2018, we finally got access to stable Context APIs. They give us a straightforward way to share data between all the components inside a given context without explicitly having to pass it via props from one component to another, even from children to a parent component. If you want to learn more about React Context, I highly recommend reading the official React documentation: https://reactjs.org/docs/context.html.

Starting with this section, we will always use the same boilerplate code for approaching global state management with different libraries. You can find this boilerplate code here: https://github.com/PacktPublishing/Real-World-Next.js/tree/main/05-state-management-made-easy/boilerplate.

We will also keep the same approach for storing the selected products in the global state for simplicity's sake; our state will be a JavaScript object. Each property is the ID of a product, and its value will represent the number of products that the user has selected. If you open the data/items.js file, you will find an array of objects representing our products. If a user selects four carrots and two onions, our state will look like this:

{

  "8321-k532": 4,

  "9126-b921": 2

}

That being said, let's start by creating the context for our shopping cart. We can do that by creating a new file: components/context/cartContext.js:

import { createContext } from 'react';

const ShoppingCartContext = createContext({

  items: {},

  setItems: () => null,

});

export default ShoppingCartContext;

Just like in a typical client-side rendered React app, we now want to wrap all the components that need to share the cart data under the same context. For instance, the /components/Navbar.js component needs to be mounted inside the same context as the /components/ProductCard.js component.

We should also consider that we want our global state to be persistent when changing the page, as we want to display the number of products selected by the user on the checkout page. That said, we can customize the /pages/_app.js page, as seen in Chapter 3, Next.js Basics and Built-In Components, to wrap the entire application under the same React context:

import { useState } from 'react';

import Head from 'next/head';

import CartContext from

  '../components/context/cartContext';

import Navbar from '../components/Navbar';

function MyApp({ Component, pageProps }) {

  const [items, setItems] = useState({});

  return (

    <>

      <Head>

        <link

href="https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css"

          rel="stylesheet"

        />

      </Head>

      <CartContext.Provider value={{ items, setItems }}>

        <Navbar />

        <div className="w-9/12 m-auto pt-10">

          <Component {...pageProps} />

        </div>

      </CartContext.Provider>

    </>

  );

}

export default MyApp;

As you can see, we're wrapping both <Navbar /> and <Component {...pageProps /> under the same context. That way, they gain access to the same global state, creating a link between all the components rendered on every page and the navigation bar.

Now let's take a quick look at the /pages/index.js page:

import ProductCard from '../components/ProductCard';

import products from '../data/items';

function Home() {

  return (

    <div className="grid grid-cols-4 gap-4">

      {products.map((product) => (

        <ProductCard key={product.id} {...product} />

      ))}

    </div>

  );

}

export default Home;

To make it simpler, we're importing all the products from a local JavaScript file, but of course, they could also come from a remote API. For each product, we render the ProductCard component, which will allow the users to add them to the shopping cart and then proceed to the checkout.

Let's take a look at the ProductCard component:

function ProductCard({ id, name, price, picture }) {

  return (

    <div className="bg-gray-200 p-6 rounded-md">

    <div className="relative 100% h-40 m-auto">

      <img src={picture} alt={name} className="object-cover" />

    </div>

    <div className="flex justify-between mt-4">

    <div className="font-bold text-l"> {name} </div>

    <div className="font-bold text-l text-gray-500"> ${price}       per kg </div>

    </div>

    <div className="flex justify-between mt-4 w-2/4 m-auto">

      <button

      className="pl-2 pr-2 bg-red-400 text-white rounded-md"

      disabled={false /* To be implemented */}

      onClick={() => {} /* To be implemented */}>

        -

      </button>

    <div>{/* To be implemented */}</div>

      <button

      className="pl-2 pr-2 bg-green-400 text-white rounded-md"      onClick={()  => {} /* To be implemented */}>

        +

      </button>

</div>

</div>

  );

}

export default ProductCard;

As you can see, we are already building the UI for that component, but nothing happens when clicking on both the increment and decrement buttons. We now need to link that component to the cartContext context and then update the context state as soon as the user clicks on one of the two buttons:

import { useContext } from 'react';

import cartContext from '../components/context/cartContext';

function ProductCard({ id, name, price, picture }) {

const { setItems, items } = useContext(cartContext);

// ...

Using the useContext Hook, we're linking both setItems and items from the _app.js page to our ProductCard component. Every time we call setItems on that component, we will be updating the global items object, and that change will be propagated to all the components living under the same context and linked to the same global state. That also means that we don't need to keep a local state for each ProductCard component, as the information about the number of single products added to the shopping cart already exists in our context state. Therefore, if we want to know the number of products added to the shopping cart, we can proceed as follows:

import { useContext } from 'react';

import cartContext from '../components/context/cartContext';

function ProductCard({ id, name, price, picture })

  const { setItems, items } = useContext(cartContext);

  const productAmount = id in items ? items[id] : 0;

// ...

That way, every time the user clicks on the increment button for a given product, the global items state will change, the ProductCard component will be re-rendered, and the productAmount constant will end up having a new value.

Talking again about handling both increment and decrement actions, we need to control the user clicks on those buttons. We can write a generic handleAmount function taking a single argument that can be either "increment" or "decrement". If the passed parameter is "increment", we need to check if the current product already exists inside the global state (remember, an initial global state is an empty object). If it exists, we only need to increment its value by one; otherwise, we need to create a new property inside the items object where the key will be our product ID, and its value will be set to 1.

If the parameter is "decrement", we should check whether the current product already exists inside of the global items object. If that's the case, and the value is greater than 0, we can just decrement it. In all other cases, we can just exit the function, as we cannot have a negative number as a value for an amount of our products:

import { useContext } from 'react';

import cartContext from '../components/context/cartContext';

function ProductCard({ id, name, price, picture }) {

  const { setItems, items } = useContext(cartContext);

  const productAmount = items?.[id] ?? 0;

  const handleAmount = (action) => {

    if (action === 'increment') {

      const newItemAmount = id in items ? items[id] + 1 : 1;

      setItems({ ...items, [id]: newItemAmount });

    }

    if (action === 'decrement') {

      if (items?.[id] > 0) {

        setItems({ ...items, [id]: items[id] - 1 });

      }

    }

  };

// ...

We now just need to update the increment and decrement buttons to trigger the handleAmount function on click:

<div className="flex justify-between mt-4 w-2/4 m-auto">

<button

  className="pl-2 pr-2 bg-red-400 text-white rounded-md"

  disabled={productAmount === 0}

  onClick={() => handleAmount('decrement')}>

    -

</button>

  <div>{productAmount}</div>

<button

  className="pl-2 pr-2 bg-green-400 text-white rounded-md"

  onClick={() => handleAmount('increment')}>

    +

</button>

</div>

If we now try to increment and decrement our products' amount, we will see the number inside of the ProductCard component changing after each button click! But looking at the navigation bar, the value will remain set to 0, as we haven't linked the global items state to the Navbar component. Let's open the /components/Navbar.js file and type the following:

import { useContext } from 'react';

import Link from 'next/link';

import cartContext from '../components/context/cartContext';

function Navbar() {

  const { items } = useContext(cartContext);

// ...

We don't need to update the global items state from our navigation bar, so, in that case, we don't need to declare the setItems function. In that component, we only want to display the total amount of products added to the shopping cart (for instance, if we add two carrots and one onion, we should see 3 as the total in our Navbar). We can do that quite easily:

import { useContext } from 'react';

import Link from 'next/link';

import cartContext from '../components/context/cartContext';

function Navbar() {

  const { items } = useContext(cartContext);

  const totalItemsAmount = Object.values(items)

    .reduce((x, y) => x + y, 0);

// ...

Now let's just display the totalItemsAmount variable inside of the resulting HTML:

// ...

<div className="font-bold underline">

  <Link href="/cart" passHref>

    <a>{totalItemsAmount} items in cart</a>

  </Link>

</div>

// ...

Great! We just missed one last thing: clicking on the Navbar link to the checkout page, we can't see any products displayed on the page. We can fix that by fixing the /pages/cart.js page:

import { useContext } from 'react';

import cartContext from '../components/context/cartContext';

import data from '../data/items';

function Cart() {

  const { items } = useContext(cartContext);

// ...

As you can see, we're importing the context objects as usual and the complete product list. That's because we need to get the complete product info (inside the state, we only have the relationship between a product ID and product amount) for displaying the name of the product, the amount, and the total price for that product. We then need a way to get the whole product object given a product ID. We can write a getFullItem function (outside of our component) that only takes an ID and returns the entire product object:

import { useContext } from 'react';

import cartContext from '../components/context/cartContext';

import data from '../data/items';

function getFullItem(id) {

  const idx = data.findIndex((item) => item.id === id);

  return data[idx];

}

function Cart() {

  const { items } = useContext(cartContext);

// ...

Now that we have access to the complete product object, we can get the total price for all of our products inside of the shopping cart:

// ...

function Cart() {

  const { items } = useContext(cartContext);

  const total = Object.keys(items)

    .map((id) => getFullItem(id).price * items[id])

    .reduce((x, y) => x + y, 0);

// ...

We also want to display a list of products inside of the shopping cart in the format of x2 Carrots ($7). We can easily create a new array called amounts and fill it with all the products we've added to the cart, plus the amount for every single product:

// ...

function Cart() {

  const { items } = useContext(cartContext);

  const total = Object.keys(items)

    .map((id) => getFullItem(id).price * items[id])

    .reduce((x, y) => x + y, 0);

  const amounts = Object.keys(items).map((id) => {

    const item = getFullItem(id);

    return { item, amount: items[id] };

  });

// ...

Now, we only need to update the returning template for that component:

// ...

<div>

<h1 className="text-xl font-bold"> Total: ${total} </h1>

<div>

  {amounts.map(({ item, amount }) => (

    <div key={item.id}>

      x{amount} {item.name} (${amount *

        item.price})

</div>

  ))}

</div>

</div>

// ...

And we're done! After booting the server, we can add as many products as we want to the shopping cart and see the total price going to the /cart page.

Using the context APIs in Next.js is not that difficult, as the concepts are the same for the vanilla React applications. In the next section, we will see how to achieve the same results using Redux as a global state manager.

Using Redux

In 2015, two years after the initial React public release, there weren't asmany frameworks and libraries as today for handling large-scale application states. The most advanced way for handling unidirectional data flow was Flux, which, as time has passed, has been superseded by more straightforward and modern libraries such as Redux and MobX.

Redux, in particular, had a significant impact on the React community and quickly became a de facto state manager for building large-scale applications in React.

In this section, we will be using plain Redux (without middlewares such as redux-thunk or redux-saga) for managing the storefront state instead of using the React Context APIs.

Let's start by cloning the boilerplate code from https://github.com/PacktPublishing/Real-World-Next.js/tree/main/05-managing-local-and-global-states-in-nextjs/boilerplate(just like we did in the previous section).

At this point, we will need to install two new dependencies:

yarn add redux react-redux

We can also install the Redux DevTools extension, which allows us to inspect and debug the application state from our browser:

yarn add -D redux-devtools-extension

Now we can start coding our Next.js + Redux application.

First of all, we will need to initialize the global store, which is the part of our application containing the application state. We can do that by creating a new folder inside of the root of our project, calling it redux/. Here we can write a new store.js file containing the logic for initializing our store on both the client side and server side:

import { useMemo } from 'react';

import { createStore, applyMiddleware } from 'redux';

import { composeWithDevTools } from 'redux-devtools-extension';

let store;

const initialState = {};

// ...

As you can see, we start by instantiating a new variable, store, which (as you may have guessed) will be used later on for keeping the Redux store.

Then, we initialize the initialState for our Redux store. In that case, it will be an empty object, as we will add more properties depending on which product our users select on the storefront.

We now need to create our first and only reducer. In a real-world application, we would write many different reducers in many different files, making things more manageable in terms of maintainability for our project. In that case, we will write just one reducer (as it is the only one we need), and we will include that in the store.js file for simplicity's sake:

//...

const reducer = (state = initialState, action) => {

  const itemID = action.id;

  switch (action.type) {

    case 'INCREMENT':

      const newItemAmount = itemID in state ?

        state[itemID] + 1 : 1;

      return {

        ...state,

        [itemID]: newItemAmount,

      };

    case 'DECREMENT':

      if (state?.[itemID] > 0) {

        return {

          ...state,

          [itemID]: state[itemID] - 1,

        };

      }

      return state;

    default:

      return state;

  }

};

The reducer's logic is not that different from the one we wrote in the previous section inside the handleAmount function for our ProductCard component.

Now we need to initialize our store, and we can do that by creating two different functions. The first one will be a simple helper function called initStore, and it will make things easier later on:

// ...

function initStore(preloadedState = initialState) {

  return createStore(

    reducer,

    preloadedState,

    composeWithDevTools(applyMiddleware())

  );

}

The second function we need to create is the one we will use for properly initializing the store, and we're going to call it initializeStore:

// ...

export const initializeStore = (preloadedState) => {

  let _store = store ?? initStore(preloadedState);

  if (preloadedState && store) {

    _store = initStore({

      ...store.getState(),

      ...preloadedState,

    });

    store = undefined;

  }

  //Return '_store' when initializing Redux on the server-side

  if (typeof window === 'undefined') return _store;

  if (!store) store = _store;

  return _store;

};

Now that we have our store set up, we can create one last function, a Hook we'll be using in our components. We'll wrap it inside a useMemo function to take advantage of the React built-in memoization system, which will cache complex initial states, avoiding the system re-parsing it on every useStore function call:

// ...

export function useStore(initialState) {

  return useMemo(

    () => initializeStore(initialState), [initialState]

  );

}

Great! We're now ready to move on and attach Redux to our Next.js application.

Just as we did with the Context APIs in the previous section, we will need to edit our _app.js file so that Redux will be globally available for every component living inside of our Next.js app:

import Head from 'next/head';

import { Provider } from 'react-redux';

import { useStore } from '../redux/store';

import Navbar from '../components/Navbar';

function MyApp({ Component, pageProps }) {

  const store = useStore(pageProps.initialReduxState);

  return (

  <>

<Head>

  <link href="https://unpkg.com/tailwindcss@^2/dist/tailwind.    min.css" rel="stylesheet" />

</Head>

  <Provider store={store}>

<Navbar />

  <div className="w-9/12 m-auto pt-10">

    <Component {...pageProps} />

  </div>

  </Provider>

</>

  );

}

export default MyApp;

If you compare this _app.js file with the one we created in the previous section, you may notice some similarities. From this moment, the two implementations will look very similar, as Context APIs try to make global state management more accessible and easier for everyone, and the Redux influence in shaping those APIs is visible.

We now need to implement the increment/decrement logic for our ProductCard component using Redux. Let's start by opening the components/ProductCard.js file and add the following imports:

import { useDispatch, useSelector, shallowEqual } from 'react-redux';

// ...

Now, let's create a Hook that will come in handy when we need to fetch all the products in our Redux store:

import { useDispatch, useSelector, shallowEqual } from 'react-redux';

function useGlobalItems() {

  return useSelector((state) => state, shallowEqual);

}

// ...

Staying inside the same file, let's edit the ProductCard component by integrating the Redux Hooks we need:

// ...

function ProductCard({ id, name, price, picture }) {

  const dispatch = useDispatch();

  const items = useGlobalItems();

  const productAmount = items?.[id] ?? 0;

  return (

// ...

Finally, we need to trigger a dispatch when the user clicks on one of our component's buttons. Thanks to the useDispatch Hook we previously imported, that operation will be straightforward to implement. We just need to update the onClick callback for our HTML buttons inside the render function as follows:

// ...

<div className="flex justify-between mt-4 w-2/4 m-auto">

  <button

    className="pl-2 pr-2 bg-red-400 text-white rounded-md"

    disabled={productAmount === 0}

    onClick={() => dispatch({ type: 'DECREMENT', id })}>

      -

  </button>

<div>{productAmount}</div>

  <button

    className="pl-2 pr-2 bg-green-400 text-white rounded-md"

    onClick={() => dispatch({ type: 'INCREMENT', id })}>

      +

  </button>

</div>

// ...

Suppose you've installed the Redux DevTools extension for your browser. In that case, you can now start to increment or decrement a product and see the action as it is dispatched directly inside your debugging tools.

By the way, we still need to update the navigation bar when we add or remove a product from our cart. We can easily do that by editing the components/NavBar.js component just as we did for the ProductCard one:

import Link from 'next/link';

import { useSelector, shallowEqual } from 'react-redux';

function useGlobalItems() {

  return useSelector((state) => state, shallowEqual);

}

function Navbar() {

  const items = useGlobalItems();

  const totalItemsAmount = Object.keys(items)

    .map((key) => items[key])

    .reduce((x, y) => x + y, 0);

  return (

    <div className="w-full bg-purple-600 p-4 text-white">

    <div className="w-9/12 m-auto flex justify-between">

    <div className="font-bold">

      <Link href="/" passHref>

        <a> My e-commerce </a>

      </Link>

    </div>

    <div className="font-bold underline">

      <Link href="/cart" passHref>

        <a>{totalItemsAmount} items in cart</a>

      </Link>

     </div>

     </div>

     </div>

  );

}

export default Navbar;

We can now try to add and remove products from our storefront and see the state change reflected in the navigation bar.

One last thing before we can consider our e-commerce app complete: we need to update the /cart page so that we can see a summary of the shopping cart before moving to the checkout step. It will be incredibly easy, as we will combine what we learned from the previous section using the Context APIs and the knowledge of Redux Hooks we've just gained. Let's open the pages/Cart.js file and import the same Redux Hook we used for the other components:

import { useSelector, shallowEqual } from 'react-redux';

import data from '../data/items';

function useGlobalItems() {

  return useSelector((state) => state, shallowEqual);

}

// ...

At this point, we can just replicate the getFullItem function we created for the Context APIs in the previous section:

// ...

function getFullItem(id) {

  const idx = data.findIndex((item) => item.id === id);

  return data[idx];

}

// ...

The same happens for the Cart component. We will basically replicate what we did in the previous section, with a simple difference: the items object will come from the Redux store instead of a React context:

function Cart() {

  const items = useGlobalItems();

  const total = Object.keys(items)

    .map((id) => getFullItem(id).price * items[id])

    .reduce((x, y) => x + y, 0);

  const amounts = Object.keys(items).map((id) => {

    const item = getFullItem(id);

    return { item, amount: items[id] };

  });

  return (

    <div>

      <h1 className="text-xl font-bold"> Total: ${total}  

      </h1>

    <div>

        {amounts.map(({ item, amount }) => (

          <div key={item.id}>

            x{amount} {item.name} (${amount * item.price})

    </div>

        ))}

    </div>

    </div>

  );

}

export default Cart;

If you now try to add a couple of products to your shopping cart, then move to the /cart page, you will see a summary of your expenses.

As you may have noticed, there aren't many differences between Context APIs and plain Redux (without using any middleware). By using Redux, by the way, you'll gain access to an incredibly vast ecosystem of plugins, middleware, and debugging tools that will make your developer experience way more effortless once you need to scale and handle very complex business logic inside of your web application.

Summary

In this chapter, we focused on state management using both React built-in APIs (the Context APIs and Hooks) and external libraries (Redux). There are many other tools and libraries for managing an application's global state (MobX, Recoil, XState, Unistore, to name just a few). You can use all of them inside your Next.js application by initializing them for both client-side and server-side usage, just like we did with Redux.

Also, you can use Apollo GraphQL and its in-memory cache for managing your application state, gaining access to a formal query language for mutating and querying your global data.

We can now create more complex and interactive web applications, managing different kinds of state with any library we want.

But once you have your data well organized and ready to use, you need to display it and render your application UI depending on your application state. In the next chapter, you will see how to style your web app by configuring and using different CSS and JavaScript libraries.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.145.178.157