Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use jotai to store global preferences #1823

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 73 additions & 0 deletions docs/adr/adr-global-state-management.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# ADR: Use Jotai for Global State Management

Trello-card: [Link to Trello card if relevant]

Decision Deadline: [Add Date]

Discussion Participants: [Add list of discussion participants and list their Github profile or email]

## Status

Proposed

## Context or Problem Statement

We need a global state management solution for our React application. Currently, we are using React Context for this purpose. However, we have encountered some issues with ease of use and rendering performance. We are considering switching to Jotai as an alternative.

## Decision Drivers

1. **Ease of use:** How simple is it to implement and maintain the state management solution?
2. **Rendering performance:** How well does the solution minimize unnecessary re-renders?
3. **Scalability:** How well does the solution handle increasing complexity and state size?
4. **Community and support:** How active and supportive is the community around the solution?

## Considered Options

1. Continue using React Context
2. Switch to Jotai

## Decision

We will switch to Jotai for global state management.

## Consequences

### Advantages of Jotai

1. **Ease of use:** Jotai provides a simpler and more intuitive API for managing state compared to React Context. It allows for more granular control over state updates, making it easier to manage complex state logic.
2. **Rendering performance:** Jotai minimizes unnecessary re-renders by only updating components that depend on the specific piece of state that has changed. This leads to better performance compared to React Context, which can cause entire component trees to re-render when the context value changes.
3. **Scalability:** Jotai's atom-based approach allows for better scalability as the state grows in size and complexity. Each atom represents a single piece of state, making it easier to manage and reason about the state.
4. **Community and support:** Jotai has an active and growing community, with good documentation and support. It is also backed by the same team that maintains Zustand, another popular state management library.

### Drawbacks of Jotai

1. **Learning curve:** While Jotai is simpler to use than React Context, it still requires developers to learn a new API and mental model for managing state.
2. **Ecosystem:** React Context is a built-in feature of React, while Jotai is an external library. This means that Jotai may not have the same level of ecosystem integration and support as React Context.

## Comparison with React Context

### Ease of use

- **React Context:** Requires creating context providers and consumers, which can lead to boilerplate code and complexity in managing state updates.
- **Jotai:** Provides a simpler API with atoms and hooks, reducing boilerplate and making it easier to manage state updates.

### Rendering performance

- **React Context:** Can cause entire component trees to re-render when the context value changes, leading to potential performance issues.
- **Jotai:** Minimizes unnecessary re-renders by only updating components that depend on the specific piece of state that has changed.

### Scalability

- **React Context:** Can become difficult to manage as the state grows in size and complexity, leading to potential performance and maintainability issues.
- **Jotai:** Atom-based approach allows for better scalability, making it easier to manage and reason about the state.

### Community and support

- **React Context:** Built-in feature of React with a large community and ecosystem support.
- **Jotai:** Active and growing community with good documentation and support, but may not have the same level of ecosystem integration as React Context.

## References

