• Stars
    star
    177
  • Rank 208,702 (Top 5 %)
  • Language
    JavaScript
  • License
    MIT License
  • Created almost 10 years ago
  • Updated over 6 years ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

🚞 action dispatcher for unidirectional data flows

barracks

NPM version build status Test coverage Downloads

Action dispatcher for unidirectional data flows. Creates tiny models of data that can be accessed with actions through a small API.

Usage

const barracks = require('barracks')

const store = barracks()

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

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

const createSend = store.start({ subscriptions: 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(hooks?)

Initialize a new barracks instance. Takes an optional object of hooks which is passed to .use().

store.use(hooks)

Register new hooks on the store. Hooks are little plugins that can extend behavior or perform actions at specific points in the life cycle. The following hooks are possible:

  • models: an array of models that will be merged with the store.
  • onError(err, state, createSend): called when an effect or subscription emit an error; if no hook is passed, the default hook will throw on each error
  • onAction(state, data, name, caller, createSend): called when an action is fired
  • onStateChange(state, data, prev, caller, createSend): called after a reducer changes the state.
  • wrapSubscriptions(fn): wraps a subscription to add custom behavior
  • wrapReducers(fn): wraps a reducer to add custom behavior
  • wrapEffects(fn): wraps an effect to add custom behavior
  • wrapInitialState(obj): mutate the initial state to add custom behavior - useful to mutate the state before starting up

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 instead of a provided callback. It then returns a send(actionName, data?) function.

The wrap* hooks are synchronously resolved when the store.start() method is called, and the corresponding values from the models are loaded. All wrap hooks (or wrappers) are passed the argument that would usually be called, so it can be wrapped or modified. Say we want to make all our reducers print 'golden pony' every time they're run, we'd do:

const barracks = require('barracks')
const store = barracks()

store.use({
  wrapReducers: function wrapConstructor (reducer) {
    return function wrapper (state, data) {
      console.log('golden pony')
      return reducer(state, data)
    }
  }
})

Hooks 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 hooks.

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 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.

store.stop()

After an app is "stopped" all subsequent send() calls become no-ops.

store.stop()
send('trimBeard') // -> does not call a reducer/effect

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 synchronously. 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: (state, data) => {
      return { count: state.count + 1 }
    },
    trimBeard: (state, data) => ({ 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 asynchronously, and then call send() again to trigger a reducer that can update the state. effects can also trigger other effects, making them fully composable. Generally, 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 propagates 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: (state, data, prev, send) => send('app:error', data)
})

app.model({
  namespace: 'app',
  effects: {
    error: (state, data, 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: (state, data) => ({ foo: state.foo + data.count })
  }
  effects: {
    fetch: (state, data, 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: (state, data) => 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 that 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 transparent in them makes for better 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 explicitly 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 ✨

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

More Repositories

1

rust-for-js-peeps

Know JS, want to try Rust, but not sure where to start? This is for you!
1,262
star
2

vmd

πŸ™ preview markdown files
JavaScript
1,175
star
3

notes

notes on things
839
star
4

tiny-guide-to-non-fancy-node

A tiny guide to non fancy, high-value Node.js things
735
star
5

futures-concurrency

Structured concurrency operations for async Rust
Rust
345
star
6

github-standard-labels

Create a standard set of issue labels for a GitHub project
JavaScript
241
star
7

sheet-router

fast, modular client-side router
JavaScript
223
star
8

pretty-hot-ranking-algorithm

Algorithm that measures how relevant a given data set is, kinda like Reddit
JavaScript
204
star
9

markdown-to-medium

Publish markdown to medium
JavaScript
198
star
10

html

Type-safe HTML support for Rust
HTML
186
star
11

virtual-html

🌴 HTML β†’ virtual-dom
JavaScript
174
star
12

dotfiles

Linux desktop config
Shell
127
star
13

es2020

Because in hindsight we don't need most of ES6
JavaScript
126
star
14

miow

A zero-overhead Windows I/O library, focusing on IOCP
Rust
109
star
15

fsm-event

🎰 stateful finite state machine
JavaScript
91
star
16

changelog

Changelog generator
Rust
85
star
17

vel

minimal virtual-dom library
JavaScript
84
star
18

fd-lock

Advisory cross-platform file locks using file descriptors
Rust
69
star
19

exponential-backoff

Exponential backoff generator with jitter.
Rust
66
star
20

memdb

Thread-safe in-memory key-value store.
Rust
64
star
21

previewify

Preview tool for applications
JavaScript
62
star
22

server-router

Server router
JavaScript
61
star
23

speaking

Slides, proposals and more for talks I give
JavaScript
57
star
24

electron-collection

Set of helper modules to build Electron applications
JavaScript
57
star
25

cache-element

Cache an HTML element that's used in DOM diffing algorithms
JavaScript
56
star
26

mdjson

πŸ“– Transform markdown to an object where headings are keys
JavaScript
55
star
27

context-attribute

Set the error context using doc comments
Rust
53
star
28

newspeak

πŸ’¬ natural language localization
JavaScript
51
star
29

copy-template-dir

High throughput template dir writes
JavaScript
51
star
30

assert-snapshot

Snapshot UI testing for tape tests
JavaScript
50
star
31

polite-element

Politely waits to render an element until the browser has spare time
JavaScript
45
star
32

choo-persist

Synchronize choo state with indexedDB
JavaScript
44
star
33

power-warn

Warn on low power level.
Rust
42
star
34

base-elements

A selection of configurable native DOM UI elements
JavaScript
41
star
35

nanostack

Small middleware stack library
JavaScript
40
star
36

millennial-js

πŸ’
CSS
39
star
37

on-intersect

Call back when an element intersects with another
JavaScript
35
star
38

github-templates

Generate .github templates
Rust
35
star
39

microcomponent

Smol event based component library
JavaScript
35
star
40

observe-resize

Trigger a callback when an element is resized
JavaScript
33
star
41

hypertorrent

Stream a torrent into a hyperdrive
JavaScript
31
star
42

npm-install-package

Install an npm package
JavaScript
30
star
43

async-iterator

An async version of iterator
Rust
30
star
44

rust-lib-template

Rust lib template repository
Rust
29
star
45

heckcheck

A heckin small test generator
Rust
29
star
46

maxstache

Minimalist mustache template replacement
JavaScript
28
star
47

normcore

No-config distributed streams using hypercore
JavaScript
28
star
48

chic

Pretty parser error reporting.
Rust
27
star
49

winstall

Install all dependencies required by a project
JavaScript
27
star
50

validate-formdata

Data structure for validating form data
JavaScript
25
star
51

playground-tide-mongodb

Example using tide + mongodb
Rust
24
star
52

playground-nanoframework

Building tiny frameworks yo
JavaScript
24
star
53

hyperlapse

Distributed process manager
JavaScript
23
star
54

promise-each

Call a function for each value in an array and return a Promise
JavaScript
23
star
55

omnom

Streaming parser extensions for BufRead
Rust
23
star
56

virtual-widget

Create a virtual-dom widget
JavaScript
22
star
57

tasky

fluent async task spawning experiments
Rust
22
star
58

debug-to-json

πŸ”§ Convert debug logs to JSON
JavaScript
22
star
59

http-sse

Create server-sent-events
JavaScript
21
star
60

kv-log-macro

Log macro for logs kv-unstable backend
Rust
21
star
61

initialize

Generate a fresh package
JavaScript
21
star
62

github-changelist

Generate a list of merged PRs since the last release
Rust
21
star
63

assert-html

Assert two HTML strings are equal
JavaScript
21
star
64

workshop-distributed-patterns

Learn how to create robust multi-server applications in Node
HTML
20
star
65

crossgen

Cross compilation template generator
Rust
20
star
66

electron-crash-report-service

Aggregate crash reports for Electron apps
JavaScript
19
star
67

pid-lite

A small PID controller library
Rust
19
star
68

playground-virtual-app

playground with some virtual-* tech
JavaScript
19
star
69

virtual-raf

Create a RAF loop for virtual-dom
JavaScript
19
star
70

promise-map

Map over an array and return a Promise.
JavaScript
19
star
71

how

how(1) - learn how to do anything
Rust
18
star
72

secure-password

Safe password hashing.
Rust
18
star
73

futures-time

async time combinators
Rust
18
star
74

microanalytics

Capture analytics events in the browser
JavaScript
18
star
75

noop2

No operation as a moduleβ„’
Makefile
18
star
76

github-to-hypercore

Stream a github event feed into a hypercore
JavaScript
17
star
77

hyperreduce

Distributed reduce on top of hypercore
JavaScript
17
star
78

microframe

Smol requestAnimationFrame package
JavaScript
17
star
79

github_auth

Authenticate with GitHub from the command line.
Rust
17
star
80

rust-cli

rust(1) cli prototype
Rust
16
star
81

virtual-streamgraph

Create a virtual-dom streamgraph
JavaScript
16
star
82

extract-html-class

Extract all classes from html
JavaScript
16
star
83

templates

Template files used to generate things
Shell
16
star
84

from2-string

Create a stream from a string. Sugary wrapper around from2
JavaScript
16
star
85

fin

Simple finance visualizations
JavaScript
16
star
86

async-collection

Collection of async functions
JavaScript
15
star
87

buffer-graph

Resolve a dependency graph for buffer creation
JavaScript
15
star
88

document-ready

Document ready listener for browsers
Rust
15
star
89

choo-pull

Wrap handlers to use pull-stream in a choo plugin
JavaScript
15
star
90

json-stream-to-object

Parse a JSON stream into an object
JavaScript
15
star
91

formdata-to-object

Convert a formData object or form DOM node to a KV object
JavaScript
14
star
92

promise-reduce

Reduce an array and return a Promise
JavaScript
14
star
93

ergonomic-viewport

Get the current ergonomic viewport
JavaScript
14
star
94

choo-model

Experimental state management lib for choo
JavaScript
14
star
95

shared-component

Share a component instance inside a window context
JavaScript
13
star
96

multipart-read-stream

Read a multipart stream over HTTP
JavaScript
13
star
97

nanopubsub

Tiny message bus
JavaScript
13
star
98

server-render

HTML server rendering middleware
JavaScript
13
star
99

const-combinations

Experiment to get k-combinations working as a const fn
Rust
13
star
100

noframeworkframework

[wip]
JavaScript
12
star