{"id":24504612,"url":"https://github.com/johnny-quesada-developer/todo-list-with-global-hooks","last_synced_at":"2025-03-15T08:27:20.381Z","repository":{"id":158460441,"uuid":"618481650","full_name":"johnny-quesada-developer/todo-list-with-global-hooks","owner":"johnny-quesada-developer","description":"This is a simple Todo List app built using React Native and Global State Hooks. The app uses a custom implementation of the `react-native-global-state-hooks` package along with `AsyncStorage` for persisting the state.","archived":false,"fork":false,"pushed_at":"2023-12-31T12:13:07.000Z","size":656,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-01-21T23:28:37.061Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/johnny-quesada-developer.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2023-03-24T15:03:47.000Z","updated_at":"2024-01-22T22:16:35.000Z","dependencies_parsed_at":"2023-12-16T02:26:40.379Z","dependency_job_id":"d4125d01-988f-426c-865e-6e49adf9211f","html_url":"https://github.com/johnny-quesada-developer/todo-list-with-global-hooks","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/johnny-quesada-developer%2Ftodo-list-with-global-hooks","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/johnny-quesada-developer%2Ftodo-list-with-global-hooks/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/johnny-quesada-developer%2Ftodo-list-with-global-hooks/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/johnny-quesada-developer%2Ftodo-list-with-global-hooks/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/johnny-quesada-developer","download_url":"https://codeload.github.com/johnny-quesada-developer/todo-list-with-global-hooks/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":243705604,"owners_count":20334382,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2025-01-21T23:24:55.694Z","updated_at":"2025-03-15T08:27:20.349Z","avatar_url":"https://github.com/johnny-quesada-developer.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Todo List with Global State Hooks\n\nThis is a simple Todo List app built using React Native and Global State Hooks. The app uses a custom implementation of the `react-native-global-state-hooks` package along with `AsyncStorage` for persisting the state.\n\n## Installation\n\nClone the repository:\n\n```bash\ngit clone https://github.com/your-username/todo-list-with-global-hooks.git\n```\n\n## Introduction\n\nIn this project, we demonstrate how to use the `react-native-global-state-hooks` package to handle global state in a React Native app. We also show how to extend the `GlobalStore` class to create a persisted store that can be rehydrated between sessions.\n\n## Features\n\n- Add and remove todo items\n- Persists todo items using AsyncStorage\n- Uses Global State Hooks to manage state across components\n\n## Implementation Details\n\n### Creating the `useTodos` Hook\n\nIn the `@src/stores/todos.ts` file, we define a simple Todo interface and create a new instance of the `GlobalStore` class to manage the state of our todos. We then define a new hook called `useTodos` that retrieves the state and setter functions from the global store.\n\n```ts\nimport { GlobalStore } from './GlobalStore';\n\nexport interface Todo {\n  id: string;\n  title: string;\n}\n\nconst store = new GlobalStore\u003cMap\u003cstring, Todo\u003e\u003e(new Map(), {\n  asyncStorageKey: 'todos',\n});\n\nexport const useTodos = store.getHook();\n```\n\n### Using the `useTodos` Hook\n\nIn the `TodoList` component, we use the `useTodos` hook to manage the state of our todos. We add a new todo by creating a new object with a unique id and title and then updating the state with the new object. We remove a todo by deleting it from the state object.\n\n```tsx\nconst TodoList: React.FC = () =\u003e {\n  const [todos, setTodos, { isAsyncStorageReady }] = useTodos(); // State to store the todos\n\n  //...\n  // if isAsyncStorageReady means that the hook already retrieved the information from the asyncStorage\n// ...\n```\n\n### Persisting the State\n\nTo persist the state of our todos between app sessions, we extend the `GlobalStore` class to add support for AsyncStorage. We define a new class called `AsyncGlobalStore` that extends the `GlobalStore` class and adds methods for reading and writing the state to AsyncStorage. We then create a new instance of `AsyncGlobalStore` and pass it to our `useTodos` hook.\n\n```ts\nconst store = new GlobalStore\u003cMap\u003cstring, Todo\u003e\u003e(new Map(), {\n  asyncStorageKey: 'todos', // for persisting the data you just need to add a key for the asyncStorageKey\n});\n```\n\n## Conclusion\n\nUsing Global State Hooks and the `AsyncStorage` API in React Native can simplify state management and make it easier to share state across components. By extending the `GlobalStore` class, we can create custom stores that support advanced features like persistence.\n\n## `[IMPORTANT!]`: From version 6.0.0, you can continue creating your custom implementations or using your previous ones. However, now AsyncStorage is already integrated into the global hooks with @react-native-async-storage/async-storage. You simply need to add a key for the persistent storage, and that will do the trick.\n\n```ts\n// this is all you need fo using async storage\nconst useCountPersisted = createGlobalState(1, {\n  asyncStorage: {\n    key: 'count',\n  },\n});\n\n/**\n * Usage in your components:\n * [NOTE]: If no key is provided, the default metadata is null. Otherwise, it's set to { isAsyncStorageReady: false }.\n *\n * Upon the first successful retrieval from AsyncStorage, components will re-render with { isAsyncStorageReady: true } in the metadata.\n * The metadata and components will be updated and re-rendered even if there's no difference between the value stored in AsyncStorage and the store's default value. This is the only instance where the metadata forces a re-render, after this the metadata will not update the component\n */\nconst [count, setCount, { isAsyncStorageReady }] = useCountPersisted();\n```\n\nNow lets continue analizing how to create a custom GlobalStore\n\n# GlobalStore With AsyncStorage\n\n```ts\nimport { GlobalStore as GlobalStoreBase } from 'react-native-global-state-hooks';\nimport { formatFromStore, formatToStore } from 'json-storage-formatter';\nimport asyncStorage from '@react-native-async-storage/async-storage';\n\nimport {\n  StateSetter,\n  StateConfigCallbackParam,\n  StateChangesParam,\n  ActionCollectionConfig,\n} from 'react-native-global-state-hooks/lib/GlobalStore.types';\n\n/**\n * GlobalStore is an store that could also persist the state in the async storage\n * @template {TState} TState - The state of the store\n * @template {TMetadata} TMetadata - The metadata of the store, it must contain a readonly property called isAsyncStorageReady which cannot be set from outside the store\n * @template {TStateSetter} TStateSetter - The storeActionsConfig of the store\n */\nexport class GlobalStore\u003c\n  TState,\n  // this restriction is needed to avoid the consumers to set the isAsyncStorageReady property from outside the store,\n  // ... even when the value will be ignored is better to avoid it to avoid confusion\n  TMetadata extends { readonly isAsyncStorageReady?: never } \u0026 Record\u003c\n    string,\n    unknown\n  \u003e = {},\n  TStateSetter extends StorageSetter\u003cTState, TMetadata\u003e = StateSetter\u003cTState\u003e\n\u003e extends GlobalStoreBase\u003cTState, StorageMetadata\u003cTMetadata\u003e, TStateSetter\u003e {\n  /**\n   * Config for the async storage\n   * includes the asyncStorageKey and the metadata which will be used to determine if the async storage is ready or not\n   * @template {TState} TState - The state of the store\n   * @template {TMetadata} TMetadata - The metadata of the store\n   * @template {TStateSetter} TStateSetter - The storeActionsConfig of the store\n   **/\n  protected config: StorageConfig\u003cTState, TMetadata, TStateSetter\u003e = {};\n\n  /**\n   * Creates a new instance of the GlobalStore\n   * @param {TState} state - The initial state of the store\n   * @param {GlobalStoreConfig\u003cTState, TMetadata, ActionCollectionConfig\u003cTState, TMetadata\u003e | StateSetter\u003cTState\u003e\u003e \u0026 { asyncStorageKey: string; }} config - The config of the store\n   * @param  {GlobalStoreConfig\u003cTState, TMetadata, ActionCollectionConfig\u003cTState, TMetadata\u003e | StateSetter\u003cTState\u003e\u003e \u0026 { asyncStorageKey: string; }} config.metadata - The metadata of the store which will be used to determine if the async storage is ready or not, also it could store no reactive data\n   * @param  {GlobalStoreConfig\u003cTState, TMetadata, ActionCollectionConfig\u003cTState, TMetadata\u003e | StateSetter\u003cTState\u003e\u003e \u0026 { asyncStorageKey: string; }} config.asyncStorageKey - The key of the async storage\n   * @param  {GlobalStoreConfig\u003cTState, TMetadata, ActionCollectionConfig\u003cTState, TMetadata\u003e | StateSetter\u003cTState\u003e\u003e \u0026 { asyncStorageKey: string; }} config.onInit - The callback that will be called once the store is created\n   * @param  {GlobalStoreConfig\u003cTState, TMetadata, ActionCollectionConfig\u003cTState, TMetadata\u003e | StateSetter\u003cTState\u003e\u003e \u0026 { asyncStorageKey: string; }} config.onStateChange - The callback that will be called once the state is changed\n   * @param  {GlobalStoreConfig\u003cTState, TMetadata, ActionCollectionConfig\u003cTState, TMetadata\u003e | StateSetter\u003cTState\u003e\u003e \u0026 { asyncStorageKey: string; }} config.onSubscribed - The callback that will be called every time a new component is subscribed to the store\n   * @param  {GlobalStoreConfig\u003cTState, TMetadata, ActionCollectionConfig\u003cTState, TMetadata\u003e | StateSetter\u003cTState\u003e\u003e \u0026 { asyncStorageKey: string; }} config.computePreventStateChange - The callback that will be called before the state is changed, if it returns true the state will not be changed\n   * @param {TStateSetter} setterConfig - The actions configuration object (optional) (default: null) if not null the store manipulation will be done through the actions\n   */\n  constructor(\n    state: TState,\n    config: StorageConfig\u003cTState, TMetadata, TStateSetter\u003e | null = null,\n    setterConfig: TStateSetter | null = null\n  ) {\n    const { onInit, asyncStorageKey, ...configParameters } =\n      config ?? ({} as StorageConfig\u003cTState, TMetadata, TStateSetter\u003e);\n\n    super(state, configParameters, setterConfig as TStateSetter);\n\n    // if there is not async storage key this is not a persistent store\n    const isAsyncStorageReady: boolean | null = asyncStorageKey ? false : null;\n\n    this.config = {\n      ...config,\n      metadata: {\n        ...((configParameters.metadata ?? {}) as TMetadata),\n        isAsyncStorageReady,\n      },\n    };\n\n    const hasInitCallbacks = !!(asyncStorageKey || onInit);\n    if (!hasInitCallbacks) return;\n\n    const parameters = this.getConfigCallbackParam({});\n\n    this.onInit(parameters);\n    onInit?.(parameters);\n  }\n\n  /**\n   * This method will be called once the store is created after the constructor,\n   * this method is different from the onInit of the confg property and it won't be overriden\n   */\n  protected onInit = async ({\n    setState,\n    setMetadata,\n    getMetadata,\n  }: StateConfigCallbackParam\u003c\n    TState,\n    StorageMetadata\u003cTMetadata\u003e,\n    NonNullable\u003cTStateSetter\u003e\n  \u003e) =\u003e {\n    const { asyncStorageKey } = this.config;\n    if (!asyncStorageKey) return;\n\n    const storedItem: string = await asyncStorage.getItem(asyncStorageKey);\n\n    setMetadata({\n      ...getMetadata(),\n      isAsyncStorageReady: true,\n    });\n\n    if (storedItem === null) return;\n\n    const jsonParsed = JSON.parse(storedItem);\n    const items = formatFromStore\u003cTState\u003e(jsonParsed);\n\n    setState(items);\n  };\n\n  protected onStateChanged = ({\n    getState,\n  }: StateChangesParam\u003c\n    TState,\n    StorageMetadata\u003cTMetadata\u003e,\n    NonNullable\u003cTStateSetter\u003e\n  \u003e) =\u003e {\n    const { asyncStorageKey } = this.config;\n    if (!asyncStorageKey) return;\n\n    const state = getState();\n    const formattedObject = formatToStore(state, {\n      stringify: true,\n    });\n\n    asyncStorage.setItem(asyncStorageKey, formattedObject);\n  };\n}\n\n/**\n * Metadata of the store\n * @template {TMetadata} TMetadata - The metadata type which also contains the isAsyncStorageReady property\n */\ntype StorageMetadata\u003cTMetadata\u003e = Omit\u003cTMetadata, 'isAsyncStorageReady'\u003e \u0026 {\n  readonly isAsyncStorageReady?: boolean | null;\n};\n\n/**\n * The setter of the store\n * @template {TState} TState - The state of the store\n * @template {TMetadata} TMetadata - The metadata of the store, it must contain a readonly property called isAsyncStorageReady which cannot be set from outside the store\n * */\ntype StorageSetter\u003cTState, TMetadata\u003e =\n  | ActionCollectionConfig\u003cTState, StorageMetadata\u003cTMetadata\u003e\u003e\n  | StateSetter\u003cTState\u003e\n  | null;\n\n/**\n * Config for the async storage\n * includes the asyncStorageKey\n * @template {TState} TState - The state of the store\n * @template {TMetadata} TMetadata - The metadata of the store, it must contain a readonly property called isAsyncStorageReady which cannot be set from outside the store\n * @template {TStateSetter} TStateSetter - The storeActionsConfig of the store\n */\ntype StorageConfig\u003c\n  TState,\n  TMetadata extends { readonly isAsyncStorageReady?: never },\n  TStateSetter extends\n    | ActionCollectionConfig\u003cTState, StorageMetadata\u003cTMetadata\u003e\u003e\n    | StateSetter\u003cTState\u003e\n    | null = StateSetter\u003cTState\u003e\n\u003e = {\n  asyncStorageKey?: string;\n\n  metadata?: TMetadata;\n\n  onInit?: (\n    parameters: StateConfigCallbackParam\u003c\n      TState,\n      StorageMetadata\u003cTMetadata\u003e,\n      NonNullable\u003cTStateSetter\u003e\n    \u003e\n  ) =\u003e void;\n\n  onStateChanged?: (\n    parameters: StateChangesParam\u003c\n      TState,\n      StorageMetadata\u003cTMetadata\u003e,\n      NonNullable\u003cTStateSetter\u003e\n    \u003e\n  ) =\u003e void;\n\n  onSubscribed?: (\n    parameters: StateConfigCallbackParam\u003c\n      TState,\n      StorageMetadata\u003cTMetadata\u003e,\n      NonNullable\u003cTStateSetter\u003e\n    \u003e\n  ) =\u003e void;\n\n  computePreventStateChange?: (\n    parameters: StateChangesParam\u003c\n      TState,\n      StorageMetadata\u003cTMetadata\u003e,\n      NonNullable\u003cTStateSetter\u003e\n    \u003e\n  ) =\u003e boolean;\n};\n```\n\n## License\n\nMIT\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjohnny-quesada-developer%2Ftodo-list-with-global-hooks","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjohnny-quesada-developer%2Ftodo-list-with-global-hooks","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjohnny-quesada-developer%2Ftodo-list-with-global-hooks/lists"}