- [Jotai Documentation](https://jotai.org/docs/introduction)
- [React Context Documentation](https://reactjs.org/docs/context.html)
- [Comparison of State Management Libraries](https://blog.logrocket.com/comparing-state-management-tools-react/)
1 change: 1 addition & 0 deletions front/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"@zxing/browser": "^0.1.5",
"@zxing/library": "^0.21.3",
"chakra-react-select": "4.9.2",
"jotai": "^2.10.3",
"react-big-calendar": "^1.17.0",
"react-icons": "^5.4.0",
"react-table": "^7.8.0",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useCallback, useEffect, useMemo, useState } from "react";
import { useMutation, useQuery, useReactiveVar } from "@apollo/client";
import { useAtomValue } from "jotai";
import { boxReconciliationOverlayVar } from "queries/cache";
import { SHIPMENT_BY_ID_WITH_PRODUCTS_AND_LOCATIONS_QUERY } from "queries/queries";
import { UPDATE_SHIPMENT_WHEN_RECEIVING } from "queries/mutations";
Expand All @@ -14,7 +15,7 @@ import {
ILocationData,
IProductWithSizeRangeData,
} from "./components/BoxReconciliationView";
import { useBaseIdParam } from "hooks/useBaseIdParam";
import { selectedBaseIdAtom } from "stores/globalPreferenceStore";

export interface IBoxReconciliationOverlayData {
shipmentDetail: ShipmentDetail;
Expand All @@ -31,7 +32,7 @@ export function BoxReconciliationOverlay({
}) {
const { createToast } = useNotification();
const { triggerError } = useErrorHandling();
const { baseId } = useBaseIdParam();
const baseId = useAtomValue(selectedBaseIdAtom);
const boxReconciliationOverlayState = useReactiveVar(boxReconciliationOverlayVar);
const [boxUndeliveredAYSState, setBoxUndeliveredAYSState] = useState<string>("");
const navigate = useNavigate();
Expand Down
11 changes: 6 additions & 5 deletions front/src/components/BreadcrumbNavigation.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { useContext } from "react";
import { GlobalPreferencesContext } from "providers/GlobalPreferencesProvider";
import { Breadcrumb, BreadcrumbItem, BreadcrumbLink, Button } from "@chakra-ui/react";
import { ChevronLeftIcon, ChevronRightIcon } from "@chakra-ui/icons";
import { Link } from "react-router-dom";
import { useLoadAndSetGlobalPreferences } from "hooks/useLoadAndSetGlobalPreferences";
import { BreadcrumbNavigationSkeleton } from "./Skeletons";
import { useAtomValue } from "jotai";
import { organisationAtom, selectedBaseAtom } from "stores/globalPreferenceStore";

interface IBreadcrumbItemData {
label: string;
Expand Down Expand Up @@ -35,9 +35,10 @@ export function MobileBreadcrumbButton({ label, linkPath }: IBreadcrumbItemData)
}

export function BreadcrumbNavigation({ items }: IBreadcrumbNavigationProps) {
const { globalPreferences } = useContext(GlobalPreferencesContext);
const orgName = globalPreferences.organisation?.name;
const baseName = globalPreferences.selectedBase?.name;
const organisation = useAtomValue(organisationAtom);
const selectedBase = useAtomValue(selectedBaseAtom);
const orgName = organisation?.name;
const baseName = selectedBase?.name;
const { isLoading: isGlobalStateLoading } = useLoadAndSetGlobalPreferences();

if (isGlobalStateLoading) return <BreadcrumbNavigationSkeleton />;
Expand Down
17 changes: 7 additions & 10 deletions front/src/components/HeaderMenu/BaseSwitcher.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,27 +11,24 @@ import {
RadioGroup,
Stack,
} from "@chakra-ui/react";
import { useContext, useEffect, useState } from "react";
import { useEffect, useState } from "react";
import { useNavigate, useLocation } from "react-router-dom";

import { GlobalPreferencesContext } from "providers/GlobalPreferencesProvider";
import { useBaseIdParam } from "hooks/useBaseIdParam";
import { useAtomValue } from "jotai";
import { availableBasesAtom, selectedBaseIdAtom } from "stores/globalPreferenceStore";

function BaseSwitcher({ isOpen, onClose }: { isOpen: boolean; onClose: () => void }) {
const navigate = useNavigate();
const { pathname } = useLocation();
const { baseId: currentBaseId } = useBaseIdParam();
const { globalPreferences } = useContext(GlobalPreferencesContext);
const currentOrganisationBases = globalPreferences.availableBases?.filter(
(base) => base.id !== currentBaseId,
);
const baseId = useAtomValue(selectedBaseIdAtom);
const availableBases = useAtomValue(availableBasesAtom);
const currentOrganisationBases = availableBases?.filter((base) => base.id !== baseId);
const firstAvailableBaseId = currentOrganisationBases?.find((base) => base)?.id;
const [value, setValue] = useState(firstAvailableBaseId);

// Need to set this as soon as we have this value available to set the default radio selection.
useEffect(() => {
setValue(firstAvailableBaseId);
}, [firstAvailableBaseId, currentBaseId]);
}, [firstAvailableBaseId, baseId]);

const switchBase = () => {
const currentPath = pathname.split("/bases/")[1].substring(1);
Expand Down
7 changes: 4 additions & 3 deletions front/src/components/HeaderMenu/HeaderMenuContainer.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import { useMemo } from "react";
import { useAtomValue } from "jotai";
import { useReactiveVar } from "@apollo/client";
import QrReaderOverlay from "components/QrReaderOverlay/QrReaderOverlay";
import { qrReaderOverlayVar } from "queries/cache";
import { useReactiveVar } from "@apollo/client";
import { useAuthorization } from "hooks/useAuthorization";
import HeaderMenu, { MenuItemsGroupData } from "./HeaderMenu";
import { useBaseIdParam } from "hooks/useBaseIdParam";
import { selectedBaseIdAtom } from "stores/globalPreferenceStore";

function HeaderMenuContainer() {
const authorize = useAuthorization();
const { baseId } = useBaseIdParam();
const baseId = useAtomValue(selectedBaseIdAtom);
const qrReaderOverlayState = useReactiveVar(qrReaderOverlayVar);

// TODO: do this at route definition
Expand Down
19 changes: 11 additions & 8 deletions front/src/components/HeaderMenu/MenuDesktop.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,24 +11,27 @@ import { NavLink } from "react-router-dom";

import { ACCOUNT_SETTINGS_URL } from "./consts";
import { useHandleLogout } from "hooks/hooks";
import { GlobalPreferencesContext } from "providers/GlobalPreferencesProvider";
import BoxtributeLogo from "./BoxtributeLogo";
import { IHeaderMenuProps } from "./HeaderMenu";
import MenuIcon, { Icon } from "./MenuIcons";
import { expandedMenuIndex } from "./expandedMenuIndex";
import { useContext } from "react";
import BaseSwitcher from "./BaseSwitcher";
import { useBaseIdParam } from "hooks/useBaseIdParam";
import { useAtomValue } from "jotai";
import {
availableBasesAtom,
selectedBaseAtom,
selectedBaseIdAtom,
} from "stores/globalPreferenceStore";

function MenuDesktop({ menuItemsGroups }: IHeaderMenuProps) {
const { isOpen, onOpen, onClose } = useDisclosure();
const { handleLogout } = useHandleLogout();
const { baseId: currentBaseId } = useBaseIdParam();
const { globalPreferences } = useContext(GlobalPreferencesContext);
const baseName = globalPreferences.selectedBase?.name;
const baseId = useAtomValue(selectedBaseIdAtom);
const selectedBase = useAtomValue(selectedBaseAtom);
const availableBases = useAtomValue(availableBasesAtom);
const baseName = selectedBase?.name;
const currentOrganisationHasMoreThanOneBaseAvailable =
(globalPreferences.availableBases?.filter((base) => base.id !== currentBaseId).length || 0) >=
1;
(availableBases?.filter((base) => base.id !== baseId).length || 0) >= 1;
const [allowMultipleAccordionsOpen] = useMediaQuery("(min-height: 1080px)");

return (
Expand Down
20 changes: 12 additions & 8 deletions front/src/components/HeaderMenu/MenuMobile.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ReactNode, useContext } from "react";
import { ReactNode } from "react";
import {
Accordion,
AccordionButton,
Expand All @@ -18,15 +18,19 @@ import {
import { HamburgerIcon } from "@chakra-ui/icons";
import { useHandleLogout } from "hooks/hooks";
import { NavLink } from "react-router-dom";
import { useAtomValue } from "jotai";

import { ACCOUNT_SETTINGS_URL } from "./consts";
import { IHeaderMenuProps } from "./HeaderMenu";
import BoxtributeLogo from "./BoxtributeLogo";
import MenuIcon, { Icon } from "./MenuIcons";
import { expandedMenuIndex } from "./expandedMenuIndex";
import { GlobalPreferencesContext } from "providers/GlobalPreferencesProvider";
import BaseSwitcher from "./BaseSwitcher";
import { useBaseIdParam } from "hooks/useBaseIdParam";
import {
selectedBaseAtom,
availableBasesAtom,
selectedBaseIdAtom,
} from "stores/globalPreferenceStore";

function SubItemBox({ children, py = 1 }: { children: ReactNode | ReactNode[]; py?: number }) {
return (
Expand All @@ -48,12 +52,12 @@ function SubItemBox({ children, py = 1 }: { children: ReactNode | ReactNode[]; p
function MenuMobile({ onClickScanQrCode, menuItemsGroups }: IHeaderMenuProps) {
const { isOpen, onOpen, onClose } = useDisclosure();
const { handleLogout } = useHandleLogout();
const { baseId: currentBaseId } = useBaseIdParam();
const { globalPreferences } = useContext(GlobalPreferencesContext);
const baseName = globalPreferences.selectedBase?.name;
const baseId = useAtomValue(selectedBaseIdAtom);
const selectedBase = useAtomValue(selectedBaseAtom);
const availableBases = useAtomValue(availableBasesAtom);
const baseName = selectedBase?.name;
const currentOrganisationHasMoreThanOneBaseAvailable =
(globalPreferences.availableBases?.filter((base) => base.id !== currentBaseId).length || 0) >=
1;
(availableBases?.filter((base) => base.id !== baseId).length || 0) >= 1;

return (
<Flex as="nav" py={4} zIndex="3">
Expand Down
5 changes: 3 additions & 2 deletions front/src/components/QrReader/QrReaderContainer.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useCallback, useState, useEffect } from "react";
import { useNavigate } from "react-router-dom";
import { useAtomValue } from "jotai";
import { useErrorHandling } from "hooks/useErrorHandling";
import {
ILabelIdentifierResolvedValue,
Expand All @@ -12,9 +13,9 @@ import { useReactiveVar } from "@apollo/client";
import { qrReaderOverlayVar } from "queries/cache";
import { AlertWithoutAction } from "components/Alerts";
import QrReader from "./components/QrReader";
import { useBaseIdParam } from "hooks/useBaseIdParam";
import { QrReaderSkeleton } from "components/Skeletons";
import { Alert, CloseButton, useDisclosure } from "@chakra-ui/react";
import { selectedBaseIdAtom } from "stores/globalPreferenceStore";

interface IQrReaderContainerProps {
onSuccess: () => void;
Expand All @@ -32,7 +33,7 @@ const IOS_PSA_TEXT = (
);

function QrReaderContainer({ onSuccess }: IQrReaderContainerProps) {
const { baseId } = useBaseIdParam();
const baseId = useAtomValue(selectedBaseIdAtom);
const navigate = useNavigate();
const { triggerError } = useErrorHandling();
const { resolveQrCode } = useQrResolver();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useCallback, useEffect, useMemo, useState } from "react";
import { useQuery } from "@apollo/client";
import { useAtomValue } from "jotai";
import { GET_SCANNED_BOXES } from "queries/local-only";
import { MULTI_BOX_ACTION_OPTIONS_FOR_LOCATIONS_TAGS_AND_SHIPMENTS_QUERY } from "queries/queries";
import { IDropdownOption } from "components/Form/SelectField";
Expand All @@ -12,17 +13,17 @@ import { useMoveBoxes } from "hooks/useMoveBoxes";
import { useAssignTags } from "hooks/useAssignTags";
import { useAssignBoxesToShipment } from "hooks/useAssignBoxesToShipment";
import { locationToDropdownOptionTransformer } from "utils/transformers";
import { useBaseIdParam } from "hooks/useBaseIdParam";
import QrReaderMultiBox, { IMultiBoxAction } from "./QrReaderMultiBox";
import {
FailedBoxesFromAssignTagsAlert,
FailedBoxesFromAssingToShipmentAlert,
FailedBoxesFromMoveBoxesAlert,
NotInStockAlertText,
} from "./AlertTexts";
import { selectedBaseIdAtom } from "stores/globalPreferenceStore";

function QrReaderMultiBoxContainer() {
const { baseId } = useBaseIdParam();
const baseId = useAtomValue(selectedBaseIdAtom);

// selected radio button
const [multiBoxAction, setMultiBoxAction] = useState<IMultiBoxAction>(IMultiBoxAction.moveBox);
Expand Down
7 changes: 4 additions & 3 deletions front/src/hooks/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import { UseToastOptions, ToastPosition } from "@chakra-ui/react";
import { tableConfigsVar } from "queries/cache";
import { useReactiveVar } from "@apollo/client";
import { Filters, SortingRule } from "react-table";
import { useBaseIdParam } from "./useBaseIdParam";
import { useAtomValue } from "jotai";
import { selectedBaseIdAtom } from "stores/globalPreferenceStore";

export interface INotificationProps extends UseToastOptions {
title?: string;
Expand All @@ -31,7 +32,7 @@ export const useHandleLogout = () => {
};

export const useGetUrlForResourceHelpers = () => {
const { baseId } = useBaseIdParam();
const baseId = useAtomValue(selectedBaseIdAtom);
if (baseId == null) {
throw new Error("Could not extract baseId from URL");
}
Expand Down Expand Up @@ -66,7 +67,7 @@ export const useToggle = (initialValue = false) => {

// TODO: Probably need to refactor to remove this, seems unnecessary.
export const useGlobalSiteState = () => {
const { baseId: currentBaseId } = useBaseIdParam();
const currentBaseId = useAtomValue(selectedBaseIdAtom);
const navigate = useNavigate();

return {
Expand Down
11 changes: 0 additions & 11 deletions front/src/hooks/useBaseIdParam.ts

This file was deleted.

Loading
Loading