├── .gitignore
├── .travis.yml
├── .babelrc
├── src
├── lib
│ └── validate-action.js
└── index.js
├── CHANGELOG.md
├── test
├── lib
│ └── validate-action.test.js
└── index.test.js
├── .vscode
└── settings.json
├── LICENSE
├── package.json
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | coverage
3 | dist
4 | node_modules
5 | *.log
6 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - v10
4 | - v8
5 | script:
6 | - yarn test --coverage
7 | cache:
8 | - yarn
9 | after_success:
10 | - bash <(curl -s https://codecov.io/bash)
11 |
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | [
4 | "@babel/preset-env",
5 | {
6 | "targets": {
7 | "node": 6
8 | }
9 | }
10 | ]
11 | ],
12 | "plugins": ["@babel/plugin-proposal-class-properties"]
13 | }
14 |
--------------------------------------------------------------------------------
/src/lib/validate-action.js:
--------------------------------------------------------------------------------
1 | const validateAction = action => {
2 | if(!action || typeof action !== 'object' || Array.isArray(action)) throw new Error('Action must be an object!');
3 |
4 | if(typeof action.type === 'undefined') throw new Error('Action must be a type!');
5 | };
6 |
7 | export default validateAction;
8 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ## [0.0.5](https://github.com/ghoshnirmalya/my-redux/compare/v0.0.3...v0.0.5) (2019-12-07)
2 |
3 |
4 |
5 | ## [0.0.3](https://github.com/ghoshnirmalya/my-redux/compare/v0.0.2...v0.0.3) (2018-10-06)
6 |
7 | ### Features
8 |
9 | - **app:** Add validations to throw error if action is incorrect ([8364a8f](https://github.com/ghoshnirmalya/my-redux/commit/8364a8f))
10 |
11 |
12 |
13 | ## 0.0.2 (2018-10-03)
14 |
15 | ### Features
16 |
17 | - **app:** Bootstrap a CRL app ([3c736e8](https://github.com/ghoshnirmalya/my-redux/commit/3c736e8))
18 |
--------------------------------------------------------------------------------
/test/lib/validate-action.test.js:
--------------------------------------------------------------------------------
1 | import validateAction from "../../src/lib/validate-action";
2 |
3 | describe("#validateAction", () => {
4 | test("should throw an error if type is not present in the argument object", () => {
5 | expect(() => {
6 | validateAction({
7 | payload: {
8 | count: 40
9 | }
10 | });
11 | }).toThrowError("Action must have a type!");
12 | });
13 |
14 | test("should throw an error if argument is not an object", () => {
15 | expect(() => {
16 | validateAction("hello world");
17 | }).toThrowError("Action must be an object!");
18 | });
19 | });
20 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import validateAction from "./lib/validate-action";
2 |
3 | const createStore = (reducer, initialState) => {
4 | const store = {};
5 |
6 | store.state = initialState;
7 | store.listeners = [];
8 | store.subscribe = listener => store.listeners.push(listener);
9 | store.dispatch = action => {
10 | validateAction(action);
11 |
12 | store.state = reducer(store.state, action);
13 | store.listeners.forEach(listener => listener(action));
14 | };
15 | store.getState = () => store.state;
16 | store.dispatch({ type: "@@redux/INIT" });
17 |
18 | return store;
19 | };
20 |
21 | export default createStore;
22 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "editor.multiCursorModifier": "ctrlCmd",
3 | "editor.snippetSuggestions": "top",
4 | "editor.formatOnPaste": true,
5 | "workbench.startupEditor": "none",
6 | "files.exclude": {
7 | ".cache": true,
8 | ".github": true,
9 | "_build": true,
10 | "node_modules": true,
11 | "deps": true,
12 | "log": true,
13 | "build": true
14 | },
15 | "editor.tabSize": 2,
16 | "files.insertFinalNewline": true,
17 | "files.trimTrailingWhitespace": true,
18 | "window.zoomLevel": 0,
19 | "editor.minimap.enabled": false,
20 | "workbench.statusBar.feedback.visible": false,
21 | "explorer.confirmDragAndDrop": false,
22 | "explorer.confirmDelete": false,
23 | "editor.wordWrap": "on",
24 | "editor.formatOnSave": true,
25 | "editor.fontSize": 12,
26 | "files.autoSave": "onFocusChange",
27 | "javascript.updateImportsOnFileMove.enabled": "always",
28 | "workbench.colorTheme": "City Lights",
29 | "javascript.validate.enable": false
30 | }
31 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Nirmalya Ghosh
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "my-redux",
3 | "version": "0.0.5",
4 | "description": "Building Redux from scratch",
5 | "license": "MIT",
6 | "repository": "ghoshnirmalya/my-redux",
7 | "main": "dist/index.js",
8 | "author": {
9 | "name": "Nirmalya Ghosh",
10 | "email": "nirmalya.email@gmail.com",
11 | "url": "https://github.com/ghoshnirmalya"
12 | },
13 | "files": [
14 | "dist",
15 | "src"
16 | ],
17 | "scripts": {
18 | "test": "jest",
19 | "coverage": "npm test -- --coverage",
20 | "postcoverage": "opn coverage/lcov-report/index.html",
21 | "clean": "rimraf dist",
22 | "prebuild": "npm run clean",
23 | "build": "babel src -d dist",
24 | "preversion": "npm test && npm run build",
25 | "version": "standard-changelog && git add CHANGELOG.md",
26 | "postpublish": "git push origin master --follow-tags"
27 | },
28 | "husky": {
29 | "hooks": {
30 | "pre-commit": "lint-staged"
31 | }
32 | },
33 | "lint-staged": {
34 | "*.{js,css,json,md}": [
35 | "prettier --write",
36 | "git add"
37 | ]
38 | },
39 | "keywords": [
40 | "redux"
41 | ],
42 | "dependencies": {},
43 | "devDependencies": {
44 | "@babel/cli": "^7.7.5",
45 | "@babel/core": "^7.7.5",
46 | "@babel/plugin-proposal-class-properties": "^7.7.4",
47 | "@babel/preset-env": "^7.7.5",
48 | "babel-core": "^7.0.0-bridge.0",
49 | "babel-eslint": "^10.0.3",
50 | "babel-jest": "^24.9.0",
51 | "husky": "^3.1.0",
52 | "jest-cli": "^24.9.0",
53 | "lint-staged": "^9.5.0",
54 | "opn-cli": "^5.0.0",
55 | "prettier": "^1.19.1",
56 | "rimraf": "^3.0.0",
57 | "standard-changelog": "^2.0.18"
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # My Redux
2 |
3 | [](https://travis-ci.org/ghoshnirmalya/building-redux-from-scratch)
4 |
5 | This app demonstrates how you can build your own version of [Redux](https://redux.js.org/). [my-redux-example](https://github.com/ghoshnirmalya/my-redux-example) is an app which is using this package to maintain it's state. You can view the whole implementation in [this file](https://github.com/ghoshnirmalya/my-redux-example/blob/master/src/App.js).
6 |
7 |
8 |
9 | ## Example usage
10 |
11 | #### Step 1: Import the createStore function from my-redux into your app
12 |
13 | ```js
14 | import createStore from "my-redux";
15 | ```
16 |
17 | #### Step 2: Create a reducer function
18 |
19 | ```js
20 | const reducer = (state = initialState, action) =>
21 | action.type === "INCREMENT"
22 | ? { count: state.count + action.payload.count }
23 | : state;
24 | ```
25 |
26 | #### Step 3: Pass that reducer function to the createStore function along with the initialState of your app
27 |
28 | ```js
29 | this.store = createStore(reducer, { count: 0 });
30 | ```
31 |
32 | #### Step 4: Dispatch an action to update your store
33 |
34 | ```js
35 | this.store.dispatch({
36 | type: "INCREMENT",
37 | payload: {
38 | count: 1
39 | }
40 | });
41 | ```
42 |
43 | You store should now reflect the updated state. You can verify that by logging the state of your store:
44 |
45 | ```js
46 | console.log(this.store.getState());
47 | ```
48 |
49 | [my-redux-example](https://github.com/ghoshnirmalya/my-redux-example) is an app which is using this package to maintain it's state. You can view the whole implementation in [this file](https://github.com/ghoshnirmalya/my-redux-example/blob/master/src/App.js).
50 |
51 | ## Development
52 |
53 | ```sh
54 | $ git clone https://github.com/ghoshnirmalya/my-redux
55 | $ cd my-redux
56 | $ yarn install
57 | ```
58 |
59 | #### Running the tests
60 |
61 | ```sh
62 | $ yarn test
63 | ```
64 |
65 | #### Getting the test coverage
66 |
67 | ```sh
68 | $ yarn coverage
69 | ```
70 |
71 | #### Building the code
72 |
73 | ```sh
74 | $ yarn build
75 | ```
76 |
77 | #### Publish
78 |
79 | ```sh
80 | $ yarn version patch|minor|major
81 | $ yarn publish
82 | ```
83 |
84 | It'll automatically run `test`, `docs`, `build` and generate CHANGELOG.md file.
85 |
86 | ## License
87 |
88 | MIT © [Nirmalya Ghosh](https://github.com/ghoshnirmalya)
89 |
--------------------------------------------------------------------------------
/test/index.test.js:
--------------------------------------------------------------------------------
1 | import createStore from "../src";
2 |
3 | describe("createStore", () => {
4 | test("should return an object", () => {
5 | const reducer = jest.fn;
6 | const store = createStore(reducer, { count: 0 });
7 |
8 | expect(typeof store).toBe("object");
9 | });
10 |
11 | test("should return a state object", () => {
12 | const reducer = jest.fn;
13 | const store = createStore(reducer, { count: 0 });
14 |
15 | expect(typeof store).toBe("object");
16 | });
17 |
18 | test("should return a listeners array", () => {
19 | const reducer = jest.fn;
20 | const store = createStore(reducer, { count: 0 });
21 |
22 | expect(typeof store.listeners).toBe("object");
23 | });
24 |
25 | test("should return a dispatch function", () => {
26 | const reducer = jest.fn;
27 | const store = createStore(reducer, { count: 0 });
28 |
29 | expect(typeof store.dispatch).toBe("function");
30 | });
31 |
32 | test("should return a subscribe function", () => {
33 | const reducer = jest.fn;
34 | const store = createStore(reducer, { count: 0 });
35 |
36 | expect(typeof store.subscribe).toBe("function");
37 | });
38 |
39 | test("should return a getState function", () => {
40 | const reducer = jest.fn;
41 | const store = createStore(reducer, { count: 0 });
42 |
43 | expect(typeof store.getState).toBe("function");
44 | });
45 |
46 | test("should update the state on passing of initialState", () => {
47 | const initialState = {
48 | count: 10
49 | };
50 | const reducer = (state = initialState, action) =>
51 | action.type === "INCREMENT"
52 | ? { count: state.count + action.payload.count }
53 | : state;
54 | const store = createStore(reducer, initialState);
55 |
56 | expect(store.getState()).toEqual({ count: 10 });
57 | });
58 |
59 | test("should add middlewares if subscribed", () => {
60 | const initialState = {
61 | count: 10
62 | };
63 | const reducer = (state = initialState, action) =>
64 | action.type === "INCREMENT"
65 | ? { count: state.count + action.payload.count }
66 | : state;
67 | const store = createStore(reducer, initialState);
68 | const exampleMiddleware = jest.fn();
69 | const anotherExampleMiddleware = jest.fn();
70 |
71 | store.subscribe(exampleMiddleware);
72 | store.subscribe(anotherExampleMiddleware);
73 |
74 | [...Array(5)].map(() =>
75 | store.dispatch({
76 | type: "INCREMENT",
77 | payload: {
78 | count: 4
79 | }
80 | })
81 | );
82 |
83 | expect(exampleMiddleware).toHaveBeenCalled();
84 | expect(anotherExampleMiddleware).toHaveBeenCalled();
85 | expect(exampleMiddleware.mock.calls.length).toEqual(5);
86 | expect(anotherExampleMiddleware.mock.calls.length).toEqual(5);
87 | });
88 |
89 | test("update the store on dispatch", () => {
90 | const initialState = {
91 | count: 0
92 | };
93 | const reducer = (state = initialState, action) =>
94 | action.type === "INCREMENT"
95 | ? { count: state.count + action.payload.count }
96 | : state;
97 | const store = createStore(reducer, initialState);
98 |
99 | [...Array(5)].map(() =>
100 | store.dispatch({
101 | type: "INCREMENT",
102 | payload: {
103 | count: 4
104 | }
105 | })
106 | );
107 |
108 | expect(store.getState()).toEqual({ count: 20 });
109 | });
110 |
111 | test("should throw an error if the type is not passed", () => {
112 | const initialState = {
113 | count: 0
114 | };
115 | const reducer = jest.fn;
116 | const store = createStore(reducer, initialState);
117 |
118 | expect(() => {
119 | store.dispatch({
120 | payload: {
121 | count: 4
122 | }
123 | });
124 | }).toThrowError("Action must have a type!");
125 | });
126 |
127 | test("should throw an error if the action is not an object", () => {
128 | const initialState = {
129 | count: 0
130 | };
131 | const reducer = jest.fn;
132 | const store = createStore(reducer, initialState);
133 |
134 | expect(() => {
135 | store.dispatch("4");
136 | }).toThrowError("Action must be an object!");
137 | });
138 | });
139 |
--------------------------------------------------------------------------------