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
orstate
in German - definition of zustandglobal 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-plateBlog 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;
- a function to add
addStudent: student =>
set(state => ({
students: [
{
name: student.name,
id: Math.random() * 100 + '',
section: student.section
},
...state.students
]
})),
- 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;
}
})
}))
- a function to delete
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 thestore
we can access it directly
const students = useStudentStore(state => state.students);
- a way to get an individual student, since
students
are on thestore
we can access thestudents
directly and filter based on id. We can also cache ormemoize
the value with react'suseCallback
hook and reuse the result as long as thestudent
id doesn't change
const student = useStudentStore(
useCallback(state => state.students.find(s => s.id === studentId), [
studentId
])
);
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);
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
install zustand - https://zustand.surge.sh/
npm install zustand
// 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;
// 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);
// 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
// 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;
// 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;
// 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