react-native-global-state-hooks
Version:
This is a package to easily handling global-state across your react-native-components No-redux... The library now includes @react-native-async-storage/async-storage to persist your state across sessions... if you want to keep using the old version without
401 lines (286 loc) β’ 14.9 kB
Markdown
# react-native-global-state-hooks π

Effortless **global state management** for React & React Native! π Define a **global state in just one line of code** and enjoy **lightweight, flexible, and scalable** state management. Try it now on **[CodePen](https://codepen.io/johnnynabetes/pen/WNmeGwb?editors=0010)** and see it in action! β¨
## π Explore More
- **[Live Example](https://johnny-quesada-developer.github.io/global-hooks-example/)** π
- **[Video Overview](https://www.youtube.com/watch?v=1UBqXk2MH8I/)** π₯
- **[react-hooks-global-states](https://www.npmjs.com/package/react-hooks-global-states)** compatible with both `React & React Native`
- **[react-global-state-hooks](https://www.npmjs.com/package/react-global-state-hooks)** specific for web applications (**local-storage integration**).
- **[react-native-global-state-hooks](https://www.npmjs.com/package/react-native-global-state-hooks)** specific for React Native projects (**async-storage integration**).
## π React Hooks Global States - DevTools Extension
React Hooks Global States includes a dedicated, `devTools extension` to streamline your development workflow! Easily visualize, inspect, debug, and modify your application's global state in real-time right within your browser.
### π [Install the DevTools Extension for Chrome](https://chromewebstore.google.com/detail/bafojplmkpejhglhjpibpdhoblickpee/preview?hl=en&authuser=0)
### πΈ DevTools Highlights
| **Track State Changes** | **Modify the State** |
| ------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
|  |  |
| Effortlessly monitor state updates and history. | Instantly edit global states directly from the extension. |
| **Restore the State** | **Custom Actions Granularity** |
| --------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
|  |  |
| Quickly revert your application to a previous state. | Precisely debug specific actions affecting state changes. |
<br>
## ποΈ Async Persist Storage
To persist the global state using **Async Storage**, simply add the `asyncStorage` option:
```ts
const useCount = createGlobalState(0, {
asyncStorage: {
key: "count",
},
});
```
### πΉ How It Works
β
**Automatically syncs the state with Async Storage** if the value is serializable.
β
**Provides an `isAsyncStorageReady` flag** to indicate when the async storage has been reviewed and committed.
β
**Uses `@react-native-async-storage/async-storage` by default** (make sure to install this package if needed).
β
**Allows custom async storage managers** with `asyncStorageWrapper.addAsyncStorageManager(customAsyncStorageManager)`;
Inside your components:
```tsx
const [count, setCount, { isAsyncStorageReady }] = useCount();
```
If you specify a key in `asyncStorage`, the state value persists automatically when serializable. When connecting to async storage, expect a **second render** that updates `isAsyncStorageReady`, indicating that the storage has been reviewed and the state is committed.
### π§ Custom Async Storage Manager
You can configure your own storage selection by using `asyncStorageWrapper.addAsyncStorageManager`. Ensure that the manager is **added before any hook is called**.
`index.ts`
```ts
import { asyncStorageWrapper } from "react-global-state-hooks";
asyncStorageWrapper.addAsyncStorageManager(customAsyncStorageManager);
```
## π Creating a Global State
Define a **global state** in **one line**:
```tsx
import { createGlobalState } from "react-hooks-global-states/createGlobalState";
export const useCount = createGlobalState(0);
```
Now, use it inside a component:
```tsx
const [count, setCount] = useCount();
return <Button onClick={() => setCount((count) => count + 1)}>{count}</Button>;
```
Works just like **useState**, but the **state is shared globally**! π
## π― Selectors: Subscribing to Specific State Changes
For **complex state objects**, you can subscribe to specific properties instead of the entire state:
```tsx
export const useContacts = createGlobalState({ entities: [], selected: new Set<number>() });
```
To access only the `entities` property:
```tsx
const [contacts] = useContacts((state) => state.entities);
return (
<ul>
{contacts.map((contact) => (
<li key={contact.id}>{contact.name}</li>
))}
</ul>
);
```
### π Using Dependencies in Selectors
You can also add **dependencies** to a selector. This is useful when you want to derive state based on another piece of state (e.g., a filtered list). For example, if you're filtering contacts based on a `filter` value:
```tsx
const [contacts] = useContacts(
(state) => state.entities.filter((item) => item.name.includes(filter)),
[filter]
);
```
Alternatively, you can pass dependencies inside an **options object**:
```tsx
const [contacts] = useContacts((state) => state.entities.filter((item) => item.name.includes(filter)), {
dependencies: [filter],
isEqualRoot: (a, b) => a.entities === b.entities,
});
```
Unlike Redux, where only **root state changes trigger re-selection**, this approach ensures that **derived values recompute when dependencies change** while maintaining performance.
## π Reusing Selectors
### π Creating a Selector
```tsx
export const useContactsArray = useContacts.createSelectorHook((state) => state.entities);
export const useContactsCount = useContactsArray.createSelectorHook((entities) => entities.length);
```
### π Using Selectors in Components
```tsx
const [contacts] = useContactsArray();
const [count] = useContactsCount();
```
#### β
Selectors support inline selectors and dependencies
You can still **use dependencies** inside a selector hook:
```tsx
const [filteredContacts] = useContactsArray(
(contacts) => contacts.filter((c) => c.name.includes(filter)),
[filter]
);
```
#### β
Selector hooks share the same state mutator
The **stateMutator remains the same** across all derived selectors, meaning actions and setState functions stay consistent.
```tsx
const [actions1] = useContactsArray();
const [actions2] = useContactsCount();
console.log(actions1 === actions2); // true
```
## π State Actions: Controlling State Modifications
Restrict **state modifications** by defining custom actions:
```tsx
export const useContacts = createGlobalState(
{ filter: "", items: [] },
{
actions: {
async fetch() {
return async ({ setState }) => {
const items = await fetchItems();
setState({ items });
};
},
setFilter(filter: string) {
return ({ setState }) => {
setState((state) => ({ ...state, filter }));
};
},
},
}
);
```
Now, instead of `setState`, the hook returns **actions**:
```tsx
const [filter, { setFilter }] = useContacts();
```
## π Accessing Global State Outside Components
Use `stateControls()` to **retrieve or update state outside React components**:
```tsx
const [contactsRetriever, contactsApi] = useContacts.stateControls();
console.log(contactsRetriever()); // Retrieves the current state
```
#### β
Subscribe to changes
```tsx
const unsubscribe = contactsRetriever((state) => {
console.log("State updated:", state);
});
```
#### β
Subscriptions are great when one state depends on another.
```tsx
const useSelectedContact = createGlobalState(null, {
callbacks: {
onInit: ({ setState, getState }) => {
contactsRetriever(
(state) => state.contacts,
(contacts) => {
if (!contacts.has(getState())) setState(null);
}
);
},
},
});
```
## π Using Context for Scoped State
- **Scoped State** β Context state is **isolated inside the provider**.
- **Same API** β Context supports **selectors, actions, and state controls**.
### π Creating a Context
```tsx
import { createContext } from "react-global-state-hooks/createContext";
export const [useCounterContext, CounterProvider] = createContext(0);
```
Wrap your app:
```tsx
<CounterProvider>
<MyComponent />
</CounterProvider>
```
Use the context state:
```tsx
const [count] = useCounterContext();
```
### π Context Selectors
Works **just like global state**, but within the provider.
## π₯ Observables: Watching State Changes
Observables **let you react to state changes** via subscriptions.
### π Creating an Observable
```tsx
export const useCounter = createGlobalState(0);
export const counterLogs = useCounter.createObservable((count) => `Counter is at ${count}`);
```
### π Subscribing to an Observable
```tsx
const unsubscribe = counterLogs((message) => {
console.log(message);
});
```
### π Using Observables Inside Context
```tsx
export const [useStateControls, useObservableBuilder] = useCounterContext.stateControls();
const createObservable = useObservableBuilder();
useEffect(() => {
const unsubscribe = createObservable((count) => {
console.log(`Updated count: ${count}`);
});
return unsubscribe;
}, []);
```
## βοΈ `createGlobalState` vs. `createContext`
| Feature | `createGlobalState` | `createContext` |
| ---------------------- | ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
| **Scope** | Available globally across the entire app | Scoped to the Provider where itβs used |
| **How to Use** | `const useCount = createGlobalState(0)` | `const [useCountContext, Provider] = createContext(0)` |
| **createSelectorHook** | `useCount.createSelectorHook` | `useCountContext.createSelectorHook` |
| **inline selectors?** | β
Supported | β
Supported |
| **Custom Actions** | β
Supported | β
Supported |
| **Observables** | `useCount.createObservable` | `const [, useObservableBuilder] = useCountContext.stateControls()` |
| **State Controls** | `useCount.stateControls()` | `const [useStateControls] = useCountContext.stateControls()` |
| **Best For** | Global app state (auth, settings, cache) | Scoped module state, reusable component state, or state shared between child components without being fully global |
## π Lifecycle Methods
Global state hooks support lifecycle callbacks for additional control.
```tsx
const useData = createGlobalState(
{ value: 1 },
{
callbacks: {
onInit: ({ setState }) => {
console.log("Store initialized");
},
onStateChanged: ({ state, previousState }) => {
console.log("State changed:", previousState, "β", state);
},
computePreventStateChange: ({ state, previousState }) => {
return state.value === previousState.value;
},
},
}
);
```
Use **`onInit`** for setup, **`onStateChanged`** to listen to updates, and **`computePreventStateChange`** to prevent unnecessary updates.
## Metadata
There is a possibility to add non reactive information in the global state:
```tsx
const useCount = createGlobalState(0, { metadata: { renders: 0 } });
```
How to use it?
```tsx
const [count, , metadata] = useCount();
metadata.renders += 1;
```
## π― Ready to Try It?
π¦ **NPM Package:** [react-hooks-global-states](https://www.npmjs.com/package/react-hooks-global-states)
π Simplify your **global state management** in React & React Native today! π
# Using async persist storage
```ts
const useCount = createGlobalState(0, {
asyncStorage: {
key: "count",
},
});
```
Inside your components
```tsx
const [count, setCount, { isAsyncStorageReady }] = useCount();
```
- if you specify a key into the `asyncStorage` this will persist the state value if the same is serializable
- when connecting to the async storage you can expect a second render which will update isAsyncStorageReady indicating that the async storage was already reviewed and the state value is committed.
The async storage default functionality depends on **@react-native-async-storage/async-storage** but this dependency is optional, install the package as a dependency if you want to enable persisted state.
optionally you can configure your own selection for persisting storage by using `asyncStorageWrapper.addAsyncStorageManager`, notice that the manager should be added before any hook gets call;