Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

barracks

Package Overview
Dependencies
Maintainers
4
Versions
63
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

barracks

Action dispatcher for unidirectional data flows

  • 8.0.2
  • Source
  • npm
  • Socket score

Version published
Weekly downloads
20
increased by33.33%
Maintainers
4
Weekly downloads
 
Created
Source

barracks

NPM version build status Test coverage Downloads

Action dispatcher for unidirectional data flows. Creates tiny models of data that can be accessed through actions with an API of only 5 functions

Usage

const barracks = require('barracks')

const store = barracks({
  onError: (err, state, createSend) => {
    console.error(`error: ${err}`)
  }),
  onAction: (data, state, name, caller, createSend) => {
    console.log(`data: ${data}`)
  })
  onStateChange: (data, state, prev, caller, createSend) => {
    console.log(`state: ${prev} -> ${state}`)
  })
})

store.model({
  namespace: 'cakes',
  state: {},
  effects: {},
  reducers: {},
  subscriptions: {}
})

const createSend = store.start({ noSubscriptions: true })
const send = createSend('myDispatcher', true)
document.addEventListener('DOMContentLoaded', () => {
  store.start() // fire up subscriptions
  const state = store.state()
  send('foo:start', { name: 'Loki' })
})

API

store = barracks(handlers)

Initialize a new barracks instance. Takes an optional object of handlers. Handlers can be:

  • onError(err, state, createSend): called when an effect or subscription emit an error. If no handler is passed, the default handler will throw on each error.
  • onAction(data, state, name, caller, createSend): called when an action is fired.
  • onStateChange(data, state, prev, caller, createSend): called after a reducer changes the state.

createSend() is a special function that allows the creation of a new named send() function. The first argument should be a string which is the name, the second argument is a boolean callOnError which can be set to true to call the onError hook istead of a provided callback. It then returns a send(actionName, data?) function.

Handlers should be used with care, as they're the most powerful interface into the state. For application level code it's generally recommended to delegate to actions inside models using the send() call, and only shape the actions inside the handlers.

store.model()

Register a new model on the store. Models are optionally namespaced objects with an initial state, and handlers for dealing with data:

  • namespace: namespace the model so that it cannot access any properties and handlers in other models
  • state: initial values of state inside the model
  • reducers: synchronous operations that modify state. Triggered by actions
  • effects: asynchronous operations that don't modify state directly. Triggered by actions, can call actions
  • subscriptions: asynchronous read-only operations that don't modify state directly. Can call actions

state within handlers is immutable through Object.freeze() and thus cannot be modified. Return data from reducers to modify state. See handler signatures for more info on the handlers.

For debugging purposes internal references to values can be inspected through a series of private accessors:

  • store._subscriptions
  • store._reducers
  • store._effects
  • store._models

state = store.state(opts)

Get the current state from the store. Opts can take the following values:

  • freeze: default: true. Set to false to not freeze state in handlers using Object.freeze(). Useful for optimizing performance in production builds.
  • state: pass in a state object that will be merged with the state returned from the store. Useful for rendering in Node.

send = createSend(name) = store.start(opts)

Start the store and get a createSend(name) function. Pass a unique name to createSend() to get a send() function. Opts can take the following values:

  • subscriptions: default: true. Set to false to not register subscriptions when starting the application. Useful to delay init functions until the DOM has loaded.
  • effects: default: true. Set to false to not register effects when starting the application. Useful when only wanting the initial state
  • reducers: default: true. Set to false to not register reducers when starting the application. Useful when only wanting the initial state

If the store has disabled any of the handlers (e.g. { reducers: false }), calling store.start() a second time will register the remaining values. This is a useful if not everything can be started at the same time (e.g. have subscriptions wait for the DOMContentLoaded event).

send(name, data?)

Send a new action to the models with optional data attached. Namespaced models can be accessed by prefixing the name with the namespace separated with a :, e.g. namespace:name.

Handler signatures

These are the signatures for the properties that can be passed into a model.

namespace

An optional string that causes state, effects and reducers to be prefixed.

app.model({
  namespace: 'users'
})

state

State can either be a value or an object of values that is used as the initial state for the application. If namespaced the values will live under state[namespace].

app.model({
  namespace: 'hey',
  state: { foo: 'bar' }
})
app.model({
  namespace: 'there',
  state: { bin: [ 'beep', 'boop' ] }
})
app.model({
  namespace: 'people',
  state: 'oi'
}})

reducers

