redux-react-local
v1.4.1
Published
recovering local component state for redux
Downloads
24
Readme
redux-react-local
npm install redux-react-local --save
warning: the api's in a state of flux right now
(badum-tsh)
tl;dr
import { local } from 'redux-react-local'
// connect your components
@local({
ident: 'app',
initial: { count: 0 },
// optionally -
reducer(state, action) {
if(action.me) { // happened 'locally'
switch(action.meta.type) {
// case: increment decrement etc
}
}
// reduce on other global dispatches here
return state
}
})
class App extends React.Component {
render() {
let { state, dispatch, $ } = this.props
return (<div onClick={() => dispatch($({ type: 'increment' }))}>
clicked {state.count} times
</div>)
}
}
getting started
Add the supplied reducer to a key local
on your redux store.
// ...
combineReducers({
// your other reducers
local: reducer
})
// ...
and wrap your app with the Root
component
// ...
<Provider store={store}>
<Root>
<App/>
</Root>
</Provider>
// ...
local
decorator for a react component
ident
- a 'unique' string that corresponds to the component. eg - 'app', 'inbox', 'messages:341', etcident (props)
- a function that returns the aboveinitial
- initial local stateinitial (props)
- a function that returns the abovereducer (state, {type, payload, meta, me})
- a local reducer on every action on the store,me
will be true for actions dispatched locally, andmeta.type
will not have the${ident}:
prefixpersist
- a boolean on whether the state should persist in the store on unmount, defaults totrue
passed props
ident
- as abovedispatch(action)
- via redux$
- helper to locally scope an actionstate
- current local statesetState(state)
rationale
(This assumes some familiarity with React and redux)
So, React's 'components' are awesome, and lets us describe our UIs as trees/hierarchies. Lovely. I'm a big fan of architectures that are 'fractal' - where the 'big' elements are 'composed' of similar looking smaller ones, and React's components fit the bill. (Other examples - functions, observables, channels, js objects(!), etc) 1
However, because 'views' don't have global references / identities ala Backbone etc, communicating between these components can get cumbersome 2; we then resort to building some form of messaging system external to these components - callbacks, pubsub channels, flux stores, observable event chains, etc. The smart ones use context
to expose these systems to a particular render tree, avoiding 'global' state, but still getting a similar model.
Redux is a popular implementation of the flux pattern - all application state is held in one js object, where the value of each of its keys correspond to a 'reducer' function that gets called on every 'action' that's 'dispatched'. Components / external sources can then dispatch actions to 'effect' state in a controlled manner. Very nice.
Redux expects you to declare all your reducers outright at the beginning of the app's lifecycle. This is great for state that -
- will exist through the lifecycle of the app
- makes sense 'across' the app. eg - currently logged in user details, databases/caches, mouse/touch positions, etc.
- isn't usually associated with a specific component on the page (unless that component is alive for the life of the app)
However, this approach doesn't work well for 'dynamic' components that might pop in and out of existence. eg - imagine a dynamically fetched list of tweets (length unknown till after fetch), each with buttons to like/retweet etc. When hovering/clicking these buttons, we'd need to store this state somewhere. Options -
- We use
this.setState()
, but then that state is opaque to the rest of the app. We'd have to dispatch actions on everysetState
, and then have extra logic to differentiate based on type and some form of id, and this is already getting out of hand... - We store an array of components states, indexed by some key/id, and update that as and when. However, this collection's 'information' was already available in the react tree itself, and it's a shame we don't use it.
- This is complicated by the fact that each of these elements might have an initial state that's based on its props. This leads to boilerplate on mount/unmount to prime/remove this state, and we end up jumping too many files for something that's really local to just one component.
- Actions are differentiated (dynamic dispatch?) based on their string 'type', which implies a global 'namespace' of sorts for these action types. This is usually fine, but leads to ickiness in the above situation. ie - should one dispatch
'hover', {id}
or'${id}:hover'
?, etc
With the above issues, we lose the fractal nature of our architecture. which makes me a sad puppy. Sad puppies are officially considered an animal rights violation, so we have to do something about it.
What I'd really like to do is -
- define a reducer for a component's local 'state'
- that's called for all actions across the app
- that activates/deactivates with the component's mount/unmount lifecycle
- this state should correspond to a key/
ident
on the redux store - this
ident
could be dynamically generated as aƒ(props)
- dispatch 'locally scoped' actions - ie - where the action
type
is aƒ(ident)
- would be nice if the local reducer could 'recognize' actions that originated from 'itself'
[drum roll...]
redux-react-local
to the rescue! At its simplest, it looks like this -
@local({
ident: 'app',
initial: 0,
reducer(state, {type}){
if (type === 'increment'){
return state + 1;
}
return state;
}
})
class App extends Component{
render(){
return <div onClick={() => this.props.dispatch({type: 'increment'})}>
clicked {this.props.state} times
</div>;
}
}
We annotate our components with a @local
decorator declaring -
ident
- short for 'identity', we use this value as a key on our internal store, among other things. This is either a string ('app', 'inbox', etc), or a function that receives this component'sprops
and returns a string.initial
- initial state for this component. Can also be a function that receivesprops
and returns the initial state.reducer
- a reducer for local state; recieves all actions that flow through the redux store.persist
- a boolean indicating whether to 'keep' the state after the component has unmounted
The wrapped component will then recieve these props -
dispatch
- same as redux's dispatchstate
- current local stateident
- as above$
- a helper function to 'localize' actions.setState
- a helper to set a value directly to the local store. Unlike react'ssetState
, this does not merge values.
'local' actions
We need a way to generate actions that are 'local' to the component that's dispatching them. We can use $
which accepts a type
and a payload
, and generates an action that's associated with the current component. It looks like this -
$({
type: 'someAction',
payload: {some: 'payload'},
...moreStuff
})
// generates -
{
type: `${ident}:someAction`,
payload: {some: 'payload'},
...moreStuff,
meta: {
ident: ident,
type: 'someAction'
}
}
Further, when reducing these actions, we can 'recognize' these actions.
reducer(state, {me, meta, payload}){
if(me){ // happened locally!
switch(meta.type){
case 'someAction': //...
}
}
// you could 'listen' to all other global dispatches here
return state;
}
Convenient!
spiel
Fractal architectures are very nice to grok and work on, because it means you 'think' about your app the same way no matter what level of abstraction you're working on.
But also, there's value in being able to describe your system at very low levels, and have your software treat it as a global concern and optimize accordingly.
An example is the way React does event handling - when we first learn about front end web development, we attach event handlers directly on elements (<div onclick="handler()"/>
), because that's simple and easy. However, we're soon told by our seniors and whatnot that it's inefficient, and we should use event delegation at a top level, etc etc. This makes it faster, but we lose the programming model of colocating elements and their handlers (replaced with css selectors and what not). React looks at this a different way, and lets you colocate an event handler with an element (as <div onClick={() => {}}/>
), but internally makes sure it uses event delegation at the very highest level.
Simlarly for html - writing 'inline html' is a bad practice, and we're told to use templating languages, etc etc. React instead offers JSX to 'describe' html, sidestepping the need for string concatenation, and efficiently rendering/updating the dom when something 'changes', letting us still write 'inline html' conceptually, but with fewer warts.
redux-react-local is a similar effort, letting you colocate state, behavior, and components, while treating it as a global concern. I hope this helps you in some way!
extra - sagas
(This assumes familiarity with redux-saga. Check it out, it's the absolute bees knees.)
If you've used redux-saga, you'll notice that sagas share the same fractal-breaking problem as redux reducers - you have to declare/run them at the very start, they run for the entire lifecycle of the app, and aren't friendly with transient components.
Similar to the above, what I really want is -
- declare a saga as a component
- that 'lives' for the life of that component
- friendly with the above reducing system
tada, react-redux-saga does this! Just declare a <Saga/>
in your react tree somewhere. It looks like this -
let run = function*(getState, props){
// ...
}
// ...
<Saga saga={run} {...props}/>
This saga 'lives' while it's in the tree, and gets cancelled when it unmounts.
This gives us our own little 'event loop'/'process' for the component, with all the other goodies from redux-saga. Nice! See the mousetracking example for usage.
(A previous version of this had a *saga
definition directly in the @local
annotation, but this new method gives finer control of the input to the saga, and decouples it from this library.)
footnotes
[1] 'classes' are not fractal, because inheritance mashes methods and properties on a flat level. This gets out of hand quickly, and doesn't satisfy the big-small charecteristic of fractals. react sidesteps this problem by making an inheritance chain exactly one step deep (React.Component) - and discourages further extension, instead excouraging HOCs (higher order components) and other functional-friendly ways of composition (stateless functional components, etc). ↩
[2] Truly, this was one of the advantages of the MVC model, being able to reference components in an app easily. Sure, it led to so called 'spaghetti' code, but the mental model itself was nice (also see - MVVM, etc) ↩