gothinkster / react-redux-realworld-example-app

Exemplary real world application built with React + Redux
https://react-redux.realworld.io
MIT License
5.57k stars 2.51k forks source link

i made notes/docs #28

Closed thejmazz closed 7 years ago

thejmazz commented 7 years ago

Just looking for some place to put these.

I 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.

Enjoy:

React-Redux App

Entrypoint

File: index.js

Imports: agent.js, store.js, ./components/*

Renders routes pointing to their associated components:

  <Provider store={store}>
    <Router history={hashHistory}>
      <Route path="/" component={App}>
        <IndexRoute component={Home} />
        <Route path="login" component={Login} />
        <Route path="register" component={Register} />
        <Route path="editor" component={Editor} />
        <Route path="editor/:slug" component={Editor} />
        <Route path="article/:id" component={Article} />
        <Route path="settings" component={Settings} />
        <Route path="@:username" component={Profile} />
        <Route path="@:username/favorites" component={ProfileFavorites} />
      </Route>
    </Router>
  </Provider>

Agent

File: agent.js

Exports an object where each key is a "service" and a service has methods that internally run a request:

For example, Auth:

const Auth = {
  current: () =>
    requests.get('/user'),
  login: (email, password) =>
    requests.post('/users/login', { user: { email, password } }),
  register: (username, email, password) =>
    requests.post('/users', { user: { username, email, password } }),
  save: user =>
    requests.put('/user', { user })
};

Thus, these services essentially take some options, map to a request, and return the promise of that request. The general type could be:

type Service = {
    [key: string]: (opts: any) => Promise<T>
}

As well, agent.js locally stores a token which can be set via the exported setToken. As some config there is API_ROOT.

Redux

Store

File: store.js

Imports: reducer.js, middleware.js

Fairly simple store setup, applies promiseMiddleware before localStorageMiddleware, logger only on development.

Middleware

File: middleware.js

Imports: agent.js

promiseMiddleware

Intercepts all actions where action.payload is a Promise. In which case it:

  1. store.dispatch({ type: 'ASYNC_START', subtype: action.type })
  2. action.payload.then
    • success: store.dispatch({ type: 'ASYNC_END', promise: res })
    • error: sets action.error = true, store.dispatch({ type: 'ASYNC_END', promise: action.payload })
  3. Then, for success and error, using the modified action object: store.dispatch(action)

localStorageMiddleware

Runs after promiseMiddleware. Intercepts REGISTER | LOGIN and either

Reducers

File: reducer.js

Imports: ./reducers/*.js

Uses combineReducers to export a reducer where each key is the reducer of the file with the same key.

General Reducer Patterns

case 'ASYNC_START':
  if (action.subtype === 'LOGIN' || action.subtype === 'REGISTER') {
    return { ...state, inProgress: true };
  }
case 'REGISTER':
  return {
    ...state,
    inProgress: false,
    errors: action.error ? action.payload.errors : null
  };
case 'REGISTER':
  return {
    ...state,
    inProgress: false,
    errors: action.error ? action.payload.errors : null
  };
case 'REDIRECT':
  return { ...state, redirectTo: null };
case 'LOGOUT':
  return { ...state, redirectTo: '/', token: null, currentUser: null };
case 'ARTICLE_SUBMITTED':
  const redirectUrl = `article/${action.payload.article.slug}`;
  return { ...state, redirectTo: redirectUrl };

Components

Most mapStateToProps won't be mentionned, as there are fairly simple. Take some objects, use them in render.

mapDispatchToProps will be referred to as "handlers". Some will emerge as common ones. Dispatching some specific handlers on some specific lifecylce methods will also emerge as a pattern.

Handlers:

onLoad seems to be the most common one, used for any components that need ajax in data into store into props into their render method (which is basically everything on an SPA lol).

Patterns

if (!this.props.data) {
  component = <Loading /> // or perhaps null like in Header.js, ListErrors, EditProfileSettings in Profile
} else {
  component = <Thing data={this.props.data} />
}
componentWillMount() {
  if (this.props.params.slug) {
    return this.props.onLoad(agent.Articles.get(this.props.params.slug));
  }
  this.props.onLoad(null);
}
componentWillReceiveProps(nextProps) {
  if (this.props.params.slug !== nextProps.params.slug) {
    if (nextProps.params.slug) {
      this.props.onUnload();
      return this.props.onLoad(agent.Articles.get(this.props.params.slug));
    }
    this.props.onLoad(null);
  }
}

Root Component - "/"

Imported components: Header

Handlers

Lifecycle

componentWillMount() {
  const token = window.localStorage.getItem('jwt');
  if (token) {
    agent.setToken(token);
  }

  this.props.onLoad(token ? agent.Auth.current() : null, token);
}

componentWillReceiveProps(nextProps) {
  if (nextProps.redirectTo) {
    this.context.router.replace(nextProps.redirectTo);
    this.props.onRedirect();
  }
}

Home Component - "/"

(<IndexRoute> on "/")

Handlers

onClickTag: (tag, payload) => dispatch({ type: 'APPLY_TAG_FILTER', tag, payload }),
onLoad: (tab, payload) => dispatch({ type: 'HOME_PAGE_LOADED', tab, payload }),
onUnload: () => dispatch({  type: 'HOME_PAGE_UNLOADED' })

Lifecycle

componentWillMount() {
  const tab = this.props.token ? 'feed' : 'all';
  const articlesPromise = this.props.token ?
    agent.Articles.feed() :
    agent.Articles.all();

  this.props.onLoad(tab, Promise.all([agent.Tags.getAll(), articlesPromise]));
}

componentWillUnmount() {
  this.props.onUnload();
}

Other Components

Should be self explanatory, follow patterns described above, it was just the home and index components are somewhat unique due to handling of routing.

vkarpov15 commented 7 years ago

That's a pretty good summary, thanks :+1: @EricSimons think we might be able to use this or something like it somewhere?

EricSimons commented 7 years ago

Definitely! Thanks for this @thejmazz -- I'm going to put this in the readme later today & will attribute you :)

thejmazz commented 7 years ago

Hey @EricSimons, don't mean to bother, but it has been a little over a week and I don't think the readme has been updated!

deksden commented 7 years ago

Really good content to start some Wiki for this repo

EricSimons commented 7 years ago

Totally - was just thinking about this today. Been tied with the main RealWorld project lately, but I'd be down to make this happen over the weekend 👍 On Fri, Apr 28, 2017 at 2:57 AM Denis Kiselev notifications@github.com wrote:

Really good content to start some Wiki for this repo

— You are receiving this because you modified the open/close state.

Reply to this email directly, view it on GitHub https://github.com/gothinkster/react-redux-realworld-example-app/issues/28#issuecomment-297933792, or mute the thread https://github.com/notifications/unsubscribe-auth/AAh_hq3ckf2RyOClHDpdAy1A6guyioB0ks5r0Zv4gaJpZM4KvGlb .

-- Sent from my iPhone

EricSimons commented 7 years ago

@thejmazz your work is now powering the readme :) thanks again for your help!