Reducers are synchronous functions that return a value syncrhonously. No eventual values, just values that are relevant for the state. It takes two arguments of data and state. data is the data that was emitted, and state is the current state. Each action has a name that can be accessed through send(name), and when under a namespace can be accessed as send(namespace:name). When operating under a namespace, reducers only have access to the state within the namespace.

// some model
app.model({
  namespace: 'plantcake',
  state: {
    enums: [ 'veggie', 'potato', 'lettuce' ]
    paddie: 'veggie'
  }
})

// so this model can't access anything in the 'plantcake' namespace
app.model({
  namespace: 'burlybeardos',
  state: { count: 1 },
  reducers: {
    feedPlantcake: (data, state) => {
      return { count: state.count + 1 }
    },
    trimBeard: (data, state) => ({ count: state.count - 1 })
  }
})

effects

effects are asynchronous methods that can be triggered by actions in send(). They never update the state directly, but can instead do thing asyncrhonously, and then call send() again to trigger a reducer that can update the state. effects can also trigger other effects, making them fully composable. Generalyy it's recommended to only have effects without a namespace call other effects, as to keep namespaced models as isolated as possible.

When an effect is done executing, or encounters an error, it should call the final done(err) callback. If the effect was called by another effect it will call the callback of the caller. When an error propegates all the way to the top, the onError handler will be called, registered in barracks(handlers). If no callback is registered, errors will throw.

Having callbacks in effects means that error handling can be formalized without knowledge of the rest of the application leaking into the model. This also causes effects to become fully composable, which smooths parallel development in large teams, and keeps the mental overhead low when developing a single model.

const http = require('xhr')
const app = barracks({
  onError: (data, state, prev, send) => send('app:error', data)
})

app.model({
  namespace: 'app',
  effects: {
    error: (data, state, send, done) => {
      // if doing http calls here be super sure not to get lost
      // in a recursive error handling loop: remember this IS
      // the error handler
      console.error(data.message)
      done()
    }
  }
})

app.model({
  namespace: 'foo',
  state: { foo: 1 },
  reducers: {
    moreFoo: (data, state) => ({ foo: state.foo + data.count })
  }
  effects: {
    fetch: (data, state, send, done) => {
      http('foobar.com', function (err, res, body) {
        if (err || res.statusCode !== 200) {
          return done(new Error({
            message: 'error accessing server',
            error: err
          }))
        } else {
          send('moreFoo', { count: foo.count }, done)
        }
      })
    }
  }
})

subscriptions

subscriptions are read-only sources of data. This means they cannot be triggered by actions, but can emit actions themselves whenever they want. This is useful for stuff like listening to keyboard events or incoming websocket data. They should generally be started when the application is loaded, using the DOMContentLoaded listener.

app.model({
  subscriptions: {
    emitWoofs: (send, done) => {
      // emit a woof every second
      setInterval(() =>  send('printWoofs', { woof: 'meow?' }, done), 1000)
    }
  },
  effects: {
    printWoofs: (data, state) => console.log(data.woof)
  }
})

done() is passed as the final argument so if an error occurs in a subscriber, it can be communicated to the onError hook.

FAQ

What is an "action dispatcher"?

An action dispatcher gets data from one place to another without tightly coupling code. The best known use case for this is in the flux pattern. Say you want to update a piece of data (for example a user's name), instead of directly calling the update logic inside the view the action calls a function that updates the user's name for you. Now all the views that need to update a user's name can call the same action and pass in the relevant data. This pattern tends to make views more robust and easier to maintain.

Why did you build this?

Passing messages around should not be complicated. Many flux implementations casually throw restrictions at users without having a clear architecture. I don't like that. barracks is a package creates a clear flow of data within an application, concerning itself with state, code separation, and data flow. I believe that having strong opinions and being transparant in them makes for architectures than sprinkles of opinions left and right, without a cohesive story as to why.

How is this different from choo?

choo is a framework that handles views, data and all problems related to that. This is a package that only concerns itself with data flow, without being explicitely tied to the DOM.

This looks like more than five functions!

Welllll, no. It's technically five functions with a high arity, hah. Nah, you're right - but five functions sounds good. Besides: you don't need to know all options and toggles to get this working; that only relevant once you start hitting edge cases like we did in choo :sparkles:

See Also

  • choo - sturdy frontend framework
  • sheet-router - fast, modular client-side router
  • yo-yo - template string based view framework
  • send-action - unidirectional action emitter

Installation

$ npm install barracks

License

MIT

Keywords

FAQs

Package last updated on 08 Jul 2016

Did you know?

Socket

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Install

Related posts

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc