Skip to content

Instantly share code, notes, and snippets.

@ryanflorence
Last active June 24, 2016 09:10
Show Gist options
  • Star 26 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save ryanflorence/2eaf456fc62acc73894a to your computer and use it in GitHub Desktop.
Save ryanflorence/2eaf456fc62acc73894a to your computer and use it in GitHub Desktop.

Hang on, I'm not saying flux is dead, or that redux isn't amazing.

Okay ...

Most everybody is familiar with flux by now:

User Interaction --event data-->
  Action Creator --action-->
    Dispatcher --payload-->
      Stores --change listeners-->
        Views Update

There's two types of data in an app:

  1. Shared State (auth data, etc)
  2. Component State (how many "attendees" to a meeting in a form)

There are also two types of actions:

  1. Application actions (triggers change to shared state)
  2. Component actions (triggers change to component state)

I turned to flux to solve problem (1) in both cases above.

  • Share state between components with stores, get updates via subscriptions.
  • Avoid passing callbacks several levels down the component tree for application actions.

I then ended up forcing (2) into flux and started getting grumpy. But using flux for some state, but not all state, and for some actions, but not all actions, feels just as bad to me.

I wanted to just go back to component state, its much easier to deal with. But what about shared state and application actions?

Turns out, React already has a solution for this with context.

class App extends React.Component {
  constructor (props, context) {
    super(props, context)

    // App is your "global store"
    this.state = {
      auth: null
    }
  }

  handleDispatch (action) {
    // Handle dispatched actions by changing state
    if (action.type === 'AUTH')
      this.setState({ auth: action.auth })
  }

  render () {
    // new state goes to `AppContext`
    return (
      <AppContext
        state={this.state}
        onDispatch={(action) => this.handleDispatch(action)}
      >
        <AppView/>
      </AppContext>
    )
  }
}

class AppContext extends React.Component {

  // receives state and dispatch from `App`
  static propTypes = {
    state: object,
    onDispatch: func,
  }

  // makes app state and dispatch available to all components
  // via context
  static childContextTypes = {
    state: object,
    dispatch: func,
  }

  getChildContext () {
    return {
      state: this.props.state,
      dispatch: (action) => this.props.onDispatch(action)
    }
  }

  render () {
    return this.props.children
  }
}


class AppView extends React.Component {
  render () {
    return (
      <div>
        <Header/>
        <Content/>
      </div>
    )
  }
}


class Header extends React.Component {

  // ask for global state with context types
  static contextTypes = {
    state: object,
    dispatch: func
  }

  constructor (props, context) {
    super(props, context)

    // can have local state
    this.state = {
      isLoggingIn: false
    }
  }

  login () {
    this.setState({ isLoggingIn: true })
    login((auth) => {
      // dispatch change to app state
      this.context.dispatch({ type: 'AUTH', auth })
      this.setState({ isLoggingIn: false })
    })
  }

  render () {
    let { auth } = this.context.state
    return (
      <div>
        <h1>
          {this.context.state.auth ?
            `Welcome ${auth.name}` :
            'You are not logged in'
          }
        </h1>

        {this.context.state.auth === null ? (
          <button
            onClick={() => this.login()}
            disabled={this.state.isLoggingIn}
          >Login</button>
        ) : (
          <button onClick={() => this.logout()}>Logout</button>
        )}
      </div>
    )
  }
}
  1. click "Login" button
  2. local state changes
  3. dispatch method available in Header because of AppContext
  4. app action is dispatched
  5. App handles action, sets state
  6. AppContext gets new App state, is now available on context
  7. Entire app can now access auth state from App via context.

You want middleware? How about just overloading React.Component.prototype.setState?

Anyway, I love redux, I'm just thinking out loud, and looking for the dumbest thing that will work.

Or, another way to say that, when I add a feature I don't want to have to edit 6 files when all 6 files only affect one or two components.

@nickdima
Copy link

Could make sense for some applications but I don't know, maybe it could complicate things as the app grows. When introducing new components/data you have to reason if their state should be shared or not, whether you might need it in the future in some other place. You could start with component state and then figure out you need pieces of the data also somewhere else so in that case you'll need to refactor it to shared state...
And what about universal/isomorphic rendering? At first sight it seems more difficult to pull it off by not having your data as a tree.

@maxguzenski
Copy link

Or maybe, what you really needs, is a way to "reduce" your 6 files? Maybe something like https://github.com/erikras/ducks-modular-redux

I don't like the "default" idea of "break" the application by types (/constants, / stores, / actions) rather than by context (/activities.js, users.js, etc.)... but I think I'm alone in this opinion;)

@ryanflorence
Copy link
Author

@nickdima that question is simple, is it shared? no? component state.

@tmbtech
Copy link

tmbtech commented Sep 16, 2015

@ryanflorence I think you meant to ask for state instead of auth

class Header extends React.Component {
  static contextTypes = {
    state: object,
    dispatch: func
  }
}

btw thank you for putting this gist up, this is a really cool idea

@seidtgeist
Copy link

@ryanflorence This is cool, thank you!

@frekw
Copy link

frekw commented Sep 16, 2015

@ryanflorence my main gripe with this is that, in my experience, any "component state" that is remotely fetched (e.g application data) is always a global concern. Because chances are that you have some sort of relationship between data that you want to keep up to sync as your application state changes . For example if you're attending an event and also change your name during the same session, the attendees list should reflect that change.

So in my opinion, the only thing that should be considered "component state" is really the state a component is in right now (e.g should an action dropdown be shown or not). If you don't treat application data as global state you'll end up with inconsistent data in your application as your application grows.

@ivantsov
Copy link

Makes sense but need to try it in a big app. Maybe need some API to reduce boilerplate.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment