{"action":"created","issue":{"url":"https://api.github.com/repos/gaearon/redux/issues/113","labels_url":"https://api.github.com/repos/gaearon/redux/issues/113/labels{/name}","comments_url":"https://api.github.com/repos/gaearon/redux/issues/113/comments","events_url":"https://api.github.com/repos/gaearon/redux/issues/113/events","html_url":"https://github.com/gaearon/redux/issues/113","id":88862407,"number":113,"title":"Time travel","user":{"login":"gaearon","id":810438,"avatar_url":"https://avatars.githubusercontent.com/u/810438?v=3","gravatar_id":"","url":"https://api.github.com/users/gaearon","html_url":"https://github.com/gaearon","followers_url":"https://api.github.com/users/gaearon/followers","following_url":"https://api.github.com/users/gaearon/following{/other_user}","gists_url":"https://api.github.com/users/gaearon/gists{/gist_id}","starred_url":"https://api.github.com/users/gaearon/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/gaearon/subscriptions","organizations_url":"https://api.github.com/users/gaearon/orgs","repos_url":"https://api.github.com/users/gaearon/repos","events_url":"https://api.github.com/users/gaearon/events{/privacy}","received_events_url":"https://api.github.com/users/gaearon/received_events","type":"User","site_admin":false},"labels":[{"url":"https://api.github.com/repos/gaearon/redux/labels/discussion","name":"discussion","color":"fbca04"},{"url":"https://api.github.com/repos/gaearon/redux/labels/enhancement","name":"enhancement","color":"84b6eb"}],"state":"open","locked":false,"assignee":{"login":"gaearon","id":810438,"avatar_url":"https://avatars.githubusercontent.com/u/810438?v=3","gravatar_id":"","url":"https://api.github.com/users/gaearon","html_url":"https://github.com/gaearon","followers_url":"https://api.github.com/users/gaearon/followers","following_url":"https://api.github.com/users/gaearon/following{/other_user}","gists_url":"https://api.github.com/users/gaearon/gists{/gist_id}","starred_url":"https://api.github.com/users/gaearon/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/gaearon/subscriptions","organizations_url":"https://api.github.com/users/gaearon/orgs","repos_url":"https://api.github.com/users/gaearon/repos","events_url":"https://api.github.com/users/gaearon/events{/privacy}","received_events_url":"https://api.github.com/users/gaearon/received_events","type":"User","site_admin":false},"milestone":null,"comments":19,"created_at":"2015-06-16T23:03:24Z","updated_at":"2015-06-22T19:10:52Z","closed_at":null,"body":"Reviving musings from #6, I'm going to post my progress on working on time travel & other devtool goodness here.\r\n\r\n![screen shot 2015-06-17 at 02 00 47](https://cloud.githubusercontent.com/assets/810438/8196447/cfb4c206-1494-11e5-8621-21dccf313ab0.png)\r\n\r\nI have a working proof of concept of time travel that uses the default dispatcher and relies on *store composition* to keep a log of state. This is not feature-complete (only “jumping to state #N” is supported), but shows how to implement such functionality with Store composition:\r\n\r\n```js\r\nexport const StateKeys = {\r\n  LOG: Symbol('Log')\r\n};\r\n\r\nexport const ActionTypes = {\r\n  JUMP_TO_STATE: Symbol('JUMP_TO_STATE')\r\n};\r\n\r\nexport default function recorder(store) {\r\n  return function Recorder(state = {}, action) {\r\n    let { [StateKeys.LOG]: log = [], ...atom } = state;\r\n\r\n    let nextAtom;\r\n    switch (action.type) {\r\n    case ActionTypes.JUMP_TO_STATE:\r\n      const { index } = action;\r\n      nextAtom = log[index].nextAtom;\r\n      break;\r\n    default:\r\n      nextAtom = store(atom, action);\r\n      break;\r\n    }\r\n\r\n    log = [...log, {\r\n      action,\r\n      atom,\r\n      nextAtom\r\n    }];\r\n\r\n    return {\r\n      [StateKeys.LOG]: log,\r\n      ...nextAtom\r\n    };\r\n  }\r\n}\r\n```\r\n\r\nUsage:\r\n\r\n```js\r\nconst store = composeStores(stores);\r\nconst dispatcher = createDispatcher(recorder(store)); // wrap the store\r\nconst redux = createRedux(dispatcher);\r\n```\r\n\r\nThis relies on the root atom being “normal” a JS object though. I think we'll need to store it internally as `atom` field instead inside a plain JS object created by us to support such “hidden” tool state.\r\n\r\n**I'd accept a PR that hides the user store's state inside an `atom` field on the Redux instance's `state` object, at the same time preserving the current API and behavior exactly.** `Redux.getState()` should keep retuning that atom. Any other fields on the internal `state` will be assumed to be only used by the devtools."},"comment":{"url":"https://api.github.com/repos/gaearon/redux/issues/comments/114224133","html_url":"https://github.com/gaearon/redux/issues/113#issuecomment-114224133","issue_url":"https://api.github.com/repos/gaearon/redux/issues/113","id":114224133,"user":{"login":"bdowning","id":366426,"avatar_url":"https://avatars.githubusercontent.com/u/366426?v=3","gravatar_id":"","url":"https://api.github.com/users/bdowning","html_url":"https://github.com/bdowning","followers_url":"https://api.github.com/users/bdowning/followers","following_url":"https://api.github.com/users/bdowning/following{/other_user}","gists_url":"https://api.github.com/users/bdowning/gists{/gist_id}","starred_url":"https://api.github.com/users/bdowning/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/bdowning/subscriptions","organizations_url":"https://api.github.com/users/bdowning/orgs","repos_url":"https://api.github.com/users/bdowning/repos","events_url":"https://api.github.com/users/bdowning/events{/privacy}","received_events_url":"https://api.github.com/users/bdowning/received_events","type":"User","site_admin":false},"created_at":"2015-06-22T19:10:52Z","updated_at":"2015-06-22T19:10:52Z","body":"I'm probably missing something, but for this:\r\n\r\n```javascript\r\nexport default function createDebugRedux(reducer) {\r\n  const timeMachine = createRedux(timeTravel(reducer));\r\n\r\n  return {\r\n    timeMachine: timeMachine,\r\n    subscribe: ::timeMachine.subscribe,\r\n    getReducer: ::timeMachine.getReducer,\r\n    replaceReducer: ::timeMachine.replaceReducer,\r\n    dispatch(action) {\r\n      timeMachine.dispatch({\r\n        type: ActionTypes.PERFORM_ACTION,\r\n        action\r\n      });\r\n    },\r\n    getState() {\r\n      return timeMachine.getState()[StateKeys.CURRENT_STATE]\r\n    }\r\n  };\r\n}\r\n```\r\n\r\nWouldn't you need to implement your own `replaceReducer` to rewrap the replaced one in `timeTravel`, and also save the inner reducer to return in `getReducer`?  Otherwise you'll break the time travel if you replace the reducer and leak it out of you get the reducer.\r\n\r\nSo something like:\r\n\r\n```javascript\r\nexport default function createDebugRedux(reducer) {\r\n  const timeMachine = createRedux(timeTravel(reducer));\r\n\r\n  return {\r\n    timeMachine: timeMachine,\r\n    subscribe: ::timeMachine.subscribe,\r\n    getReducer() { return reducer; },\r\n    replaceReducer(newReducer) {\r\n        reducer = newReducer;\r\n        timeMachine.replaceReducer(timeTravel(newReducer));\r\n    },\r\n    dispatch(action) {\r\n      timeMachine.dispatch({\r\n        type: ActionTypes.PERFORM_ACTION,\r\n        action\r\n      });\r\n    },\r\n    getState() {\r\n      return timeMachine.getState()[StateKeys.CURRENT_STATE]\r\n    }\r\n  };\r\n}\r\n```"}}