{"id":19988852,"url":"https://github.com/limscoder/react-wrangler","last_synced_at":"2025-05-04T08:32:06.778Z","repository":{"id":57347839,"uuid":"76788124","full_name":"limscoder/react-wrangler","owner":"limscoder","description":"A react component for simple declarative state management with \"one way data flow\" and side effects","archived":false,"fork":false,"pushed_at":"2017-02-10T16:31:14.000Z","size":333,"stargazers_count":16,"open_issues_count":0,"forks_count":1,"subscribers_count":3,"default_branch":"master","last_synced_at":"2025-04-20T23:46:25.367Z","etag":null,"topics":["data-flow","persistent-data","react","redux","rest"],"latest_commit_sha":null,"homepage":"","language":"JavaScript","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/limscoder.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":"2016-12-18T14:35:59.000Z","updated_at":"2020-07-04T23:36:41.000Z","dependencies_parsed_at":"2022-08-28T03:01:40.420Z","dependency_job_id":null,"html_url":"https://github.com/limscoder/react-wrangler","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/limscoder%2Freact-wrangler","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/limscoder%2Freact-wrangler/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/limscoder%2Freact-wrangler/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/limscoder%2Freact-wrangler/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/limscoder","download_url":"https://codeload.github.com/limscoder/react-wrangler/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":252308244,"owners_count":21727155,"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":["data-flow","persistent-data","react","redux","rest"],"created_at":"2024-11-13T04:44:22.842Z","updated_at":"2025-05-04T08:32:05.480Z","avatar_url":"https://github.com/limscoder.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# react-wrangler\n\n`react-wrangler` simplifies state management by using a declarative component\nto implement \"one way data flow\" and side effects.\n\nIt's composable components all the way down:\nno actions, reducers, selectors, generators, middleware, thunks, sagas, query fragments, or observeables required.\n\n## Why?\n\nOne of the first things I learned when beginning react is that `props` serve as the public API for components.\nIt's a great concept because eliminating imperative function calls from the API makes components declarative and easily composable.\nAn entire react application is composed by defining a tree of react-elements and using event handlers to trigger state changes in response to user actions.\nThe `render-\u003euser action-\u003eupdate state-\u003erender` cycle is commonly referred to as \"one way data flow\".\n\nThis should be a very elegant solution to application development, but many state management frameworks force react developers to\nstruggle with imperative API calls invoked outside of the react-element render tree and allow side effects to be triggered from\nanywhere in the application, making it difficult to reason about when and why side effects are triggered.\n\nDoes \"one way data flow\" need to be this complicated?\n`react-wrangler` is an attempt to get back to basics and provide \"one way data flow\" with a simple, component based API.\n\n## Example\n\nTake a look at the [example counter application](https://limscoder.github.io/react-wrangler/pages/examples/counter/index.html) and the\n[example application code](./examples/counter/App.js) for an idea of how `react-wrangler` works.\n\n## Development\n\n### Install\n\n`react-wrangler` requires peer dependencies `immutable` and `react`.\n\n```bash\n\u003e npm i immutable\n\u003e npm i react\n\u003e npm i react-wrangler\n```\n\n### Configure\n\nThe `Wrangle` component provides a single place to store your state, represented as an [immutable Map](https://facebook.github.io/immutable-js/docs/#/Map).\nPass the `initialState` prop to the `Wrangle` component to define the application's starting state.\nIt uses `context` to expose data to descendant components, so it should only be rendered once at the root of an application.\n\n```javascript\nimport { fromJs } from 'immutable';\n\nconst initialState = fromJs({\n  user: {\n    id: '137af8eb-6baf-42a8-a38a-3df6fc36d562',\n    display: 'username999',\n    email: 'username999@wrangler.com',\n    preferences: {\n      showFirstTimeHelp: true\n    }\n  },\n});\n\nReactDOM.render(\n  \u003cWrangle initialState={ initialState }\u003e\n    \u003cRootComponent /\u003e\n  \u003c/Wrangle\u003e\n);\n```\n\n### Paths \n\n`paths` are strings used to reference nodes within `react-wrangler` state.\n\nUsing `initialState` from above:\n\n```javascript\n// path === 'user.id'\n\u003e '137af8eb-6baf-42a8-a38a-3df6fc36d562'\n\n// path === 'user.preferences'\n\u003e Map({\n  showFirstTimeHelp: true\n})\n\n// path === 'user.preferences.showFirstTimeHelp'\n\u003e true\n\n// paths that are not present in state are undefined\n// path === 'user.missing'\n\u003e undefined\n```\n\n### Path component\n\nUse the `Path` component to retrieve `path` values from state and map them to `props`:\n\n```javascript\nimport { Path } from 'react-wrangler';\n\nfunction Welcome(props) {\n  const { displayName, showFirstTimeHelp, onDismiss } = props;\n\n  const firstTimeHelp = showFirstTimeHelp ?\n    \u003cFirstTimeHelp onDismiss={ onDismiss } /\u003e : null;\n\n  return (\n    \u003cdiv\u003e\n      \u003cspan\u003eWelcome { displayName }\u003c/span\u003e\n      { firstTimeHelp }\n    \u003c/div\u003e);\n}\n\nfunction WrangledWelcome(props) {\n  // map path values to props\n  //\n  // key === prop to map path value to\n  // value === path\n  const mapPathsToProps = {\n    displayName: 'user.displayName',\n    showFirstTimeHelp: 'user.preferences.showFirstTimeHelp'\n  }\n\n  // the `component` prop contains the component to\n  // be rendered with paths mapped to props.\n  return \u003cPath component={ Welcome } mapPathsToProps={ mapPathsToProps } /\u003e;\n}\n```\n\n### Mutation\n\nAn application isn't very useful if it can't mutate it's state.\nHere is the example from above with state mutation in response to a user action:\n\n```javascript\nfunction onDismiss(store, ...args) {\n  store.setPath('user.perferences.showFirstTimeHelp', false);\n}\n\nfunction WrangledWelcome(props) {\n  // map callbacks to props\n  //\n  // key === prop to map callback to\n  // value === callback function\n  const mapCallbacksToProps = { onDismiss };\n  const mapPathsToProps = { ... };\n\n  return \u003cPath component={ Welcome }\n               mapPathsToProps={ mapPathsToProps }\n               mapCallbacksToProps={ mapCallbacksToProps } /\u003e;\n}\n```\n\n### Side effects\n\n`react-wrangler` simplifies application logic by isolating all side effects into two callbacks.\n`Wrangle.onMissingPaths` and `Wrangle.onStoreChange` are the only 2 functions that should invoke side effects within a `react-wrangler` application.\n\n#### Fetching data\n\nUse `Wrangle.onMissingPaths` to fetch data.\nIt's up to the implementer to determine how the missing path values are fetched:\n [Falcor](https://netflix.github.io/falcor/),\n [Graphql](http://graphql.org/),\n [Rest](https://en.wikipedia.org/wiki/Representational_state_transfer),\n [LocalStorage](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage),\n [AsyncStorage](https://facebook.github.io/react-native/docs/asyncstorage.html),\n etc.\n\n`onMissingPaths` is called once-per-frame with an array of de-duplicated paths that have been requested, but are not present in state.\n\n```javascript\nfunction onMissingPaths(store, missingPaths) {\n  fetchPathsFromServer(missingPaths).then((pathValues) =\u003e {\n    // call setPaths to update state and\n    // re-render the view after pathValues are fetched.\n    store.setPaths(pathValues);\n  });\n}\n\nReactDOM.render(\n  \u003cWrangle onMissingPaths={ onMissingPaths }\u003e\n    \u003cRootComponent /\u003e\n  \u003c/Wrangle\u003e\n);\n```\n\n#### Persisting data\n\nUse `Wrangle.onStoreChange` to persist data.\n`onStoreChange` is called whenever `path` values are changed.\nIt's up to the implementer to determine how the path values are persisted,\nsuch as `LocalStorage` or an API call that makes a network request.\n\n```javascript\nfunction onStoreChange(store, changedPaths) {\n  updatePathsOnServer(changedPaths)\n    .then(() =\u003e {\n      store.setPath('showNotification': 'saved successfully');\n    })\n    .catch(() =\u003e {\n      store.setPath('showNotification': 'failed to save');\n    });\n}\n\nReactDOM.render(\n  \u003cWrangle onStoreChange={ onStoreChange }\u003e\n    \u003cRootComponent /\u003e\n  \u003c/Wrangle\u003e\n);\n```\n\n### Debugging\n\nSet the `debug` prop to enable debugging messages in the console.\n\n```javascript \n\u003cWrangle debug={true} /\u003e\n```\n\nDebugging messages include information about each state update.\n\n```\n\u003e store changed (5): counter.current\n \u003e elapsed time: 0.08500000000094587ms\n \u003e changed paths: {counter.current: 92}\n \u003e new state: {counter: {current: 92}}\n \u003e type 'resetState(5)' in console to return to this state\n```\n\nUse the `resetState` function in the console to retreive any previous state for \"time travel\" debugging.\nUse the `setPath` function in the console to manually update state\n(object and array values are automatically converted to Immutable data structures).\n\n### Optimization\n\n#### Use immutable data structures\n\n`path` values should *always* be immutable so `Path.shouldComponentUpdate` can avoid unnessecary vdom re-renders.\n\n```javascript\n// don't set plain JS objects or arrays into state\nsetPath('user.preferences', { showFirstTimeHelp: true });\n\n// use Immutable data structures instead\nsetPath('user.preferences', Immutable.fromJS({ showFirstTimeHelp: true }));\n```\n\n#### Batch calls to `setPath`\n\nInvoking `setPath` triggers all `\u003cPath /\u003e` components to check their state and possibly re-render, so it should be called as infrequently as possible.\nIf multiple paths need to be set at the same time, it's more efficient to invoke `setPaths` once instead of calling `setPath` multiple times:\n\n```javascript\n// set multiple paths in one call\nsetPaths({\n  'user.preferences.showFirstTimeHelp': false,\n  'user.displayName': 'Kathy'\n});\n```\n\n#### Avoid inline closures for callbacks\n\nDynamically defined functions in `mapCallbacksToProps` will cause unnessecary vdom re-renders.\nUse statically defined functions whenever possible.\n\n#### Batch requests in `onMissingPaths`\n\nIt's recommended to batch network requests triggered from within `onMissingPaths`\ninto the fewest number of requests possible.\n\nIt's also important to note that `onMissingPaths` can be called multiple times with the same paths.\nEverytime the component re-renders, `onMissingPaths` will be called with all missing paths.\nImplementers must be careful to avoid duplicate network requests by keeping track of in-flight requests.","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flimscoder%2Freact-wrangler","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Flimscoder%2Freact-wrangler","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flimscoder%2Freact-wrangler/lists"}