├── .babelrc ├── .gitignore ├── .travis.yml ├── README.md ├── dist └── redux-create-action-types.js ├── index.js ├── package.json └── test └── index.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015"] 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - stable 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | redux-create-action-types 2 | ------------------------- 3 | 4 | Helps you create Redux action types, safely & easily 5 | 6 | [![Build Status](https://travis-ci.org/tbranyen/redux-create-action-types.svg?branch=master)](https://travis-ci.org/tbranyen/redux-create-action-types) 7 | 8 | ## Motivation 9 | 10 | You write Redux boilerplate every-single-day and this sucks. 11 | 12 | ``` js 13 | export FETCHING_RESOURCE_FRAMES = 'FETCHING_RESOURCE_FRAMES'; 14 | export FAILED_FETCHING_RESOURCE_FRAMES = 'FAILED_FETCHING_RESOURCE_FRAMES'; 15 | export FETCHED_RESOURCE_FRAMES = 'FETCHED_RESOURCE_FRAMES'; 16 | export FETCHING_RESOURCE_IMAGE = 'FETCHING_RESOURCE_IMAGE'; 17 | export FAILED_FETCHING_RESOURCE_IMAGE = 'FAILED_FETCHING_RESOURCE_IMAGE'; 18 | export FETCHED_RESOURCE_IMAGE = 'FETCHED_RESOURCE_IMAGE'; 19 | ``` 20 | 21 | One of the interesting design decisions around Redux, to me, was that it used 22 | plain strings as action type identifiers. This decision is not strictly 23 | imposed, but it does offer good serialization/deserialization, visual clues, 24 | and works very well inside `switch` statements which are a defining trait of 25 | Redux reducers. 26 | 27 | The downsides of strings are that they are completely free-form and not 28 | validated in any way besides your unit tests. Nothing guarantees your types 29 | will be unique (except for you and your well coordinated team), and this could 30 | introduce very strange behavior in your reducers when two actions are 31 | dispatched with the same type, but different action payloads. Lastly, they are 32 | very tedious to write out. You typically want to export them with the same name 33 | they represent, which incurs twice the typing. For instance: `export const 34 | MY_ACTION_TYPE = 'MY_ACTION_TYPE'` 35 | 36 | There are many common solutions to some of these problems, which I'll outline 37 | below, but no solution (that I'm aware of) that provides the beneficial niche 38 | features: 39 | 40 | ### [Symbols](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol) 41 | 42 | Symbols are string-like objects in JavaScript that are guaranteed unique, work 43 | well with `switch` statements, are easy to write, and can serialize to plain 44 | text strings. They seem like the perfect & ideal match for this use case. While 45 | they break down with the same limitations of strings in terms of verbosity, 46 | they really break down when it comes to Redux middleware (logger needs a 47 | special serializer) and with being able to playback your actions from a saved 48 | session (no way to deserialize back into the correct Symbol). 49 | 50 | ### [keyMirror](https://github.com/STRML/keyMirror) 51 | 52 | A Node module that helps with the tediousness of defining mirrored key/value 53 | pairs on an object. It works really well, but suffers from not throwing during 54 | development when you access non-existent keys. 55 | 56 | ## What this module does for you 57 | 58 | - Provides a very clear way of defining multiple action types 59 | 60 | - In development, will throw when accessing types that were not previously declared 61 | - In development, will throw when using the same type more than once 62 | - In development, will throw when assigning a non-string type 63 | - In development, will throw when assigning anything to the types object 64 | 65 | - In production, silences all errors and does nothing fancy other than a single 66 | loop that turns your strings into key/values on a plain object. 67 | 68 | ## How to use 69 | 70 | Before using, you'll need to install via [npm](https://npmjs.com) or 71 | [yarn](https://yarnpkg.com): 72 | 73 | ``` sh 74 | # Sorry for the long names, but I was late to the party... 75 | npm install redux-create-action-types 76 | yarn install redux-create-action-types 77 | ``` 78 | 79 | Then you can import using ES Modules: 80 | 81 | ``` js 82 | import createTypes from 'redux-create-action-types' 83 | ``` 84 | 85 | or CJS, if you like: 86 | 87 | ``` js 88 | const createTypes = require('redux-create-action-types') 89 | ``` 90 | 91 | Now you can create your types objects: 92 | 93 | ``` js 94 | const Types = createTypes( 95 | 'FETCHING_RESOURCE', 96 | 'FETCHED_RESOURCE', 97 | 'FAILED_FETCHING_RESOURCE' 98 | ) 99 | ``` 100 | 101 | For all intents, it will return a plain object that looks like this: 102 | 103 | ``` js 104 | { 105 | 'FETCHING_RESOURCE': 'FETCHING_RESOURCE', 106 | 'FETCHED_RESOURCE': 'FETCHED_RESOURCE', 107 | 'FAILED_FETCHING_RESOURCE': 'FAILED_FETCHING_RESOURCE', 108 | } 109 | ``` 110 | 111 | ## Eliminate `undefined` types 112 | 113 | The special features of this module are only noticeable during development. For 114 | instance if you were writing a reducer and tried to access a type that was 115 | never defined before: 116 | 117 | ``` js 118 | // This would be defined in another file... 119 | const Types = createTypes( 120 | 'FETCHING_RESOURCE', 121 | 'FETCHED_RESOURCE', 122 | 'FAILED_FETCHING_RESOURCE' 123 | ) 124 | 125 | // A typically reducer. 126 | function resource(state = {}, action) { 127 | switch (action.type) { 128 | case Types.FETCHING_SOME_RESOURCE: { 129 | return Object.assign({}, state, action) 130 | } 131 | 132 | default: { return state } 133 | } 134 | } 135 | ``` 136 | 137 | The above will throw an error in development, because you've tried to access a 138 | property that was never defined. Had you not used this, it's possible for an 139 | undefined type to match your case check and put your app into an inconsistent 140 | state. 141 | 142 | ## Prevent duplicate values 143 | 144 | While keyMirror and this module make it easy to keep your key and values 145 | consistent, the same can not be said with simple objects. The following will 146 | show how this module prevents duplicate values: 147 | 148 | ``` js 149 | const Types = createTypes( 150 | "TYPE_ONE" 151 | ); 152 | 153 | // Produces: { "TYPE_ONE": "TYPE_ONE" } 154 | ``` 155 | 156 | If you attempt to modify this value, not that you would, but mistakes happen: 157 | 158 | ``` js 159 | Types.TYPE_ONE = 'TYPE_TWO'; 160 | ``` 161 | 162 | This will immediately error in development, letting you know something tried to 163 | assign to this object. Since this module uses a new JavaScript feature, called 164 | proxies, it can prevent all property setting behind an exception being thrown. 165 | 166 | Another way this prevents duplicates is through use of a global cache: 167 | 168 | ``` js 169 | // In one file... 170 | const Types = createTypes( 171 | "TYPE_ONE" 172 | ); 173 | 174 | // In another file... 175 | const Types = createTypes( 176 | "TYPE_ONE" 177 | ); 178 | ``` 179 | 180 | The above will error as you have already used a type, and the system will not 181 | let you reuse it, since your actions are dispatched to all reducers. 182 | -------------------------------------------------------------------------------- /dist/redux-create-action-types.js: -------------------------------------------------------------------------------- 1 | (function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.ReactTypes = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o { 48 | if (!inProduction && GLOBAL_CACHE.has(type)) { 49 | throw new Error(`${type} has already been defined as an action type`); 50 | } 51 | 52 | if (!inProduction && typeof type !== 'string') { 53 | throw new Error(`${type} is of an invalid type, expected string`); 54 | } 55 | 56 | TYPES[type] = type; 57 | GLOBAL_CACHE.add(type); 58 | }); 59 | 60 | // We set the `set` hook after we initially set our properties, this seals 61 | // the object in a way that `Object.freeze` cannot (unless the source code is 62 | // in strict mode, which is not a guarentee). 63 | handler.set = (o, k) => { 64 | throw new Error(`Failed setting ${k}, object is frozen`); 65 | }; 66 | 67 | return TYPES; 68 | }; 69 | 70 | // Allows the outside user to clear the global cache state. 71 | module.exports.clearGlobalCache = () => GLOBAL_CACHE.clear(); 72 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redux-create-action-types", 3 | "version": "2.1.0", 4 | "description": "Easily create immutable, strict, and well formed action types", 5 | "main": "dist/redux-create-action-types.js", 6 | "scripts": { 7 | "test": "mocha", 8 | "build": "browserify -t babelify --no-builtins -s ReactTypes index.js | derequire > dist/redux-create-action-types.js" 9 | }, 10 | "keywords": [ 11 | "redux" 12 | ], 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/tbranyen/redux-create-action-types.git" 16 | }, 17 | "author": "Tim Branyen (@tbranyen)", 18 | "license": "MIT", 19 | "devDependencies": { 20 | "babel": "^6.5.2", 21 | "babel-preset-latest": "^6.16.0", 22 | "babelify": "^7.3.0", 23 | "browserify": "^13.1.1", 24 | "derequire": "^2.0.3", 25 | "mocha": "^3.1.2" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | const { deepEqual, throws, doesNotThrow } = require('assert'); 2 | const createTypes = require('../'); 3 | 4 | describe('createTypes', function() { 5 | afterEach(() => createTypes.clearGlobalCache()); 6 | 7 | describe('in development', () => { 8 | beforeEach(() => process.env.NODE_ENV = 'test'); 9 | 10 | it('will throw an error when no arguments are passed', () => { 11 | throws(() => createTypes()); 12 | }); 13 | 14 | it('will throw an error when a non-string argument is passed', () => { 15 | throws(() => createTypes({})); 16 | }); 17 | 18 | it('will throw if you attempt to access a non-existent type', () => { 19 | const types = createTypes('Test'); 20 | 21 | throws(() => { 22 | types.TEST; 23 | }); 24 | }); 25 | 26 | it('will throw if you assign a value to the types object', () => { 27 | const types = createTypes('Test'); 28 | throws(() => types.TEST = 'test'); 29 | }); 30 | 31 | it('will throw if you reuse the same key twice', () => { 32 | createTypes('test'); 33 | throws(() => createTypes('test')); 34 | }); 35 | }); 36 | 37 | describe('in production', () => { 38 | beforeEach(() => process.env.NODE_ENV = 'production'); 39 | 40 | it('will not throw an error when no arguments are passed', () => { 41 | doesNotThrow(() => createTypes()); 42 | }); 43 | 44 | it('will not throw an error when a non-string argument is passed', () => { 45 | doesNotThrow(() => createTypes({})); 46 | }); 47 | 48 | it('will not throw if you attempt to access a non-existent type', () => { 49 | const types = createTypes('Test'); 50 | doesNotThrow(() => types.TEST); 51 | }); 52 | 53 | it('will not throw if you assign a value to the types object', () => { 54 | const types = createTypes('Test'); 55 | doesNotThrow(() => types.TEST = 'test'); 56 | }); 57 | 58 | it('will not throw if you reuse the same key twice', () => { 59 | createTypes('test'); 60 | doesNotThrow(() => createTypes('test')); 61 | }); 62 | }); 63 | 64 | it('will return an object containing the type as key and value', () => { 65 | const actual = createTypes('test2'); 66 | deepEqual(actual, { 'test2': 'test2' }); 67 | }); 68 | }); 69 | --------------------------------------------------------------------------------