A Go port of redux-immutable-state-invariant.
Redux-style middleware that panics with an error when you try to mutate your state either inside a dispatch or between dispatches. For development use only!
Because you're not allowed to mutate your state in your reducers! And by extension, you shouldn't mutate it outside them either. To change state in your app you should always return a new instance of your state with the changes applied.
If you use persistent/immutable data structures this is handled for you. But if
you use plain objects and arrays (modeled here by *invariant.Obj and
*invariant.Arr), you need to be careful to avoid mutations — and this
middleware helps you catch them.
Go has no single dynamic "any JSON value" type with JavaScript's reference
semantics, so this port models state with a small Value type set that mirrors
JS reference identity (===):
*invariant.Obj— an ordered string-keyed object (containers compared by pointer identity, like JS objects).*invariant.Arr— an ordered list (compared by pointer identity, like JS arrays).invariant.Str,invariant.Num,invariant.Bool— primitives (compared by value).invariant.Null,invariant.Undefined— the null / undefined singletons.invariant.NaN()— aNaNnumber; like JS,NaNnever equals itself.
*Obj preserves insertion order of keys and offers Set/Get/Delete/Keys/
Clone; *Arr offers Push/Len/At/SetAt/Keys/Clone.
go get github.com/leoasis/redux-immutable-state-invariantThis is intended for use only during development. It copies and walks your state on every dispatch and will degrade performance — do not use it in production.
Add it as middleware in your (Redux-style) store setup. The middleware has the
shape func(MiddlewareAPI) func(next Dispatch) Dispatch:
import invariant "github.com/leoasis/redux-immutable-state-invariant"
mw := invariant.New(nil) // nil options == all defaults
api := invariant.MiddlewareAPI{GetState: func() invariant.Value { return store.State() }}
dispatch := mw(api)(next) // next is your base dispatchIf you're doing things correctly you'll see nothing different. But if you mutate
your state somewhere — inside a dispatch or between dispatches — the middleware
panics with an *invariant.InvariantError carrying a descriptive message
(including the mutated path, e.g. `foo.bar.3`).
A complete runnable example lives in example/main.go; run it
with:
go run ./exampleCreates the middleware. Passing nil is equivalent to {} in JS and selects
all defaults. Options customizes behavior:
-
IsImmutable func(Value) bool— specify whether a value should be treated as immutable. The default (IsImmutableDefault, selected when this isnil) returnstruefor primitives (Str,Num,Bool,Null,Undefined) andfalsefor*Obj/*Arr. Useful if some of your state uses persistent structures you want the middleware to skip. -
Ignore []string— branch(es) of the state object to ignore when detecting mutations. Each element is a dot-separated "path" string matching named nodes from the root state.// ignore mutation detection along the 'foo' & 'bar.thingsToIgnore' branches mw := invariant.New(&invariant.Options{ Ignore: []string{"foo", "bar.thingsToIgnore"}, })
invariant.TrackForMutations(isImmutable, ignore, obj) returns a *Tracker
whose DetectMutations() reports whether the tracked object was mutated since
construction, returning a MutationResult{WasMutated bool, Path []string}. This
is the primitive the middleware is built on and is exported for direct use and
testing.
- Thrown JS
Errors become panics with*invariant.InvariantError(message prefixedInvariant Violation:, identical text otherwise). - JS objects/arrays are modeled by the
Valuetypes described above to preserve reference-identity semantics. - The circular-safe action serialization used in the inside-dispatch message is
a port of
json-stringify-safe(circular references render as[Circular ~]).
Behavior is otherwise identical; the full original test suite (8 middleware + 25 tracker cases) is ported to Go and passes.
MIT — see LICENSE.md. Copyright (c) 2015 Leonardo Andres Garcia Crespo.