Managing React State with Zustand
Tags: zustand, react, statemanagement, javascript
Published: 2021-07-13 06:24:52

## Managing React State with Zustand
![Managing React State with Zustand](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/onuv5cnxk5mj99gs9nsn.png)

> 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/](https://zustand.surge.sh/) - Means `condition` or `state` in German - [definition of zustand](https://dictionary.cambridge.org/dictionary/german-english/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](https://daveceddia.com/react-state-management/)

- 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

```jsx
// 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;
```

- a function to add

```jsx
  addStudent: student =>
    set(state => ({
      students: [
        {
          name: student.name,
          id: Math.random() * 100 + '',
          section: student.section
        },
        ...state.students
      ]
    })),
```

- a function to update

```jsx
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;
        }
      })
    }))
```

- a function to delete

```jsx
  removeStudent: id =>
    set(state => ({
      students: state.students.filter(student => student.id !== id)
    })),
```

- a way to get all of the students, since the `students` are a property on the `store` we can access it directly

```jsx
 const students = useStudentStore(state => state.students);
```

- 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

```jsx
  const student = useStudentStore(
    useCallback(state => state.students.find(s => s.id === studentId), [
      studentId
    ])
  );
```

### Accessing The Store

```jsx
// 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);
```

### Full App Example

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

{% stackblitz react-managing-state-with-zustand %}

create a blank app with ionic

```jsx
ionic start --type=react
```

install zustand - [https://zustand.surge.sh/](https://zustand.surge.sh/)

```jsx
npm install zustand
```

```jsx
// 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;
```

```jsx
// 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);
```

```jsx
// 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);
```



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

```jsx
// 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;
```

```jsx
// 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;
```

```jsx
// 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();
```

### Video
{% youtube CmXEMy2qK8w %}

[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/W7W31U7HM)