{"id":20419501,"url":"https://github.com/risingstack/easy-state-hook-examples","last_synced_at":"2025-12-03T23:10:54.609Z","repository":{"id":39024427,"uuid":"253103068","full_name":"RisingStack/easy-state-hook-examples","owner":"RisingStack","description":"Notes for a blogpost, nothing to see here (yet).","archived":false,"fork":false,"pushed_at":"2023-01-05T23:33:26.000Z","size":4264,"stargazers_count":5,"open_issues_count":79,"forks_count":1,"subscribers_count":4,"default_branch":"master","last_synced_at":"2025-01-15T14:17:15.577Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"HTML","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/RisingStack.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}},"created_at":"2020-04-04T21:35:06.000Z","updated_at":"2023-01-31T18:03:08.000Z","dependencies_parsed_at":"2023-02-04T23:46:23.842Z","dependency_job_id":null,"html_url":"https://github.com/RisingStack/easy-state-hook-examples","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/RisingStack%2Feasy-state-hook-examples","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/RisingStack%2Feasy-state-hook-examples/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/RisingStack%2Feasy-state-hook-examples/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/RisingStack%2Feasy-state-hook-examples/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/RisingStack","download_url":"https://codeload.github.com/RisingStack/easy-state-hook-examples/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":241961482,"owners_count":20049473,"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":"2024-11-15T06:37:30.363Z","updated_at":"2025-12-03T23:10:49.538Z","avatar_url":"https://github.com/RisingStack.png","language":"HTML","readme":"# Reinventing hooks with React Easy State\n\nAlthough I use React hooks a lot, I don't really like them. They are solving though problems but with an alien API that's hard to manage at scale.\n\nIt's even harder to wire them together with a library that is based on mutable data. The two concepts don't play well together and forcing them would cause a hot mess. Instead, the React Easy State team at RisingStack is working on alternative patterns that combine the core values of hooks and mutable data.\n\nWe think these core values are:\n\n- encapsulation of pure logic\n- reusability\n- and composability.\n\nAt the same time we are trying to get rid of:\n\n- the strange API\n- reliance on closures to store data\n- and overused patterns.\n\nThis article guides you through these points and how React Easy State tackles them compared to vanilla hooks.\n\n## A basic example\n\nLet's see how the same document title setting application can be written with hooks and with React Easy State.\n\n### Hooks version\n\n```jsx\nimport React, { useState, useCallback, useEffect } from \"react\";\n\nexport default () =\u003e {\n  const [title, setTitle] = useState(\"App title\");\n  const onChange = useCallback(ev =\u003e setTitle(ev.target.value), [setTitle]);\n\n  useEffect(() =\u003e {\n    document.title = title;\n  }, [title]);\n\n  return \u003cinput value={title} onChange={onChange} /\u003e;\n};\n```\n\n\u003e [CodeSandbox demo](https://codesandbox.io/s/github/RisingStack/easy-state-hook-examples/tree/master/hook-app)\n\n### React Easy State version\n\n```jsx\nimport React from \"react\";\nimport { view, store, autoEffect } from \"@risingstack/react-easy-state\";\n\nexport default view(() =\u003e {\n  const title = store({\n    value: \"App title\",\n    onChange: ev =\u003e (title.value = ev.target.value)\n  });\n\n  autoEffect(() =\u003e (document.title = title.value));\n\n  return \u003cinput value={title.value} onChange={title.onChange} /\u003e;\n});\n```\n\n\u003e [CodeSandbox demo](https://codesandbox.io/s/github/RisingStack/easy-state-hook-examples/tree/master/store-app)\n\n`autoEffect` replaces the `useEffect` hook while `store` replaces `useState`, `useCallback`, `useMemo` and others. Under the hood they are built on top of React hooks but they utilize a significantly different API and mindset.\n\n## Reusability\n\nWhat if you really love setting the document's title? Having to repeat the same code every time would disappointing. Luckily hooks were designed to capture reusable logic.\n\n### Hooks version\n\n_useTitle.js_\n\n```js\nimport { useState, useCallback, useEffect } from \"react\";\n\nexport default function useTitle(initalTitle) {\n  const [title, setTitle] = useState(initalTitle);\n  const onChange = useCallback(ev =\u003e setTitle(ev.target.value), [setTitle]);\n\n  useEffect(() =\u003e {\n    document.title = title;\n  }, [title]);\n\n  return [title, onChange];\n}\n```\n\n_App.js_\n\n```jsx\nimport React from \"react\";\nimport useTitle from \"./useTitle\";\n\nexport default () =\u003e {\n  const [title, onChange] = useTitle();\n  return \u003cinput value={title} onChange={onChange} /\u003e;\n};\n```\n\n\u003e [CodeSandbox demo](https://codesandbox.io/s/github/RisingStack/easy-state-hook-examples/tree/master/title-hook)\n\n### React Easy State version\n\nReact Easy State tackles the same problem with **store factories**: a store factory is a function that returns a store. There are no other rules, you can use `store` and `autoEffect` - among other things - inside it.\n\n_titleStore.js_\n\n```js\nimport { store, autoEffect } from \"@risingstack/react-easy-state\";\n\nexport default function titleStore(initalTitle) {\n  const title = store({\n    value: initalTitle,\n    onChange: ev =\u003e (title.value = ev.target.value)\n  });\n\n  autoEffect(() =\u003e (document.title = title.value));\n\n  return title;\n}\n```\n\n_App.js_\n\n```jsx\nimport React from \"react\";\nimport { view } from \"@risingstack/react-easy-state\";\nimport titleStore from \"./titleStore\";\n\nexport default view(() =\u003e {\n  const title = titleStore(\"App title\");\n  return \u003cinput value={title.value} onChange={title.onChange} /\u003e;\n});\n```\n\n\u003e [CodeSandbox demo](https://codesandbox.io/s/github/RisingStack/easy-state-hook-examples/tree/master/title-store)\n\n## Closures and dependency arrays\n\nThings can get messy as complexity grows, especially when async code gets involved. Let's write some reusable data fetching logic, maybe we will need it later (;\n\n### Hooks version\n\n_useFetch.js_\n\n```js\nimport { useState, useCallback } from \"react\";\n\nexport default function useFetch(baseURL) {\n  const [state, setState] = useState({});\n\n  const fetch = useCallback(\n    async path =\u003e {\n      setState({ loading: true });\n      try {\n        const data = await fetchJSON(baseURL + path);\n        setState({ ...state, data, error: undefined });\n      } catch (error) {\n        setState({ ...state, error });\n      } finally {\n        setState(state =\u003e ({ ...state, loading: false }));\n      }\n    },\n    [baseURL, state]\n  );\n\n  return [state, fetch];\n}\n```\n\n_App.js_\n\n```jsx\nimport React from \"react\";\nimport useFetch from \"./useFetch\";\n\nconst POKE_API = \"https://pokeapi.co/api/v2/pokemon/\";\n\nexport default () =\u003e {\n  const [{ data, error, loading }, fetch] = useFetch(POKE_API);\n\n  return (\n    \u003c\u003e\n      \u003cbutton onClick={() =\u003e fetch(\"ditto\")}\u003eFetch pokemon\u003c/button\u003e\n      \u003cdiv\u003e\n        {loading ? \"Loading ...\" : error ? \"Error!\" : JSON.stringify(data)}\n      \u003c/div\u003e\n    \u003c/\u003e\n  );\n};\n```\n\n\u003e [CodeSandbox demo](https://codesandbox.io/s/github/RisingStack/easy-state-hook-examples/tree/master/fetch-hook)\n\nNotice how we have to use a `setState` with an updater function in the `finally` block of `useFetch`. Do you know why does it need special handling?\n\n- If not, try to rewrite it to `setState({ ...state, loading: false })` in the [CodeSandbox demo](https://codesandbox.io/s/github/RisingStack/easy-state-hook-examples/tree/master/fetch-hook) and see what happens. Then read [this article](https://dmitripavlutin.com/react-hooks-stale-closures/) to gain a deeper understanding of hooks and stale closures. Seriously, do these before you go on!\n\n- Otherwise try to think of a good reason why the other `setState`s should be rewritten to use updater functions. (Keep reading for the answer.)\n\n### React Easy State version\n\nProbably you have heard that mutable data is bad (like a 1000 times) over your career. Well... closures are worse.\n\nHooks are heavily relying on closures to store data which leads to issues like the above example. Obviously this is not a bug in the hooks API, but it is a serious cognitive overhead that gets mind-bending as your complexity grows.\n\nReact Easy State is storing its data in mutable objects instead, which has its own quirks but it is way easier to handle in practice. You will always get what you ask for and not some stale data from a long-gone render.\n\n_fetchStore.js_\n\n```js\nimport { store } from \"@risingstack/react-easy-state\";\n\nexport default function fetchStore(baseURL) {\n  const resource = store({\n    async fetch(path) {\n      resource.loading = true;\n      try {\n        resource.data = await fetchJSON(baseURL + path);\n        resource.error = undefined;\n      } catch (error) {\n        resource.error = error;\n      } finally {\n        resource.loading = false;\n      }\n    }\n  });\n\n  return resource;\n}\n```\n\n_App.js_\n\n```jsx\nimport React from \"react\";\nimport { view } from \"@risingstack/react-easy-state\";\nimport fetchStore from \"./fetchStore\";\n\nconst POKE_API = \"https://pokeapi.co/api/v2/pokemon/\";\n\nexport default view(() =\u003e {\n  const { loading, data, error, fetch } = fetchStore(POKE_API);\n\n  return (\n    \u003c\u003e\n      \u003cbutton onClick={() =\u003e fetch(\"ditto\")}\u003eFetch pokemon\u003c/button\u003e\n      \u003cdiv\u003e\n        {loading ? \"Loading ...\" : error ? \"Error!\" : JSON.stringify(data)}\n      \u003c/div\u003e\n    \u003c/\u003e\n  );\n});\n```\n\n\u003e [CodeSandbox demo](https://codesandbox.io/s/github/RisingStack/easy-state-hook-examples/tree/master/fetch-store)\n\n## Composability\n\nWhile we played with fetching data the document title setting application turned into a massive hit with tons of feature requests. Eventually you end up fetching related pokemons from the [free pokeAPI](https://pokeapi.co/). Luckily you already have a data fetching hook, what a coincidence...\n\nYou don't want to refactor your existing code snippets, it would be nicer to **compose** them together into more complex units. The hooks API was designed to handle this.\n\n### Hooks version\n\n_usePokemon.js_\n\n```js\nimport { useEffect } from \"react\";\nimport useTitle from \"./useTitle\";\nimport useFetch from \"./useFetch\";\n\nconst POKE_API = \"https://pokeapi.co/api/v2/pokemon/\";\n\nexport default function usePokemon(initialName) {\n  const [name, onNameChange] = useTitle(initialName);\n  const [data, fetch] = useFetch(POKE_API);\n\n  useEffect(() =\u003e {\n    fetch(name);\n  }, [fetch, name]);\n\n  return { ...data, name, onNameChange };\n}\n```\n\n_App.js_\n\n```jsx\nimport React from \"react\";\nimport usePokemon from \"./usePokemon\";\n\nexport default () =\u003e {\n  const pokemon = usePokemon(\"ditto\");\n\n  return (\n    \u003c\u003e\n      \u003cinput value={pokemon.name} onChange={pokemon.onNameChange} /\u003e\n      \u003cdiv\u003e\n        {pokemon.loading\n          ? \"Loading ...\"\n          : pokemon.error\n          ? \"Error!\"\n          : JSON.stringify(pokemon.data)}\n      \u003c/div\u003e\n    \u003c/\u003e\n  );\n};\n```\n\n\u003e [CodeSandbox demo](https://codesandbox.io/s/github/RisingStack/easy-state-hook-examples/tree/master/poke-hook)\n\nThis example has a serious but hard to grasp flaw - an infinite loop - caused by the long-forgotten `useFetch` hook.\n\n\u003e Otherwise try to think of a good reason why the other `setState`s should be rewritten to use updater functions. (Keep reading for the answer.)\n\u003e\n\u003e -- \u003ccite\u003eMe, a paragraph ago\u003c/cite\u003e\n\nSo you kept reading and it's finally answer time! Let's take a closer look at `useFetch` again.\n\nA _useFetch.js_ part\n\n```js\nconst [state, setState] = useState({});\n\nconst fetch = useCallback(\n  async path =\u003e {\n    setState({ loading: true });\n    try {\n      const data = await fetchJSON(baseURL + path);\n      setState({ ...state, data, error: undefined });\n    } catch (error) {\n      setState({ ...state, error });\n    } finally {\n      setState(state =\u003e ({ ...state, loading: false }));\n    }\n  },\n  [baseURL, state]\n);\n```\n\nThe `fetch` callback uses `state` and has it inside its dependency array. This means that whenever `state` changes `fetch` gets recreated, and whenever `fetch` gets recreated our `useEffect` in `usePokemon` kicks in ...\n\n```js\nuseEffect(() =\u003e {\n  fetch(name);\n}, [fetch, name]);\n```\n\nThat's bad news, we only want to refetch the pokemon when `name` changes. It's time to remove `fetch` from the dependency array.\n\nAnd it breaks again... This time it is not looping but it always fetches the first (stale) pokemon. We keep using an old fetch that is stuck with a stale closure as its data source.\n\nThe correct solution is to modify our `useFetch` hook to use function `setState`s inside the `fetch` callback and remove the `state` dependency from its dependency array.\n\nThis mess is caused by the combination of closures and hook dependency arrays. Let's avoids both of them.\n\n### React Easy State version\n\nReact Easy State takes a different approach for composability. Stores are simple objects which can be combined by nesting them in other objects.\n\n_pokeStore.js_\n\n```js\nimport { store, autoEffect } from \"@risingstack/react-easy-state\";\nimport titleStore from \"./titleStore\";\nimport fetchStore from \"./fetchStore\";\n\nconst POKE_API = \"https://pokeapi.co/api/v2/pokemon/\";\n\nexport default function pokeStore(initialName) {\n  const pokemon = store({\n    name: titleStore(initialName),\n    data: fetchStore(POKE_API)\n  });\n\n  autoEffect(() =\u003e pokemon.data.fetch(pokemon.name.value));\n\n  return pokemon;\n}\n```\n\n_App.js_\n\n```jsx\nimport React from \"react\";\nimport { view } from \"@risingstack/react-easy-state\";\nimport pokeStore from \"./pokeStore\";\n\nexport default view(() =\u003e {\n  const pokemon = pokeStore(\"ditto\");\n\n  return (\n    \u003c\u003e\n      \u003cinput value={pokemon.name.value} onChange={pokemon.name.onChange} /\u003e\n      \u003cdiv\u003e\n        {pokemon.data.loading\n          ? \"Loading ...\"\n          : pokemon.data.error\n          ? \"Error!\"\n          : JSON.stringify(pokemon.data.data)}\n      \u003c/div\u003e\n    \u003c/\u003e\n  );\n});\n```\n\n\u003e [CodeSandbox demo](https://codesandbox.io/s/github/RisingStack/easy-state-hook-examples/tree/master/poke-store)\n\nThe data is stored in - always fresh - mutable objects and hook-like dependency arrays are not required because of the underlying transparent reactivity. Our original `fetchStore` works without any modification.\n\n## Bonus points\n\nReact Easy State is a state management library, not a hook alternative. It provides some features that hooks can not.\n\n### Global state\n\nYou can turn any local state into a global one by moving it outside of component scope. Global state can be shared between components regardless of their relative position to each other.\n\n_pokemon.js_\n\n```js\nimport pokeStore from \"./pokeStore\";\n\n// this global state can be used by any component\nexport default pokeStore(\"ditto\");\n```\n\n_Input.js_\n\n```jsx\nimport React from \"react\";\nimport { view } from \"@risingstack/react-easy-state\";\nimport pokemon from \"./pokemon\";\n\nexport default view(() =\u003e (\n  \u003cinput value={pokemon.name.value} onChange={pokemon.name.onChange} /\u003e\n));\n```\n\n_Display.js_\n\n```jsx\nimport React from \"react\";\nimport { view } from \"@risingstack/react-easy-state\";\nimport pokemon from \"./pokemon\";\n\nexport default view(() =\u003e (\n  \u003cdiv\u003e\n    {pokemon.data.loading\n      ? \"Loading ...\"\n      : pokemon.data.error\n      ? \"Error!\"\n      : JSON.stringify(pokemon.data.data)}\n  \u003c/div\u003e\n));\n```\n\n_App.js_\n\n```jsx\nimport React from \"react\";\nimport { view } from \"@risingstack/react-easy-state\";\nimport Input from \"./Input\";\nimport Display from \"./Display\";\n\nexport default view(() =\u003e (\n  \u003c\u003e\n    \u003cInput /\u003e\n    \u003cDisplay /\u003e\n  \u003c/\u003e\n));\n```\n\n\u003e [CodeSandbox demo](https://codesandbox.io/s/github/RisingStack/easy-state-hook-examples/tree/master/poke-store-global)\n\nAs you can see old-school prop propagation and dependency injection is replaced by simply importing and using the store. How does this affect testability though?\n\n### Testing\n\nHooks encapsulate pure logic but they can not be tested as such. You must wrap them into components and simulate user interactions to access their logic. Ideally this is fine, since you want to test everything - logic and components alike. Practically no one has time to do that, I usually test my logic and leave my components alone. React Easy State store factories return simple objects, which can be tested as such.\n\n_fetchStore.test.js_\n\n```js\nimport fetchStore from \"./fetchStore\";\n\ndescribe(\"fetchStore\", () =\u003e {\n  const TEST_URL = \"https://test.com/\";\n  let fetchMock;\n\n  beforeAll(() =\u003e {\n    fetchMock = jest\n      .spyOn(global, \"fetch\")\n      .mockReturnValue(Promise.resolve({ json: () =\u003e \"Some data\" }));\n  });\n  afterAll(() =\u003e {\n    fetchMock.mockRestore();\n  });\n\n  test(\"should fetch the required resource\", async () =\u003e {\n    const resource = fetchStore(TEST_URL);\n\n    const fetchPromise = resource.fetch(\"resource\");\n    expect(resource.loading).toBe(true);\n    expect(fetchMock).toBeCalledWith(\"https://test.com/resource\");\n    await fetchPromise;\n    expect(resource.loading).toBe(false);\n    expect(resource.data).toBe(\"Some data\");\n  });\n});\n```\n\n\u003e [CodeSandbox demo](https://codesandbox.io/s/github/RisingStack/easy-state-hook-examples/tree/master/fetch-store-testing)\n\n### Class components\n\nWhile hooks are new primitives for function components only, store factories work regardless where they are consumed. This is how you can use our `pokeStore` in a class component.\n\n_App.js_\n\n```jsx\nimport React, { Component } from \"react\";\nimport { view } from \"@risingstack/react-easy-state\";\nimport pokeStore from \"./pokeStore\";\n\nclass App extends Component {\n  pokemon = pokeStore(\"ditto\");\n\n  render() {\n    return (\n      \u003c\u003e\n        \u003cinput\n          value={this.pokemon.name.value}\n          onChange={this.pokemon.name.onChange}\n        /\u003e\n        \u003cdiv\u003e\n          {this.pokemon.data.loading\n            ? \"Loading ...\"\n            : this.pokemon.data.error\n            ? \"Error!\"\n            : JSON.stringify(this.pokemon.data.data)}\n        \u003c/div\u003e\n      \u003c/\u003e\n    );\n  }\n}\n\nexport default view(App);\n```\n\n\u003e [CodeSandbox demo](https://codesandbox.io/s/github/RisingStack/easy-state-hook-examples/tree/master/poke-store-class)\n\n\u003e Using store factories in classes still has a few rough edges regarding `autoEffect` cleanup, we will address these in the coming releases.\n\n## Reality check\n\nThis article defied a lot of trending patterns, like:\n\n- hooks\n- avoiding mutable data\n- traditional dependency injection\n- and full front-end testing.\n\nWhile I think all of the above patterns need a revisit, the provided alternatives are not guaranteed to be 'better'. React Easy State has its own rough edges and we are working hard to soften them in the coming releases. As a starter, keep tuned for our 'Idiomatic React Easy State' docs in the near future. Consider this article as a fun and thought provoking experiment in the meantime.\n\n\u003e The important thing is to not stop questioning. Curiosity has its own reason for existing.\n\u003e\n\u003e -- \u003ccite\u003eAlbert Einstein\u003c/cite\u003e\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frisingstack%2Feasy-state-hook-examples","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Frisingstack%2Feasy-state-hook-examples","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frisingstack%2Feasy-state-hook-examples/lists"}