Managing React State with Zustand

Aaron K Saunders - Jul 13 '21 - - Dev Community

Managing React State with Zustand

Managing React State with Zustand

A small, fast and scalable bearbones state-management solution using simplified flux principles. Has a comfy api based on hooks, isn't boilerplatey or opinionated.

  • Zustand - https://zustand.surge.sh/ - Means condition or state in German - definition of zustand

  • global state is all of the objects that are needed through out the application

  • There are various state management packages available for react, the most common would probably be react-redux. We are looking for a more straight forward state management library that doesn't require a lot of boiler-plate

    Blog Post on different State Management Libraries - React State Management Libraries and How to Choose

  • Apps have local state which are local to the specific component, this can be managed with useState

  • I think that zustand is simple enough to pick up and use without requiring a lot of deep understanding of state management concepts and it just works... It is exactly what will work in place of react-redux

Simple list of students that can be created, updated or delete

  • Create the store and then add the necessary functions
// store/index.js
import create from 'zustand';

const useStore = create(set => ({
  students: [
    { id: '1', name: 'Aaron Saunders', section: 'advanced' },
    { id: '2', name: 'Andrea Saunders', section: 'beginners' },
    { id: '3', name: 'Bill Smith', section: 'beginners' },
    { id: '4', name: 'John Chambers', section: 'beginners' },
    { id: '5', name: 'Joe Johnson', section: 'advanced' }
  ]
}));
export const useStudentStore = useStore;
Enter fullscreen mode Exit fullscreen mode
  • a function to add
  addStudent: student =>
    set(state => ({
      students: [
        {
          name: student.name,
          id: Math.random() * 100 + '',
          section: student.section
        },
        ...state.students
      ]
    })),
Enter fullscreen mode Exit fullscreen mode
  • a function to update
updateStudent: student =>
    set(state => ({
      students: state.students.map(item => {
        if (item.id === student.id) {
          return {
            ...item,
            name: student.name,
            section: student.section
          };
        } else {
          return item;
        }
      })
    }))
Enter fullscreen mode Exit fullscreen mode
  • a function to delete
  removeStudent: id =>
    set(state => ({
      students: state.students.filter(student => student.id !== id)
    })),
Enter fullscreen mode Exit fullscreen mode
  • a way to get all of the students, since the students are a property on the store we can access it directly
 const students = useStudentStore(state => state.students);
Enter fullscreen mode Exit fullscreen mode
  • a way to get an individual student, since students are on the store we can access the students directly and filter based on id. We can also cache or memoize the value with react's useCallback hook and reuse the result as long as the student id doesn't change
  const student = useStudentStore(
    useCallback(state => state.students.find(s => s.id === studentId), [
      studentId
    ])
  );
Enter fullscreen mode Exit fullscreen mode

Accessing The Store

// import it in the component
import { useStudentStore } from '../store';

// use it in the function, get all the students
const students = useStudentStore(state => state.students);

// get the function from the store to add students
const addStudent = useStudentStore(state => state.addStudent);
Enter fullscreen mode Exit fullscreen mode

Full App Example

https://stackblitz.com/edit/react-managing-state-with-zustand?file=src/pages/Detail.jsx

create a blank app with ionic

ionic start --type=react
Enter fullscreen mode Exit fullscreen mode

install zustand - https://zustand.surge.sh/

npm install zustand
Enter fullscreen mode Exit fullscreen mode
// store/index.js
import create from 'zustand';
import { devtools, persist } from 'zustand/middleware';

const useStore = create(set => ({
  students: [
    { id: '1', name: 'Aaron Saunders', section: 'advanced' },
    { id: '2', name: 'Andrea Saunders', section: 'beginners' },
    { id: '3', name: 'Bill Smith', section: 'beginners' },
    { id: '4', name: 'John Chambers', section: 'beginners' },
    { id: '5', name: 'Joe Johnson', section: 'advanced' }
  ],
  addStudent: student =>
    set(state => ({
      students: [
        {
          name: student.name,
          id: Math.random() * 100 + '',
          section: student.section
        },
        ...state.students
      ]
    })),
  removeStudent: id =>
    set(state => ({
      currentStudent: state.students.filter(student => student.id !== id)
    })),
  updateStudent: student =>
    set(state => ({
      students: state.students.map(item => {
        if (item.id === student.id) {
          return {
            ...item,
            name: student.name,
            section: student.section
          };
        } else {
          return item;
        }
      })
    }))
}));

export const useStudentStore = useStore;
Enter fullscreen mode Exit fullscreen mode
// Home.jsx
import {
  IonButton,
  IonButtons,
  IonContent,
  IonHeader,
  IonIcon,
  IonItem,
  IonLabel,
  IonList,
  IonModal,
  IonPage,
  IonTitle,
  IonToolbar
} from '@ionic/react';
import { trashOutline, pencilOutline } from 'ionicons/icons';

import { useStudentStore } from '../store';
import AddStudentModal from '../components/AddStudentModal';
import React, { useState } from 'react';
import { useHistory } from 'react-router';

const Home = () => {
  const history = useHistory();
  const [modalOpen, setModalOpen] = useState(false);
  const [modalData, setModalData] = useState(null);
  const students = useStudentStore(state => state.students);
  const addStudent = useStudentStore(state => state.addStudent);
  const removeStudent = useStudentStore(state => state.removeStudent);
  const updateStudent = useStudentStore(state => state.updateStudent);

  console.log(students);

  /**
   *
   * @param response
   */
  const handleModalClose = response => {
    setModalOpen(false);
    if (response) {
      console.log(response);
      if (response.id) {
        updateStudent({
          name: response.name,
          section: response.section,
          id: response.id
        });
      } else {
        addStudent({ name: response.name, section: response.section });
      }
    }
    modalData && setModalData(null);
  };

  const handleDelete = id => {
    removeStudent(id);
  };

  const editItem = item => {
    setModalData(item);
    setModalOpen(true);
  };

  return (
    <IonPage>
      <IonHeader>
        <IonToolbar>
          <IonTitle>Student Manager</IonTitle>
          <IonButtons slot="end">
            <IonButton onClick={() => setModalOpen(true)}>ADD</IonButton>
          </IonButtons>
        </IonToolbar>
      </IonHeader>
      <IonContent fullscreen>
        {/* <!-- modal --> */}
        <IonModal isOpen={modalOpen}>
          <AddStudentModal
            onCloseModal={handleModalClose}
            initialData={modalData}
          />
        </IonModal>
        {/* <!-- list --> */}
        <IonList>
          {students.map(item => (
            <IonItem key={item.id}>
              <IonLabel onClick={() => history.push(`/detail/${item.id}`)}>
                <h1>{item.section}</h1>
                {item.name}
              </IonLabel>
              <IonButton
                onClick={() => handleDelete(item.id)}
                fill="outline"
                color="danger"
              >
                <IonIcon color="danger" icon={trashOutline} />
              </IonButton>
              <IonButton onClick={() => editItem(item)} fill="outline">
                <IonIcon icon={pencilOutline} />
              </IonButton>
            </IonItem>
          ))}
        </IonList>
      </IonContent>
    </IonPage>
  );
};

export default React.memo(Home);
Enter fullscreen mode Exit fullscreen mode
// Detail.jsx
import {
  IonButton,
  IonBackButton,
  IonButtons,
  IonContent,
  IonHeader,
  IonIcon,
  IonItem,
  IonLabel,
  IonList,
  IonModal,
  IonPage,
  IonTitle,
  IonToolbar
} from '@ionic/react';

import { useStudentStore } from '../store';
import AddStudentModal from '../components/AddStudentModal';
import React, { useState, useEffect, useCallback } from 'react';
import { useParams } from 'react-router';
import { useStudentStore } from '../store';

