fluxed

2.1.0 • Public • Published

fluxed

A very small flux-like state container with the same React bindings as react-redux.

Example

The first thing we need to manage our state is a store. In fluxed a Store is a base class you should subclass. This subclass is where you put the shared global state for your application. Example:

import { Store } from 'fluxed'

export default class MyStore extends Store {
  state = {
    userLoggedIn: false,
    isLoggingIn: false,
  }

  async login(username, password) {
    this.setState({ isLoggingIn: true })
    try {
      const response = await fetch('/login', { method: 'POST' })
      if (response.ok) {
        const body = await response.json()
        this.setState({ userLoggedIn: true })
      }
    } finally {
      this.setState({ isLogginIn: false })
    }
  }
}

There. No action creators, constants, reducers, thunks, selectors, etc. Just a single class that you can call setState on to set new state.

That's really all you need for flux. To manually hook this store up to a component it would look something like this contrived example:

import React, { Component } from 'react'
import MyStore from './store'

const store = new MyStore()

class Login extends Component {
  state = {
    username: '',
    password: ''
  }

  componentWillMount() {
    // attach our component to the store - whenever the store's state changes
    // we will update our component's local state to be equal to the store's state
    // also we will automatically unsubscribe from the store when this component unmounts
    this.componentWillUnmount = store.subscribe((state) => this.setState(state))
  }

  onSubmit = (e) => {
    e.preventDefault()
    e.stopPropagation()
    const { username, password } = this.state
    store.login(username, password)
      .catch(e => alert('login failed!'))
  }

  render() {
    const { isLoggingIn, username, login } = this.state
    if (isLoggingIn) {
      return <div>Logging in, please wait...</div>
    }
    if (userLoggedIn) {
      return <div>Welcome user!</div>
    }
    return (
      <form onSubmit={this.onSubmit}>
        <input placeholder='Username' value={username} onChange={e => this.setState({ username: e.target.value })} />
        <input placeholder='Password' value={password} onChange={e => this.setState({ password: e.target.value })} />
        <input type='submit'>Log in!</input>
      </form>
    )
  }
}

That's it!

But wait...

One thing that's not nice about the example above is the Login component is coupled directly to an instance of the store. We lose out on a lot of composability and reusability because everywhere the Login component goes it takes with it its own instance of MyStore.

We could instantiate MyStore in a different file and require that file & it's single instance in the Login component. That way we could share the single store instance with other components in our app; however, each component would still be referencing the store directly both when subscribing/unsubscribing to the store, and when calling actions on the store.

Provider & connect

If you're familiar with react-redux we've copied the concepts of its "dependency injection" here. We can "connect" our components to a store instance "provided" to the component hierarchy via the <Provider /> component. It looks like this:

import React, { Component } from 'react'
import { Provider, connect, Store } from 'fluxed'

class NameStore extends Store {
  state = {
    isNameValid: true,
    name: 'foo',
  }

  setName(name) {
    // don't allow blank names!
    const isNameValid = name && name.length
    this.setState({ name, isNameValid })
  }
}

const store = new NameStore()

// this is our main 'app' component
class App extends Component {
  render() {
    // we 'provide' the store instance to all sub-components
    // anywhere they are in the component hierarchy under Provider
    return (
      <Provider store={store}>
        <div>
          <NavBar />
          <div>
            <Content />
          </div>
          <Footer />
        </div>
      </Provider>
    )
  }
}

@connect
class NavBar extends Component {
  render() {
    // notice the state of the store is now available as props.
    // our NavBar component has no idea the props come from the store and not
    // directly set by a parent component
    const { name, isNameValid } = this.props
    const text = isNameValid ? `Hello ${name}!` : `Please enter a name`
    return <div>{text}</div>
  }
}

@connect
class Content extends Component {
  render() {
    // Notice all the methods on the store instance
    // are passed in as props to this component as well.
    // The component doesn't know or care if it came from a parent component directly
    // or from a connected store
    const { name, setName } = this.props
    return (
      <div>
        <p>Please enter your name:</p>
        <input value={name} onChange={e => setName(e.target.value)} />
      </div>
    )
  }
}

const Footer = connect()((props) => {
  return (
    <div>You can connect functional components as well, {props.name || 'whoever you are'}!</div>
  )
})

The <Provider /> and connect methods intentionally mirror the react-redux method signatures. This is to make it easy to migrate to redux if/when you want to. I think fluxed is a great way to get started & teach the concepts of flux without also having to give a long talk on functional programming concepts and introduce a lot of ceremony.

API

Store

Store is a class. It is intended to be subclassed much like React.Component is subclassed.

store.setState(newState: object) => void

Used to update the state of the store. All subscription callbacks will be synchronously called with the new state immediately after the state is updated.

Keys are shallowly merged with existing store state similar to how react.setState works. Unlike react's setState this method is not async and does not batch calls.

store.subscribe(callback: (state: object) => void) => (unsubscribe: () => void)

Subscribes a callback to the store which will be called with the new store state every time the store state changes. Returns a function you can call to remove this subscription from the store.

store.state: object

The current state of the store. You should avoid accessing this externally, but can be useful in store methods to check the existing state & computing new state from it.

store.mount(path: string, subStore: Store) => void

Mounts a subStore at a given path. The whenever the sub-store is updated, the parent store's subscriptions will also be notified.

Provider

<Provider /> is a higher-order component which has a required store property. Internally provider sets the supplied store on the context allowing any connectedComponent created via connect to access the store given to the <Provider /> regardless of where the connected components live within the component hierarchy.

This mirrors react-redux 1:1 AFAIK.

connect

// flowtype type signature
() => ((component: ReactComponent) => connectedComponent: ReactComponent)

Connect is a function that takes no arguments. It returns a function which takes an a React component and returns a higher-order React connectedComponent which "connects" instances of the component to a provided store. The connected store comes from whichever store is supplied as a prop to the <Provider /> component. note: the <Provider /> component must be a higher level in the dom tree than all connected components. Commonly <Provider /> is at or near the very top of your application's dom tree. The store's state and the store's methods will both be passed into the component instance as props. Locally supplied props to the component will take precedence over any comming from the connected store.

note: react-redux has mapStateToProps and mapDispatchToProps as arguments to its connect() function. Fluxed doesn't have that at this time.

Composition

In larger apps it's common to break your state up into multiple 'regions' or 'namespaces' where different concerns can be isolated. Fluxed supports that with the mount method on a store which allows you to compose stores by mounting them at different points within parent stores.

example:

class AppStore extends Store {

}

class AuthStore extends Store {
  async login(username, password) {
    const res = await fetch('/login', {
      method: 'POST',
      body: JSON.stringify({ username, password }),
    })
    this.setState({ loggedIn: res.ok })
  }
}

class PostStore extends Store {
  async load() {
    const res = await fetch('/posts')
    const list = await res.json()
    this.setState({ list })
  }
}

const store = new AppStore()
store.mount('auth', AuthStore)
store.mount('posts', PostStore)

await store.auth.login()
await store.posts.load()

assert.deepEqual(store.state, {
  auth: { loggedIn: true },
  posts: { list: [ /* array of posts here */ ]}
})

/// etc...

License

Copyright (c) 2017 ShipStation

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

Readme

Keywords

none

Package Sidebar

Install

npm i fluxed

Weekly Downloads

10

Version

2.1.0

License

MIT

Last publish

Collaborators

  • brianc