{"action":"created","issue":{"url":"https://api.github.com/repos/gothinkster/react-redux-realworld-example-app/issues/28","repository_url":"https://api.github.com/repos/gothinkster/react-redux-realworld-example-app","labels_url":"https://api.github.com/repos/gothinkster/react-redux-realworld-example-app/issues/28/labels{/name}","comments_url":"https://api.github.com/repos/gothinkster/react-redux-realworld-example-app/issues/28/comments","events_url":"https://api.github.com/repos/gothinkster/react-redux-realworld-example-app/issues/28/events","html_url":"https://github.com/gothinkster/react-redux-realworld-example-app/issues/28","id":188610543,"number":28,"title":"i made notes/docs","user":{"login":"thejmazz","id":1270998,"avatar_url":"https://avatars.githubusercontent.com/u/1270998?v=3","gravatar_id":"","url":"https://api.github.com/users/thejmazz","html_url":"https://github.com/thejmazz","followers_url":"https://api.github.com/users/thejmazz/followers","following_url":"https://api.github.com/users/thejmazz/following{/other_user}","gists_url":"https://api.github.com/users/thejmazz/gists{/gist_id}","starred_url":"https://api.github.com/users/thejmazz/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/thejmazz/subscriptions","organizations_url":"https://api.github.com/users/thejmazz/orgs","repos_url":"https://api.github.com/users/thejmazz/repos","events_url":"https://api.github.com/users/thejmazz/events{/privacy}","received_events_url":"https://api.github.com/users/thejmazz/received_events","type":"User","site_admin":false},"labels":[],"state":"closed","locked":false,"assignee":null,"assignees":[],"milestone":null,"comments":2,"created_at":"2016-11-10T20:29:09Z","updated_at":"2016-11-14T19:22:13Z","closed_at":"2016-11-14T19:22:13Z","body":"Just looking for some place to put these.\r\n\r\nI think understanding patterns/idioms is more important than reading a codebase, so I tried to extract all that I could find in this project. As well some of my own ideas snuck a little in (e.g. thinking of how to integrate Flow), Would like feedback on anything missed, **ideally this describes how everything works in the project**.\r\n\r\nEnjoy:\r\n\r\n\r\n\r\n\r\n\r\n# React-Redux App\r\n\r\n## Entrypoint\r\n\r\nFile: `index.js`\r\n\r\nImports: `agent.js`, `store.js`, `./components/*`\r\n\r\nRenders routes pointing to their associated components:\r\n\r\n```html\r\n  <Provider store={store}>\r\n    <Router history={hashHistory}>\r\n      <Route path=\"/\" component={App}>\r\n        <IndexRoute component={Home} />\r\n        <Route path=\"login\" component={Login} />\r\n        <Route path=\"register\" component={Register} />\r\n        <Route path=\"editor\" component={Editor} />\r\n        <Route path=\"editor/:slug\" component={Editor} />\r\n        <Route path=\"article/:id\" component={Article} />\r\n        <Route path=\"settings\" component={Settings} />\r\n        <Route path=\"@:username\" component={Profile} />\r\n        <Route path=\"@:username/favorites\" component={ProfileFavorites} />\r\n      </Route>\r\n    </Router>\r\n  </Provider>\r\n```\r\n\r\n## Agent\r\n\r\nFile: `agent.js`\r\n\r\nExports an object where each key is a \"service\" and a service has\r\nmethods that internally run a request:\r\n\r\n- `get`\r\n- `put`\r\n- `post`\r\n- `delete`\r\n\r\nFor example, `Auth`:\r\n\r\n```javascript\r\nconst Auth = {\r\n  current: () =>\r\n    requests.get('/user'),\r\n  login: (email, password) =>\r\n    requests.post('/users/login', { user: { email, password } }),\r\n  register: (username, email, password) =>\r\n    requests.post('/users', { user: { username, email, password } }),\r\n  save: user =>\r\n    requests.put('/user', { user })\r\n};\r\n```\r\n\r\nThus, these services essentially take some options, map to a request, and\r\nreturn the promise of that request. The general type could be:\r\n\r\n```javascript\r\ntype Service = {\r\n    [key: string]: (opts: any) => Promise<T>\r\n}\r\n```\r\n\r\nAs well, `agent.js` locally stores a token which can be set via the exported\r\n`setToken`. As some config there is `API_ROOT`.\r\n\r\n## Redux\r\n\r\n### Store\r\n\r\nFile: `store.js`\r\n\r\nImports: `reducer.js`, `middleware.js`\r\n\r\nFairly simple store setup, applies `promiseMiddleware` before\r\n`localStorageMiddleware`, logger only on development.\r\n\r\n### Middleware\r\n\r\nFile: `middleware.js`\r\n\r\nImports: `agent.js`\r\n\r\n#### promiseMiddleware\r\n\r\nIntercepts all actions where `action.payload` is a Promise. In which case it:\r\n\r\n1. `store.dispatch({ type: 'ASYNC_START', subtype: action.type })`\r\n2. `action.payload.then`\r\n    - success: `store.dispatch({ type: 'ASYNC_END', promise: res })`\r\n    - error: sets `action.error = true`, `store.dispatch({ type: 'ASYNC_END', promise: action.payload })`\r\n3. Then, for success and error, using the modified `action` object: `store.dispatch(action)`\r\n\r\n#### localStorageMiddleware\r\n\r\nRuns after `promiseMiddleware`. Intercepts `REGISTER | LOGIN` and either\r\n\r\n  - a. sets token into localstorage and `agent.setToken(token)`\r\n  - b. sets token in localstorage to `''` and does `agent.setToken(null)`\r\n\r\n### Reducers\r\n\r\nFile: `reducer.js`\r\n\r\nImports: `./reducers/*.js`\r\n\r\nUses `combineReducers` to export a reducer where each key is the reducer\r\nof the file with the same key.\r\n\r\n#### General Reducer Patterns\r\n\r\n- map payload into piece of state\r\n- toggle loading states by casing on `ASYNC_START` and `action.subtype`\r\n\r\n```javascript\r\ncase 'ASYNC_START':\r\n  if (action.subtype === 'LOGIN' || action.subtype === 'REGISTER') {\r\n    return { ...state, inProgress: true };\r\n  }\r\n```\r\n\r\n- toggle errors by taking `action.errors` if it is there (see middleware)\r\n\r\n```javascript\r\ncase 'REGISTER':\r\n  return {\r\n    ...state,\r\n    inProgress: false,\r\n    errors: action.error ? action.payload.errors : null\r\n  };\r\n```\r\n\r\n- set state keys to null if they did not come in payload (Flow type issues?)\r\n\r\n```javascript\r\ncase 'REGISTER':\r\n  return {\r\n    ...state,\r\n    inProgress: false,\r\n    errors: action.error ? action.payload.errors : null\r\n  };\r\n```\r\n\r\n- handle redirections (will be triggered by `componentWillReceiveProps` somewhere)\r\n\r\n```javascript\r\ncase 'REDIRECT':\r\n  return { ...state, redirectTo: null };\r\ncase 'LOGOUT':\r\n  return { ...state, redirectTo: '/', token: null, currentUser: null };\r\ncase 'ARTICLE_SUBMITTED':\r\n  const redirectUrl = `article/${action.payload.article.slug}`;\r\n  return { ...state, redirectTo: redirectUrl };\r\n```\r\n\r\n## Components\r\n\r\nMost `mapStateToProps` won't be mentionned, as there are fairly simple. Take\r\nsome objects, use them in render.\r\n\r\n`mapDispatchToProps` will be referred to as \"handlers\". Some will emerge as\r\ncommon ones. Dispatching some specific handlers on some specific lifecylce\r\nmethods will also emerge as a pattern.\r\n\r\nHandlers:\r\n\r\n- `onLoad`\r\n- `onUnload`\r\n- `onSubmit`\r\n- `onClick`\r\n- `onX`\r\n\r\n`onLoad` seems to be the most common one, used for any components that need ajax in\r\ndata into store into props into their render method (which is basically everything on\r\nan SPA lol).\r\n\r\nPatterns\r\n\r\n- `onLoad` handlers pass a Promise or multiple promises via `Promise.all`\r\n - sending multiple leads to magic `payload[0]` and `payload[1]` in reducer (see `reducers/article.js`)\r\n\r\n- pass a handler, e.g. `onClickTag` as a prop to a child component. child\r\n  component then calls it with agent: `props.onClickTag(tag,\r\n  agent.Articles.byTag(tag))`. (does this only ever happen with a connected `index.jsx` inside a folder?)\r\n\r\n- to render or not to render:\r\n\r\n```javascript\r\nif (!this.props.data) {\r\n  component = <Loading /> // or perhaps null like in Header.js, ListErrors, EditProfileSettings in Profile\r\n} else {\r\n  component = <Thing data={this.props.data} />\r\n}\r\n```\r\n\r\n- similary, if you cannot call handlers yet since props are not ready:\r\n\r\n```javascript\r\ncomponentWillMount() {\r\n  if (this.props.params.slug) {\r\n    return this.props.onLoad(agent.Articles.get(this.props.params.slug));\r\n  }\r\n  this.props.onLoad(null);\r\n}\r\n```\r\n\r\n- use `componentWillReceiveProps` to call handlers if necessary, e.g. in `Editor.js`:\r\n\r\n```javascript\r\ncomponentWillReceiveProps(nextProps) {\r\n  if (this.props.params.slug !== nextProps.params.slug) {\r\n    if (nextProps.params.slug) {\r\n      this.props.onUnload();\r\n      return this.props.onLoad(agent.Articles.get(this.props.params.slug));\r\n    }\r\n    this.props.onLoad(null);\r\n  }\r\n}\r\n```\r\n\r\n\r\n### Root Component - \"/\"\r\n\r\nImported components: `Header`\r\n\r\nHandlers\r\n\r\n- `onLoad: (payload, token) => dispatch({ type: 'APP_LOAD', payload, token, skipTracking: true })`\r\n- `onRedirect: () => dispatch({ type: 'REDIRECT' })`\r\n\r\nLifecycle\r\n\r\n```javascript\r\ncomponentWillMount() {\r\n  const token = window.localStorage.getItem('jwt');\r\n  if (token) {\r\n    agent.setToken(token);\r\n  }\r\n\r\n  this.props.onLoad(token ? agent.Auth.current() : null, token);\r\n}\r\n\r\ncomponentWillReceiveProps(nextProps) {\r\n  if (nextProps.redirectTo) {\r\n    this.context.router.replace(nextProps.redirectTo);\r\n    this.props.onRedirect();\r\n  }\r\n}\r\n```\r\n\r\n### Home Component - \"/\"\r\n\r\n(`<IndexRoute>` on \"/\")\r\n\r\nHandlers\r\n\r\n```javascript\r\nonClickTag: (tag, payload) => dispatch({ type: 'APPLY_TAG_FILTER', tag, payload }),\r\nonLoad: (tab, payload) => dispatch({ type: 'HOME_PAGE_LOADED', tab, payload }),\r\nonUnload: () => dispatch({  type: 'HOME_PAGE_UNLOADED' })\r\n```\r\n\r\nLifecycle\r\n\r\n```javascript\r\ncomponentWillMount() {\r\n  const tab = this.props.token ? 'feed' : 'all';\r\n  const articlesPromise = this.props.token ?\r\n    agent.Articles.feed() :\r\n    agent.Articles.all();\r\n\r\n  this.props.onLoad(tab, Promise.all([agent.Tags.getAll(), articlesPromise]));\r\n}\r\n\r\ncomponentWillUnmount() {\r\n  this.props.onUnload();\r\n}\r\n```\r\n\r\n### Other Components\r\n\r\nShould be self explanatory, follow patterns described above, it was just the home\r\nand index components are somewhat unique due to handling of routing."},"comment":{"url":"https://api.github.com/repos/gothinkster/react-redux-realworld-example-app/issues/comments/260434176","html_url":"https://github.com/gothinkster/react-redux-realworld-example-app/issues/28#issuecomment-260434176","issue_url":"https://api.github.com/repos/gothinkster/react-redux-realworld-example-app/issues/28","id":260434176,"user":{"login":"EricSimons","id":556934,"avatar_url":"https://avatars.githubusercontent.com/u/556934?v=3","gravatar_id":"","url":"https://api.github.com/users/EricSimons","html_url":"https://github.com/EricSimons","followers_url":"https://api.github.com/users/EricSimons/followers","following_url":"https://api.github.com/users/EricSimons/following{/other_user}","gists_url":"https://api.github.com/users/EricSimons/gists{/gist_id}","starred_url":"https://api.github.com/users/EricSimons/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/EricSimons/subscriptions","organizations_url":"https://api.github.com/users/EricSimons/orgs","repos_url":"https://api.github.com/users/EricSimons/repos","events_url":"https://api.github.com/users/EricSimons/events{/privacy}","received_events_url":"https://api.github.com/users/EricSimons/received_events","type":"User","site_admin":false},"created_at":"2016-11-14T19:22:13Z","updated_at":"2016-11-14T19:22:13Z","body":"Definitely! Thanks for this @thejmazz -- I'm going to put this in the readme later today & will attribute you :)"}}
{"actor":{"display_login":"EricSimons"}}