const Detail = () => {
  const { studentId } = useParams();
  const student = useStudentStore(
    useCallback(state => state.students.find(s => s.id === studentId), [
      studentId
    ])
  );

  return (
    <IonPage>
      <IonHeader>
        <IonToolbar>
          <IonTitle>Student Manager</IonTitle>
          <IonButtons slot="start">
            <IonBackButton />
          </IonButtons>
        </IonToolbar>
      </IonHeader>
      <IonContent fullscreen className="ion-padding">
        <p>{student && JSON.stringify(student)}</p>
      </IonContent>
    </IonPage>
  );
};

export default React.memo(Detail);
Enter fullscreen mode Exit fullscreen mode

Using the state information from above to create an input form to capture the data on the students when they need to be added and removed from the list

// AddStudentModal.jsx
import {
  IonButton,
  IonContent,
  IonHeader,
  IonInput,
  IonItem,
  IonLabel,
  IonPage,
  IonSelect,
  IonSelectOption,
  IonToolbar
} from '@ionic/react';
import React, { useState, useEffect } from 'react';

const AddStudentModal = ({ onCloseModal, initialData }) => {
  const [section, setSection] = useState();
  const [name, setName] = useState();

  useEffect(() => {
    setSection(initialData?.section);
    setName(initialData?.name);
  }, []);

  const handleCancel = () => {
    onCloseModal(null);
  };

  const handleSave = () => {
    onCloseModal({
      name,
      section,
      id: initialData?.id
    });
  };

  return (
    <IonPage>
      <IonHeader>
        <IonToolbar />
      </IonHeader>
      <IonContent className="ion-padding">
        <strong>Ready to create an app?</strong>
        <IonItem>
          <IonLabel>Name</IonLabel>
          <IonInput value={name} onIonChange={e => setName(e.detail.value)} />
        </IonItem>

        <IonItem>
          <IonLabel>Section</IonLabel>
          <IonSelect
            value={section}
            placeholder="Select One"
            onIonChange={e => setSection(e.detail.value)}
          >
            <IonSelectOption value="advanced">Advanced</IonSelectOption>
            <IonSelectOption value="beginners">Beginners</IonSelectOption>
          </IonSelect>
        </IonItem>
        <div style={{ paddingTop: 10 }}>
          <IonButton onClick={handleSave}>SAVE STUDENT</IonButton>
          <IonButton onClick={handleCancel} color="danger">
            CANCEL
          </IonButton>
        </div>
      </IonContent>
    </IonPage>
  );
};

export default AddStudentModal;
Enter fullscreen mode Exit fullscreen mode
// App.jsx
import React, { useState } from 'react';
import { IonApp, IonContent, IonPage, IonRouterOutlet } from '@ionic/react';
import { IonReactRouter } from '@ionic/react-router';
import { Route, Link, Redirect } from 'react-router-dom';

/* Core CSS required for Ionic components to work properly */
import '@ionic/react/css/core.css';

/* Basic CSS for apps built with Ionic */
import '@ionic/react/css/normalize.css';
import '@ionic/react/css/structure.css';
import '@ionic/react/css/typography.css';

/* Optional CSS utils that can be commented out */
import '@ionic/react/css/padding.css';
import '@ionic/react/css/float-elements.css';
import '@ionic/react/css/text-alignment.css';
import '@ionic/react/css/text-transformation.css';
import '@ionic/react/css/flex-utils.css';
import '@ionic/react/css/display.css';

import './styles.css';

import Home from './pages/Home';
import Detail from './pages/Detail';

function App() {
  return (
    <IonApp>
      <IonReactRouter>
        <IonRouterOutlet>
          <Route exact path="/home" component={Home} />
          <Route exact path="/detail/:studentId" component={Detail} />

          <Route exact path="/">
            <Redirect to="/home" />
          </Route>
        </IonRouterOutlet>
      </IonReactRouter>
    </IonApp>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode
// index.tsx
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import * as serviceWorkerRegistration from './serviceWorkerRegistration';
import reportWebVitals from './reportWebVitals';

ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  document.getElementById('root')
);

// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://cra.link/PWA
serviceWorkerRegistration.unregister();

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
Enter fullscreen mode Exit fullscreen mode

Video

ko-fi

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .