DEV Community

loading...
Cover image for Build an email subscription app - Part 2: Connect Zustand
Buildable.dev

Build an email subscription app - Part 2: Connect Zustand

moekatib profile image Moe Katib Updated on ・7 min read

Overview

This tutorial is a 3-part series and will give a detailed walk-through of how to do state management on the component level with Zustand. We'll show how to use Zustand in a tactically relevant way while creating a fully integrated React component.

Here's a breakdown of what we'll be covering throughout the series:

As a caveat, each part above is linked to a Code Sandbox, complete with the section's fully completed code, for convenience. To make the most use of your time while following this tutorial, we recommend opening and forking the part's sandbox at the beginning of the section in a separate tab. Our Code Sandbox example can be your 'target repo'. While you're completing each part of the tutorial, your goal should be to write code that eventually resembles the target.

Prerequisites

This part has the same prerequisites as Part 1.

Our Objective

Frontends can be stubborn - and a huge headache - so let's try to remove as much complexity as possible. In this part of the tutorial, we're going to explore Zustand and create our data models, create our store and hook them up to our React component that we set up in Part 1.

Getting Started: Zustand Overview

Understanding Zustand

Zustand is a flexible state management system. Here’s how the creators of Zustand describe the tool:

“A small, fast and scaleable bearbones state-management solution. Has a comfy api based on hooks, isn't boilerplatey or opinionated, but still just enough to be explicit and flux-like. Don't disregard it because it's cute. It has quite the claws, lots of time was spent to deal with common pitfalls, like the dreaded zombie child problem, react concurrency, and context loss between mixed renderers. It may be the one state-manager in the React space that gets all of these right.”

Installing Zustand

To start connecting Zustand, we’ll need to NPM install Zustand.

npm i zustand
Enter fullscreen mode Exit fullscreen mode

Creating a fallback file

Since we're going to be using Zustand to load the component's content dynamically, we always want to have backup content available in case there's any unexpected issue loading data. We'll house this backup content in our fallback.js file.

Head to the src/components/email-block folder and add your fallback.js file here. Once the file is generated, add the following code snippet:

const content = {
  title: "Become smarter in just 5 minutes",
  subTitle:
    "Get the daily email that makes reading the news actually enjoyable. Stay informed and entertained, for free.",
  input: {
    id: "email-input",
    type: "email",
    label: "Email is required please",
    placeholder: "Enter your email",
    variant: "outlined"
  },
  errors: {
    invalidEmail: "We require a valid email",
    empty: "Email is required please"
  },
  button: {
    states: {
      initial: "Submit",
      processing: "Sending request",
      success: "Sent successfully",
      failed: "Failed! Try again."
    }
  }
};

export { content };
Enter fullscreen mode Exit fullscreen mode

Understanding the component's states

We're going to control the value of the component's button in accordance with its state. Once our component is loading, it can be in one of the following four states 1) initial 2) processing 3) success or 4) failed

https://uploads-ssl.webflow.com/5f6b9731fe02400ffade5b26/6026b8a5bccd1c11fcedc0a9_button.gif

Adding a Logic folder

Head to src/components/email-block folder and create a logic folder. Inside this folder, create the following files: data-models.js, flows.js and store.js. To keep things clean, it's important that these three files exist independently. Your current folder structure should look like this:

https://uploads-ssl.webflow.com/5f6b9731fe02400ffade5b26/6026b85d410f5b25ad9c7768_Screen_Shot_2021-01-21_at_8.05.17_PM.png

Creating our two data models

A model is nothing more than a piece of data that we want to manage across the application. As a professional preference, we like to separate the data models from the store. For this app, we'll need two data models - the content model and the loading model.

Let's start by tackling the content model, which will be responsible for the Title, Subtitle and Button Text. In the model, we're going to need:

  • content
  • currentButtonText
  • setButtonText
  • getContent (used to get content dynamically)
  • setContent (used to update the inner state of the content)

Head to the src/components/logic/data-models.js file and add the following code snippet:

import { content } from "../fallback";

const initContentModel = (set) => ({
  content,
  currentButtonText: content.button?.states?.initial,
  setButtonText: (buttonText) =>
    set((state) => ({ ...state, currentButtonText: buttonText })),
  getContent: () => {},
  setContent: (content) => {
    set((state) => ({ ...state, content }));
  }
});

export { initContentModel };

Enter fullscreen mode Exit fullscreen mode

In this code, we are creating an initContentModel function to help us organize our Zustand store. At this stage, we are using the content from the fallback.js file as the default value. We'll be changing this to dynamic data later on in the tutorial.

While we're here, let's tackle the loading model, which will be responsible for loading and processing. Loading is used when we are requesting data from a server and Processing is used for when we are sending data to a server. In the model, we're going to need:

  • loading
  • processing
  • setLoading
  • clearLoading
  • setProcessing
  • clearProcessing

In the same src/components/logic/data-models.js file, expand the file using the following code snippet:

import { content } from "../fallback";

const initContentModel = (set) => ({
  content,
  currentButtonText: content.button?.states?.initial,
  setButtonText: (buttonText) =>
    set((state) => ({ ...state, currentButtonText: buttonText })),
  getContent: () => {},
  setContent: (content) => {
    set((state) => ({ ...state, content }));
  }
});

const initLoadingModel = (set) => ({
  loading: false,
  processing: false,
  setLoading: () => {
    set((state) => ({ ...state, loading: true }));
  },
  clearLoading: () => {
    set((state) => ({ ...state, loading: false }));
  },
  setProcessing: () => {
    set((state) => ({ ...state, processing: true }));
  },
  clearProcessing: () => {
    set((state) => ({ ...state, processing: false }));
  }
});

export { initContentModel, initLoadingModel };

Enter fullscreen mode Exit fullscreen mode

Creating the Zustand store

For clarity's sake, let's first define what a store actually is before doing anything else. According to Zustand documentation, stores are simply hooks. Here's a snippet from the NPM page:

https://uploads-ssl.webflow.com/5f6b9731fe02400ffade5b26/6026b8c3dd15e6cc9d6613cd_Screen_Shot_2021-01-21_at_8.14.35_PM.png

We're now going to create our Zustand store and add our previously created data models. To do this, head to the src/components/email-block/logic/store.js file and add the following code snippet:

import create from "zustand";
import { devtools } from "zustand/middleware";
import { initContentModel, initLoadingModel } from "./data-models";

const [useStore] = create(
  devtools((set) => ({
    ...initContentModel(set),
    ...initLoadingModel(set)
  })),
  "smart-blocks-store"
);

export { useStore };
Enter fullscreen mode Exit fullscreen mode

We are using the dev tools to help us with debugging. For more on Zustand's dev tools, visit Zustand's documentation on dev tools:

Congrats! Your store is now created 🎉

Connecting the Store to the component

To connect the store to the component, it's as easy as using a hook. Head back to src/components/email-block/EmailBlock.js and import useStore from the src/components/email-block/logic/store.js file.

We will use useStore to grab the content, the loading state and the processing state.

import React from "react";
import { Box, Text, Heading } from "grommet";
import { TextField } from "@material-ui/core";
import theme from "../../theme";
import Button from "./Button";
import LoadingBlock from "./LoadingBlock";
import { useStore } from "./logic/store";

const { colors } = theme;

const WrapperBox = ({ children }) => (
  <Box
    elevation={"large"}
    width={"500px"}
    round="8px"
    background={colors.white}
    pad={"large"}
    gap={"medium"}
  >
    {children}
  </Box>
);

const EmailBlock = () => {
  const [isLoading, isProcessing] = useStore((state) => [
    state.loading,
    state.processing
  ]);

  const [content, currentButtonText] = useStore((state) => [
    state.content,
    state.currentButtonText
  ]);

  return (
    <>
      {isLoading && (
        <WrapperBox>
          <LoadingBlock />
        </WrapperBox>
      )}
      {!isLoading && (
        <WrapperBox>
          <Heading level={1} color={colors.black}>
            {content.title}
          </Heading>
          <Text size={"medium"}>{content.subTitle}</Text>
          <TextField {...content.input} />
          <Button
            type="submit"
            onClick={(e) => {
              e.preventDefault();
            }}
            disabled={isProcessing}
            background={colors.primary}
            color={colors.white}
            style={{
              paddingTop: "16px",
              paddingBottom: "16px"
            }}
          >
            {currentButtonText}
          </Button>
        </WrapperBox>
      )}
    </>
  );
};

export default EmailBlock;

Enter fullscreen mode Exit fullscreen mode

Testing the connection

At this point, you should see the text being pulled successfully from our fallback.js file. We can also test the connection by setting the loading initial state to true and confirming that the component UI is actually showing the skeleton loader.

https://uploads-ssl.webflow.com/5f6b9731fe02400ffade5b26/6026b914d6756c470a597017_loading.gif

Creating a dispatch for the button

Now that we have our UI fully connected to the Zustand store, we can manage the button dispatch. As a caveat, our professional preference is to organize any user triggered event in a flows.js file. This will allow us to transition the button state following a user event, like a button click:

https://uploads-ssl.webflow.com/5f6b9731fe02400ffade5b26/6026b92409f1df1344b4c4c7_button_state.gif

Let's create a wait function to simulate a network delay. Inside the flows.js file, add this code snippet:

const wait = async (time) =>
  new Promise((resolve) => setTimeout(() => resolve(true), time));

Enter fullscreen mode Exit fullscreen mode

In the same file, let's create an empty function useDispatchEmailFlow that will return an async dispatch function.

const useDispatchEmailFlow = () => {

  const dispatch = async () => {};

  return dispatch;
}

Enter fullscreen mode Exit fullscreen mode

Once that's done, update the useDispatchEmailFlow function with the following code:

import { useStore } from "./store";

const wait = async (time) =>
  new Promise((resolve) => setTimeout(() => resolve(true), time));

const useDispatchEmailFlow = () => {
  const [
    setProcessing,
    clearProcessing,
    setButtonText,
    buttonStates
  ] = useStore((store) => [
    store.setProcessing,
    store.clearProcessing,
    store.setButtonText,
    store.content?.button?.states
  ]);

  const dispatch = async () => {
    setProcessing();
    setButtonText(buttonStates?.processing);
    await wait(2000);
    setButtonText(buttonStates?.success);
    await wait(1000);
    setButtonText(buttonStates?.initial);
    clearProcessing();
  };
  return dispatch;
};

export { useDispatchEmailFlow };

Enter fullscreen mode Exit fullscreen mode

As you can see, inside the use useDispatchEmailFlow we're using the Zustand store to grab the functions setProcessing, clearProcessing, setButtonText and the content of the button. Inside the dispatch function, we're simulating a network call using the wait function.

Now, when you click on the button, you should see the button get disabled, transition states, then reset. Pretty cool, right?!

Conclusion

That’s it! In this part of the series, you learned how to set up data models, create a store and connect it to a React component.

https://uploads-ssl.webflow.com/5f6b9731fe02400ffade5b26/602c14eb142907b117481624_giphy-1.gif

Head to Part 3 of this tutorial where we will create the backend logic and microservice data resources for our web app. Our goal will be to complete and deploy everything in less than 20 minutes.

You can find the app’s finished up until this point on this Code Sandbox.

Discussion (0)

pic
Editor guide