fluxed
v2.1.0
Published
Like redux, but simple.
Downloads
78
Readme
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.