I'm now playing around with this new basic blog template from Next.js examples --> available on Next.js GitHub.

Luciano Lupo Notes.

Zustand for modals state

Cover Image for Zustand for modals state

TL;DR Use Zustand!

Redux is famous for its rich ecosystem and plethora of middleware and tooling options. But, let's be honest, it can also require a bit more work upfront to set up and manage the store, actions, and reducers. If you're looking for a more streamlined approach, Zustand is worth checking out. This library is all about simplifying things and reducing boilerplate code. With Zustand, you'll enjoy a more straightforward API and fewer code constructs to worry about when defining and using your stores, actions, and selectors.

To start I'm gonna copy paste the example form the docs page because is worth being shown:

import { create } from "zustand";

const useStore = create((set) => ({
  bears: 0,
  increasePopulation: () => set((state) => ({ bears: state.bears + 1 })),
  removeAllBears: () => set({ bears: 0 }),
  updateBears: (newBears) => set({ bears: newBears }),
}));

function BearCounter() {
  const bears = useStore((state) => state.bears);
  return <h1>{bears} around here...</h1>;
}

function Controls() {
  const increasePopulation = useStore((state) => state.increasePopulation);
  return <button onClick={increasePopulation}>one up</button>;
}

It is really that easy to use.

Now lets deep dive

This is solution for modals (open, close, and internal data), this particular case is from a real project in which I've worked on, so I'm gonna change names and data, and also is part of a larger thing that involves ChakraUI for the UI, but bare with me in the important part, the store and state management ( open and close )

First we need to make the types

import { create, StateCreator } from "zustand";

export const MODALS = {
  RANDOM_MODAL_NAME: "RANDOM_MODAL_NAME",
  RANDOM_MODAL_NAME_2: "RANDOM_MODAL_NAME_2",
} as const;

type MODALS = (typeof MODALS)[keyof typeof MODALS];
interface BaseModalState {
  isOpen: boolean;
  internalState?: unknown;
}

type ModalState = {
  [K in keyof typeof MODALS]: BaseModalState;
};

export interface ModalsSlice {
  modals: ModalState;
  open: (modalKey: MODALS) => void;
  close: (modalKey: MODALS) => void;
  setInternalData: (payload: {
    modalKey: MODALS;
    internalState: unknown;
  }) => void;
  resetModalToDefault: (modalKey: MODALS) => void;
}

then the functions we will use:


const createModals = (keys: (keyof typeof MODALS)[]): ModalState => {
  const initialModalsState: Partial<ModalState> = {};
  keys.forEach((key) => {
    const modalEntry: { [K in typeof key]?: BaseModalState } = {
      [key]: {
        isOpen: false,
        internalState: {},
      },
    };
    Object.assign(initialModalsState, modalEntry);
  });
  return initialModalsState as ModalState;
};

The initial state:

export const modalsInitialState = createModals(
  Object.keys(MODALS) as (keyof typeof MODALS)[]
);

And the slice:

const createModalsSlice: StateCreator<ModalsSlice, [], [], ModalsSlice> = (
  set
) => ({
  modals: { ...modalsInitialState },
  open: (modalKey: MODALS) =>
    set((state) => ({
      modals: {
        ...state.modals,
        [modalKey]: { ...state.modals[modalKey], isOpen: true },
      },
    })),
  close: (modalKey: MODALS) =>
    set((state) => ({
      modals: {
        ...state.modals,
        [modalKey]: { ...state.modals[modalKey], isOpen: false },
      },
    })),
  setInternalData: (payload: { modalKey: MODALS, internalState: unknown }) =>
    set((state) => ({
      modals: {
        ...state.modals,
        [payload.modalKey]: {
          ...state.modals[payload.modalKey],
          internalState: payload.internalState,
        },
      },
    })),
  resetModalToDefault: (modalKey: MODALS) =>
    set((state) => ({
      modals: { ...state.modals, [modalKey]: { isOpen: false } },
    })),
});

and finally the hook to use it:


export const useModalsStore = create<ModalsSlice>()((...a) => ({
  ...createModalsSlice(...a),
}));

full code:

import { create, StateCreator } from "zustand";

export const MODALS = {
  RANDOM_MODAL_NAME: "RANDOM_MODAL_NAME",
  RANDOM_MODAL_NAME_2: "RANDOM_MODAL_NAME_2",
} as const;

type MODALS = (typeof MODALS)[keyof typeof MODALS];
interface BaseModalState {
  isOpen: boolean;
  internalState?: unknown;
}

type ModalState = {
  [K in keyof typeof MODALS]: BaseModalState;
};

export interface ModalsSlice {
  modals: ModalState;
  open: (modalKey: MODALS) => void;
  close: (modalKey: MODALS) => void;
  setInternalData: (payload: {
    modalKey: MODALS;
    internalState: unknown;
  }) => void;
  resetModalToDefault: (modalKey: MODALS) => void;
}

const createModals = (keys: (keyof typeof MODALS)[]): ModalState => {
  const initialModalsState: Partial<ModalState> = {};
  keys.forEach((key) => {
    const modalEntry: { [K in typeof key]?: BaseModalState } = {
      [key]: {
        isOpen: false,
        internalState: {},
      },
    };
    Object.assign(initialModalsState, modalEntry);
  });
  return initialModalsState as ModalState;
};

export const modalsInitialState = createModals(
  Object.keys(MODALS) as (keyof typeof MODALS)[]
);

const createModalsSlice: StateCreator<ModalsSlice, [], [], ModalsSlice> = (
  set
) => ({
  modals: { ...modalsInitialState },
  open: (modalKey: MODALS) =>
    set((state) => ({
      modals: {
        ...state.modals,
        [modalKey]: { ...state.modals[modalKey], isOpen: true },
      },
    })),
  close: (modalKey: MODALS) =>
    set((state) => ({
      modals: {
        ...state.modals,
        [modalKey]: { ...state.modals[modalKey], isOpen: false },
      },
    })),
  setInternalData: (payload: { modalKey: MODALS; internalState: unknown }) =>
    set((state) => ({
      modals: {
        ...state.modals,
        [payload.modalKey]: {
          ...state.modals[payload.modalKey],
          internalState: payload.internalState,
        },
      },
    })),
  resetModalToDefault: (modalKey: MODALS) =>
    set((state) => ({
      modals: { ...state.modals, [modalKey]: { isOpen: false } },
    })),
});

export const useModalsStore = create<ModalsSlice>()((...a) => ({
  ...createModalsSlice(...a),
}));

of course seems a lot more complex than basic examples but is not so much more difficult….

Lets continue with the modal component:

const RandomModalNameComponent = () => {
  const { isOpen } = useModalsStore((state) => state.modals.RANDOM_MODAL_NAME);
  const { close } = useModalsStore();

  const onExit = () => {
    close(MODALS.RANDOM_MODAL_NAME);
  };

  return (
    <Modal onClose={onExit} isOpen={isOpen}>
      <ModalOverlay />
      <ModalContent>
        <CustomModalHeader headingText={"Title of my modal"} />
        <ModalCloseButton />
        <ModalBody>
          <Text> Some text </Text>
        </ModalBody>
        <ModalFooter>
          <Flex>
            <Button onClick={onExit}>Cancel</Button>
            <ConnectExternalWalletButton onConnect={onExit} />
          </Flex>
        </ModalFooter>
      </ModalContent>
    </Modal>
  );
};

and lastly this is how we can call it from whatever component we want

const SomeRandomComponent = () => {
  const { open } = useModalsStore();

  return (
    <Box>
    // ... some really important code ...
      <Button onClick={() =>open('RANDOM_MODAL_NAME'))}>Open The Modal</Button>
    // ... more really important code ...
    </Box>
  );
};

Lets say you want to update internal data of the modal, just call the setInternalData before calling the open function there is no complex pattern structure, no ACTION and REDUCER, just a store and hooks for implementing functions, just define internalData for that modal (the types) and import the types in the modal component.